import { addNodeToHostGraph, createHostNode, getFirstSelectedHostCanvasNode, getHostGraph, importWorkflowIntoHostCanvas, refreshHostCanvasGraph, } from "../../../app/hostAdapter.js"; import { copyTextToClipboard } from "../../../utils/dom.js"; import { writeWidgetValue } from "../workflowSidebar/widgetAdapters.js"; import { getNodeType, getNodeParamEntries, getNodeWidgetValueEntries, } from "./workflowGraphMapData.js"; type LooseRecord = Record; type WidgetRecord = LooseRecord; interface TransferResult { ok: boolean; count: number; reason: "no-target" | "no-widgets" | "ok" | "no-match"; targetNode?: LooseRecord; } export async function copyNodeJson(node: any): Promise { if (!node) return false; const text = JSON.stringify(_prepareNodeForExternalUse(node), null, 2); return copyTextToClipboard(text); } export async function copyNodeParamValue(value: any): Promise { return copyTextToClipboard(formatParamClipboardValue(value)); } export function formatParamClipboardValue(value: any): string { if (value == null) return ""; if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); try { return JSON.stringify(value, null, 2); } catch { return String(value); } } export function importWorkflowToCanvas(workflow: any): boolean { return importWorkflowIntoHostCanvas(workflow); } export function importNodeToCurrentGraph(node: LooseRecord | null | undefined): boolean { const graph = getHostGraph(); if (!node || !graph || typeof graph.add !== "function") return false; const nodeType = String(node?.type || node?.class_type || node?.comfyClass || "").trim(); let created: LooseRecord | null = null; try { created = createHostNode(nodeType) as LooseRecord | null; } catch (e: any) { console.debug?.("[MFV Graph Map] createNode failed", e); } if (!created) return false; try { const clone = typeof structuredClone === "function" ? structuredClone(node) : JSON.parse(JSON.stringify(node)); _normalizeExternalNodeClone(clone, node); delete clone.id; if (Array.isArray(clone.pos)) { clone.pos = [Number(clone.pos[0] || 0) + 32, Number(clone.pos[1] || 0) + 32]; } if (typeof created.configure === "function") { created.configure(clone); } else { Object.assign(created, clone); } return addNodeToHostGraph(created); } catch (e: any) { console.debug?.("[MFV Graph Map] import node failed", e); return false; } } export function transferNodeParamsToSelectedCanvasNode( sourceNode: LooseRecord | null | undefined, ): TransferResult { const targetNode = getSingleSelectedCanvasNode(); if (!sourceNode || !targetNode) return { ok: false, count: 0, reason: "no-target" }; const sourceEntries = _getTransferParamEntries(sourceNode); const targetWidgets = Array.isArray(targetNode.widgets) ? targetNode.widgets : []; if (!sourceEntries.length || !targetWidgets.length) { return { ok: false, count: 0, reason: "no-widgets" }; } const targetByName = new Map(); targetWidgets.forEach((widget, index) => { for (const key of _widgetKeys(widget)) { if (!targetByName.has(key)) targetByName.set(key, { widget, index }); } }); const sourceType = _normalizeType(getNodeType(sourceNode)); const targetType = _normalizeType(targetNode?.type || targetNode?.comfyClass || targetNode?.class_type); const allowPositionFallback = Boolean(sourceType && targetType && sourceType === targetType); const usedTargets = new Set(); let count = 0; for (const entry of sourceEntries) { const key = _normalizeName(entry.label); let target = key ? targetByName.get(key) : null; if ((!target || usedTargets.has(target.index)) && allowPositionFallback && Number.isInteger(entry.index)) { const positional = targetWidgets[entry.index]; if (positional) target = { widget: positional, index: entry.index }; } if (!target || usedTargets.has(target.index)) continue; if (writeWidgetValue(target.widget, _cloneValue(entry.value), targetNode)) { usedTargets.add(target.index); count += 1; } } refreshHostCanvasGraph(); return { ok: count > 0, count, reason: count > 0 ? "ok" : "no-match", targetNode, }; } export function getSingleSelectedCanvasNode(): LooseRecord | null { return (getFirstSelectedHostCanvasNode() as LooseRecord | null) || null; } function _getTransferParamEntries(sourceNode: LooseRecord): Array<{ label: any; value: any; index?: number }> { const paramEntries = getNodeParamEntries(sourceNode); if (Array.isArray(paramEntries) && paramEntries.length) { return paramEntries.map(([label, value]) => ({ label, value })); } return getNodeWidgetValueEntries(sourceNode); } function _prepareNodeForExternalUse(node: LooseRecord): LooseRecord { const clone = typeof structuredClone === "function" ? structuredClone(node) : JSON.parse(JSON.stringify(node)); _normalizeExternalNodeClone(clone, node); return clone; } function _normalizeExternalNodeClone(clone: LooseRecord, sourceNode: LooseRecord) { const promptInputs = sourceNode?._mjrPromptInputs && typeof sourceNode._mjrPromptInputs === "object" ? sourceNode._mjrPromptInputs : null; if (promptInputs) { const normalizedWidgets = _buildKnownWidgetValuesFromPrompt(sourceNode, promptInputs); if (normalizedWidgets) clone.widgets_values = normalizedWidgets; } delete clone._mjrPromptInputs; delete clone._mjrSubgraphProxyParams; } function _buildKnownWidgetValuesFromPrompt(sourceNode: LooseRecord, promptInputs: LooseRecord): any[] | null { const type = _normalizeType(getNodeType(sourceNode)); const current = getNodeWidgetValueEntries(sourceNode); const currentByLabel = new Map(current.map((entry) => [_normalizeName(entry.label), entry.value])); if (type.includes("ksamplerselect")) { if (promptInputs.sampler_name == null) return null; return [_cloneValue(promptInputs.sampler_name)]; } if (type.includes("ksampler")) { const controlAfterGenerate = promptInputs.control_after_generate ?? currentByLabel.get("control_after_generate") ?? current[1]?.value; return [ _cloneValue(promptInputs.seed), _cloneValue(controlAfterGenerate), _cloneValue(promptInputs.steps), _cloneValue(promptInputs.cfg), _cloneValue(promptInputs.sampler_name), _cloneValue(promptInputs.scheduler), _cloneValue(promptInputs.denoise), ]; } return null; } function _widgetKeys(widget: WidgetRecord): string[] { return [widget?.name, widget?.label, widget?.options?.name, widget?.options?.label] .map(_normalizeName) .filter(Boolean); } function _normalizeName(value: any): string { return String(value ?? "") .trim() .toLowerCase() .replace(/\s+/g, "_"); } function _normalizeType(value: any): string { return String(value ?? "").trim().toLowerCase(); } function _cloneValue(value: any): any { if (value == null || typeof value !== "object") return value; try { return typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value)); } catch { return value; } }