mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-25 04:21:17 -03:00
feat: add Node Marker system with right-click marking
This commit is contained in:
126
web/comfyui/node_marker.js
Normal file
126
web/comfyui/node_marker.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
// =============================================================================
|
||||
// Node Marker – right-click node marking (no dedicated node required)
|
||||
//
|
||||
// Adds a "Mark as →" submenu with role options to any node's context menu.
|
||||
// Roles are stored in ``node.properties.lm_marker_role`` and automatically
|
||||
// persist with the workflow JSON.
|
||||
//
|
||||
// The workflow registry reads these markers and makes them available to the
|
||||
// standalone UI (e.g. ``sendEmbeddingToWorkflow`` also considers nodes marked
|
||||
// as ``send_prompt_target``).
|
||||
// =============================================================================
|
||||
|
||||
const ROLES = {
|
||||
send_prompt_target: {
|
||||
label: "Send Prompt Target",
|
||||
emoji: "\uD83D\uDCDD",
|
||||
},
|
||||
send_gen_params: {
|
||||
label: "Send Gen Params Target",
|
||||
emoji: "\uD83D\uDD27",
|
||||
},
|
||||
};
|
||||
|
||||
// ---- Helpers ----------------------------------------------------------------
|
||||
|
||||
function getMarker(node) {
|
||||
return node?.properties?.lm_marker_role ?? null;
|
||||
}
|
||||
|
||||
function setMarker(node, roleKey) {
|
||||
if (!node || !ROLES[roleKey]) return;
|
||||
node.properties = node.properties || {};
|
||||
node.properties.lm_marker_role = roleKey;
|
||||
|
||||
// Save original title if not already saved, then prefix with emoji
|
||||
if (!node.properties.lm_marker_original_title) {
|
||||
node.properties.lm_marker_original_title = node.title || "";
|
||||
}
|
||||
const def = ROLES[roleKey];
|
||||
node.title = `${def.emoji} ${node.properties.lm_marker_original_title}`;
|
||||
|
||||
if (typeof node.setDirtyCanvas === "function") {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
triggerRegistryRefresh();
|
||||
}
|
||||
|
||||
function clearMarker(node) {
|
||||
if (!node) return;
|
||||
delete node.properties.lm_marker_role;
|
||||
|
||||
// Restore original title: prefer stripping emoji from current title
|
||||
// (captures user renames after marking), fall back to saved original.
|
||||
const cleaned = node.title?.replace(
|
||||
/^(\u2709\uFE0F?|\u2699\uFE0F?|\uD83D\uDCDD|\uD83C\uDF9B\uFE0F?|\uD83D\uDD27)\s*/,
|
||||
''
|
||||
);
|
||||
if (cleaned && cleaned !== node.title) {
|
||||
node.title = cleaned;
|
||||
} else {
|
||||
const orig = node.properties.lm_marker_original_title;
|
||||
if (orig !== undefined) {
|
||||
node.title = orig;
|
||||
}
|
||||
}
|
||||
delete node.properties.lm_marker_original_title;
|
||||
|
||||
if (typeof node.setDirtyCanvas === "function") {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
triggerRegistryRefresh();
|
||||
}
|
||||
|
||||
function triggerRegistryRefresh() {
|
||||
// workflow_registry.js listens for this event to re-scan the graph.
|
||||
window.dispatchEvent(new CustomEvent("lm_marker_changed"));
|
||||
}
|
||||
|
||||
// ---- Submenu builder --------------------------------------------------------
|
||||
|
||||
function buildSubmenuOptions(node) {
|
||||
const currentRole = getMarker(node);
|
||||
const options = [];
|
||||
|
||||
for (const [key, def] of Object.entries(ROLES)) {
|
||||
const isActive = currentRole === key;
|
||||
options.push({
|
||||
content: `${isActive ? "\u2713 " : ""}${def.label}`,
|
||||
disabled: isActive,
|
||||
callback: () => setMarker(node, key),
|
||||
});
|
||||
}
|
||||
|
||||
if (currentRole) {
|
||||
options.push({
|
||||
content: "Clear marker",
|
||||
callback: () => clearMarker(node),
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function buildMenuItems(node) {
|
||||
return [
|
||||
null,
|
||||
{
|
||||
content: "Mark as",
|
||||
has_submenu: true,
|
||||
submenu: {
|
||||
options: buildSubmenuOptions(node),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ---- Extension --------------------------------------------------------------
|
||||
|
||||
app.registerExtension({
|
||||
name: "LoraManager.NodeMarker",
|
||||
getNodeMenuItems(node) {
|
||||
return buildMenuItems(node);
|
||||
},
|
||||
});
|
||||
@@ -28,6 +28,11 @@ app.registerExtension({
|
||||
api.addEventListener("lm_widget_update", (event) => {
|
||||
this.applyWidgetUpdate(event?.detail ?? {});
|
||||
});
|
||||
|
||||
// React to marker changes from the Node Marker extension
|
||||
window.addEventListener("lm_marker_changed", () => {
|
||||
this.refreshRegistry();
|
||||
});
|
||||
},
|
||||
|
||||
async refreshRegistry() {
|
||||
@@ -49,8 +54,10 @@ app.registerExtension({
|
||||
const supportsLora = LORA_NODE_CLASSES.has(node.comfyClass);
|
||||
const hasTargetWidget = widgetNames.some((name) => TARGET_WIDGET_NAMES.has(name));
|
||||
const hasTextWidget = TEXT_CAPABLE_CLASSES.has(node.comfyClass);
|
||||
const markerRole = node.properties?.lm_marker_role ?? null;
|
||||
|
||||
if (!supportsLora && !hasTargetWidget && !hasTextWidget) {
|
||||
// Skip nodes with no relevant capability UNLESS they are marked
|
||||
if (!supportsLora && !hasTargetWidget && !hasTextWidget && !markerRole) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -71,6 +78,7 @@ app.registerExtension({
|
||||
type: node.comfyClass,
|
||||
comfy_class: node.comfyClass,
|
||||
mode: node.mode,
|
||||
marker_role: markerRole,
|
||||
capabilities: {
|
||||
supports_lora: supportsLora,
|
||||
has_text_widget: hasTextWidget,
|
||||
@@ -102,11 +110,12 @@ app.registerExtension({
|
||||
applyWidgetUpdate(message) {
|
||||
const nodeId = message?.node_id ?? message?.id;
|
||||
const graphId = message?.graph_id;
|
||||
const action = message?.action;
|
||||
const widgetName = message?.widget_name;
|
||||
const value = message?.value;
|
||||
const mode = message?.mode ?? "replace";
|
||||
|
||||
if (nodeId == null || !widgetName) {
|
||||
if (nodeId == null || (!action && !widgetName)) {
|
||||
console.warn("LoRA Manager: invalid widget update payload", message);
|
||||
return;
|
||||
}
|
||||
@@ -126,33 +135,72 @@ app.registerExtension({
|
||||
return;
|
||||
}
|
||||
|
||||
const widgetIndex = node.widgets.findIndex((widget) => widget?.name === widgetName);
|
||||
if (widgetIndex === -1) {
|
||||
console.warn(
|
||||
"LoRA Manager: target widget not found on node",
|
||||
widgetName,
|
||||
node
|
||||
);
|
||||
// ---- Resolve target widget ----
|
||||
let targetWidget = null;
|
||||
|
||||
if (action === "inject_text") {
|
||||
// Find the first text-capable widget by type.
|
||||
// Normalise to lowercase for case-insensitive matching.
|
||||
const TEXT_TYPES = new Set(["string", "customtext"]);
|
||||
targetWidget = node.widgets.find((w) => {
|
||||
const t = typeof w?.type === "string" ? w.type.toLowerCase() : "";
|
||||
if (TEXT_TYPES.has(t)) return true;
|
||||
// Broad fallback for unknown composite types.
|
||||
if (t.includes("string")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!targetWidget) {
|
||||
// Last resort: pick the first widget that is not a hidden/internal type
|
||||
targetWidget = node.widgets.find((w) => w?.name && !w.name.startsWith("_"));
|
||||
if (!targetWidget) {
|
||||
console.warn(
|
||||
"LoRA Manager: no suitable widget for inject_text on node",
|
||||
node.id
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (widgetName) {
|
||||
// Legacy: find widget by name
|
||||
targetWidget = node.widgets.find((w) => w?.name === widgetName);
|
||||
if (!targetWidget) {
|
||||
console.warn(
|
||||
"LoRA Manager: target widget not found on node",
|
||||
widgetName,
|
||||
node
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.warn("LoRA Manager: no action or widget_name in payload", message);
|
||||
return;
|
||||
}
|
||||
|
||||
const widget = node.widgets[widgetIndex];
|
||||
// ---- Update widget value ----
|
||||
const widgetIndex = node.widgets.indexOf(targetWidget);
|
||||
let newValue = value;
|
||||
|
||||
if (mode === "append") {
|
||||
const separator = widget.value && widget.value.length > 0 ? " " : "";
|
||||
newValue = widget.value + separator + value;
|
||||
const separator =
|
||||
targetWidget.value && targetWidget.value.length > 0 ? " " : "";
|
||||
newValue = targetWidget.value + separator + value;
|
||||
}
|
||||
|
||||
widget.value = newValue;
|
||||
targetWidget.value = newValue;
|
||||
|
||||
if (Array.isArray(node.widgets_values) && node.widgets_values.length > widgetIndex) {
|
||||
if (
|
||||
Array.isArray(node.widgets_values) &&
|
||||
widgetIndex >= 0 &&
|
||||
node.widgets_values.length > widgetIndex
|
||||
) {
|
||||
node.widgets_values[widgetIndex] = newValue;
|
||||
}
|
||||
|
||||
if (typeof widget.callback === "function") {
|
||||
if (typeof targetWidget.callback === "function") {
|
||||
try {
|
||||
widget.callback(newValue);
|
||||
targetWidget.callback(newValue);
|
||||
} catch (callbackError) {
|
||||
console.error("LoRA Manager: widget callback failed", callbackError);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user