diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png index 1cdaa2820c..8f97af570e 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png index e36ac821bf..ae40b41501 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png differ diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index 2ec3303727..1718b80e32 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -524,17 +524,6 @@ export const CORE_SETTINGS: SettingParams[] = [ defaultValue: true, versionAdded: '1.3.42' }, - { - id: 'Comfy.RerouteBeta', - category: ['LiteGraph', 'RerouteBeta'], - name: 'Opt-in to the reroute beta test', - tooltip: 'No longer has any effect; reroutes are always enabled.', - deprecated: true, - type: 'boolean', - defaultValue: false, - versionAdded: '1.3.42', - versionModified: '1.13.3' - }, { id: 'Comfy.Graph.LinkMarkers', category: ['LiteGraph', 'Link', 'LinkMarkers'], diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index 3b52522c20..5d9ef0aadb 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -357,78 +357,6 @@ export class GroupNodeConfig { output_is_list: [] }) return def - } else if (node.type === 'Reroute') { - // @ts-expect-error fixme ts strict error - const linksTo = this.linksTo[node.index] - // @ts-expect-error fixme ts strict error - if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) { - // Being used internally - return null - } - - let config = {} - let rerouteType = '*' - if (linksFrom) { - for (const [, , id, slot] of linksFrom['0']) { - const node = this.nodeData.nodes[id] - const input = node.inputs[slot] - if (rerouteType === '*') { - rerouteType = input.type - } - if (input.widget) { - // @ts-expect-error fixme ts strict error - const targetDef = globalDefs[node.type] - const targetWidget = - targetDef.input.required[input.widget.name] ?? - targetDef.input.optional[input.widget.name] - - const widget = [targetWidget[0], config] - const res = mergeIfValid( - // @ts-expect-error invalid slot type - { - widget - }, - targetWidget, - false, - null, - widget - ) - config = res?.customConfig ?? config - } - } - } else if (linksTo) { - const [id, slot] = linksTo['0'] - rerouteType = this.nodeData.nodes[id].outputs[slot].type - } else { - // Reroute used as a pipe - for (const l of this.nodeData.links) { - if (l[2] === node.index) { - rerouteType = l[5] - break - } - } - if (rerouteType === '*') { - // Check for an external link - // @ts-expect-error fixme ts strict error - const t = this.externalFrom[node.index]?.[0] - if (t) { - rerouteType = t - } - } - } - - // @ts-expect-error - config.forceInput = true - return { - input: { - required: { - [rerouteType]: [rerouteType, config] - } - }, - output: [rerouteType], - output_name: [], - output_is_list: [] - } } console.warn( @@ -880,22 +808,13 @@ export class GroupNodeHandler { link = { ...link } const output = this.groupData.newToOldOutputMap[link.origin_slot] let innerNode = this.innerNodes[output.node.index] - let l - while (innerNode?.type === 'Reroute') { - l = innerNode.getInputLink(0) - innerNode = innerNode.getInputNode(0) - } if (!innerNode) { return null } - if (l && GroupNodeHandler.isGroupNode(innerNode)) { - return innerNode.updateLink(l) - } - link.origin_id = innerNode.id - link.origin_slot = l?.origin_slot ?? output.slot + link.origin_slot = output.slot return link } @@ -1352,23 +1271,6 @@ export class GroupNodeHandler { } } continue - } else if (innerNode.type === 'Reroute') { - const rerouteLinks = this.groupData.linksFrom[old.node.index] - if (rerouteLinks) { - for (const [_, , targetNodeId, targetSlot] of rerouteLinks['0']) { - const node = this.innerNodes[targetNodeId] - const input = node.inputs[targetSlot] - if (input.widget) { - const widget = node.widgets?.find( - // @ts-expect-error fixme ts strict error - (w) => w.name === input.widget.name - ) - if (widget) { - widget.value = newValue - } - } - } - } } // @ts-expect-error fixme ts strict error @@ -1411,30 +1313,6 @@ export class GroupNodeHandler { return true } - // @ts-expect-error fixme ts strict error - populateReroute(node, nodeId, map) { - if (node.type !== 'Reroute') return - - const link = this.groupData.linksFrom[nodeId]?.[0]?.[0] - if (!link) return - const [, , targetNodeId, targetNodeSlot] = link - const targetNode = this.groupData.nodeData.nodes[targetNodeId] - const inputs = targetNode.inputs - const targetWidget = inputs?.[targetNodeSlot]?.widget - if (!targetWidget) return - - const offset = inputs.length - (targetNode.widgets_values?.length ?? 0) - const v = targetNode.widgets_values?.[targetNodeSlot - offset] - if (v == null) return - - const widgetName = Object.values(map)[0] - // @ts-expect-error fixme ts strict error - const widget = this.node.widgets.find((w) => w.name === widgetName) - if (widget) { - widget.value = v - } - } - populateWidgets() { if (!this.node.widgets) return @@ -1448,9 +1326,6 @@ export class GroupNodeHandler { const widgets = Object.keys(map) if (!node.widgets_values?.length) { - // special handling for populating values into reroutes - // this allows primitives connect to them to pick up the correct value - this.populateReroute(node, nodeId, map) continue } diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 1d6b083672..6ae99c8bf6 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -10,7 +10,6 @@ import './load3d' import './maskeditor' import './nodeTemplates' import './noteNode' -import './rerouteNode' import './saveImageExtraOutput' import './saveMesh' import './simpleTouchSupport' diff --git a/src/extensions/core/rerouteNode.ts b/src/extensions/core/rerouteNode.ts deleted file mode 100644 index 3bf5bb7321..0000000000 --- a/src/extensions/core/rerouteNode.ts +++ /dev/null @@ -1,319 +0,0 @@ -import type { IContextMenuValue } from '@comfyorg/litegraph' -import { LGraphCanvas, LGraphNode, LiteGraph } from '@comfyorg/litegraph' - -import { app } from '../../scripts/app' -import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs' - -// Node that allows you to redirect connections for cleaner graphs - -app.registerExtension({ - name: 'Comfy.RerouteNode', - registerCustomNodes(app) { - interface RerouteNode extends LGraphNode { - __outputType?: string - } - - class RerouteNode extends LGraphNode { - static category: string | undefined - static defaultVisibility = false - - constructor(title?: string) { - // @ts-expect-error fixme ts strict error - super(title) - if (!this.properties) { - this.properties = {} - } - this.properties.showOutputText = RerouteNode.defaultVisibility - this.properties.horizontal = false - - this.addInput('', '*') - this.addOutput(this.properties.showOutputText ? '*' : '', '*') - - this.onAfterGraphConfigured = function () { - requestAnimationFrame(() => { - // @ts-expect-error fixme ts strict error - this.onConnectionsChange(LiteGraph.INPUT, null, true, null) - }) - } - - this.onConnectionsChange = (type, _index, connected) => { - if (app.configuringGraph) return - - // Prevent multiple connections to different types when we have no input - if (connected && type === LiteGraph.OUTPUT) { - // Ignore wildcard nodes as these will be updated to real types - const types = new Set( - // @ts-expect-error fixme ts strict error - this.outputs[0].links - .map((l) => app.graph.links[l].type) - .filter((t) => t !== '*') - ) - if (types.size > 1) { - const linksToDisconnect = [] - // @ts-expect-error fixme ts strict error - for (let i = 0; i < this.outputs[0].links.length - 1; i++) { - // @ts-expect-error fixme ts strict error - const linkId = this.outputs[0].links[i] - const link = app.graph.links[linkId] - linksToDisconnect.push(link) - } - for (const link of linksToDisconnect) { - const node = app.graph.getNodeById(link.target_id) - // @ts-expect-error fixme ts strict error - node.disconnectInput(link.target_slot) - } - } - } - - // Find root input - let currentNode: LGraphNode | null = this - let updateNodes = [] - let inputType = null - let inputNode = null - while (currentNode) { - updateNodes.unshift(currentNode) - const linkId = currentNode.inputs[0].link - if (linkId !== null) { - const link = app.graph.links[linkId] - if (!link) return - const node = app.graph.getNodeById(link.origin_id) - // @ts-expect-error fixme ts strict error - const type = node.constructor.type - if (type === 'Reroute') { - if (node === this) { - // We've found a circle - currentNode.disconnectInput(link.target_slot) - currentNode = null - } else { - // Move the previous node - currentNode = node - } - } else { - // We've found the end - inputNode = currentNode - // @ts-expect-error fixme ts strict error - inputType = node.outputs[link.origin_slot]?.type ?? null - break - } - } else { - // This path has no input node - currentNode = null - break - } - } - - // Find all outputs - const nodes: LGraphNode[] = [this] - let outputType = null - while (nodes.length) { - // @ts-expect-error fixme ts strict error - currentNode = nodes.pop() - const outputs = - // @ts-expect-error fixme ts strict error - (currentNode.outputs ? currentNode.outputs[0].links : []) || [] - if (outputs.length) { - for (const linkId of outputs) { - const link = app.graph.links[linkId] - - // When disconnecting sometimes the link is still registered - if (!link) continue - - const node = app.graph.getNodeById(link.target_id) - // @ts-expect-error fixme ts strict error - const type = node.constructor.type - - if (type === 'Reroute') { - // Follow reroute nodes - // @ts-expect-error fixme ts strict error - nodes.push(node) - updateNodes.push(node) - } else { - // We've found an output - const nodeOutType = - // @ts-expect-error fixme ts strict error - node.inputs && - // @ts-expect-error fixme ts strict error - node.inputs[link?.target_slot] && - // @ts-expect-error fixme ts strict error - node.inputs[link.target_slot].type - ? // @ts-expect-error fixme ts strict error - node.inputs[link.target_slot].type - : null - if ( - inputType && - // @ts-expect-error fixme ts strict error - !LiteGraph.isValidConnection(inputType, nodeOutType) - ) { - // The output doesnt match our input so disconnect it - // @ts-expect-error fixme ts strict error - node.disconnectInput(link.target_slot) - } else { - outputType = nodeOutType - } - } - } - } else { - // No more outputs for this path - } - } - - const displayType = inputType || outputType || '*' - const color = LGraphCanvas.link_type_colors[displayType] - - let widgetConfig - let widgetType - // Update the types of each node - for (const node of updateNodes) { - // If we dont have an input type we are always wildcard but we'll show the output type - // This lets you change the output link to a different type and all nodes will update - // @ts-expect-error fixme ts strict error - node.outputs[0].type = inputType || '*' - // @ts-expect-error fixme ts strict error - node.__outputType = displayType - // @ts-expect-error fixme ts strict error - node.outputs[0].name = node.properties.showOutputText - ? displayType - : '' - // @ts-expect-error fixme ts strict error - node.setSize(node.computeSize()) - - // @ts-expect-error fixme ts strict error - for (const l of node.outputs[0].links || []) { - const link = app.graph.links[l] - if (link) { - link.color = color - - if (app.configuringGraph) continue - const targetNode = app.graph.getNodeById(link.target_id) - // @ts-expect-error fixme ts strict error - const targetInput = targetNode.inputs?.[link.target_slot] - if (targetInput?.widget) { - const config = getWidgetConfig(targetInput) - if (!widgetConfig) { - widgetConfig = config[1] ?? {} - widgetType = config[0] - } - - const merged = mergeIfValid(targetInput, [ - config[0], - widgetConfig - ]) - if (merged.customConfig) { - widgetConfig = merged.customConfig - } - } - } - } - } - - for (const node of updateNodes) { - if (widgetConfig && outputType) { - // @ts-expect-error fixme ts strict error - node.inputs[0].widget = { name: 'value' } - // @ts-expect-error fixme ts strict error - setWidgetConfig(node.inputs[0], [ - widgetType ?? displayType, - widgetConfig - ]) - } else { - // @ts-expect-error fixme ts strict error - setWidgetConfig(node.inputs[0], null) - } - } - - if (inputNode) { - // @ts-expect-error fixme ts strict error - const link = app.graph.links[inputNode.inputs[0].link] - if (link) { - link.color = color - } - } - } - - this.clone = function () { - const cloned = RerouteNode.prototype.clone.apply(this) - // @ts-expect-error fixme ts strict error - cloned.removeOutput(0) - // @ts-expect-error fixme ts strict error - cloned.addOutput(this.properties.showOutputText ? '*' : '', '*') - // @ts-expect-error fixme ts strict error - cloned.setSize(cloned.computeSize()) - return cloned - } - - // This node is purely frontend and does not impact the resulting prompt so should not be serialized - this.isVirtualNode = true - } - - // @ts-expect-error fixme ts strict error - getExtraMenuOptions(_, options): IContextMenuValue[] { - options.unshift( - { - content: - (this.properties.showOutputText ? 'Hide' : 'Show') + ' Type', - callback: () => { - this.properties.showOutputText = !this.properties.showOutputText - if (this.properties.showOutputText) { - this.outputs[0].name = - this.__outputType || (this.outputs[0].type as string) - } else { - this.outputs[0].name = '' - } - this.setSize(this.computeSize()) - app.graph.setDirtyCanvas(true, true) - } - }, - { - content: - (RerouteNode.defaultVisibility ? 'Hide' : 'Show') + - ' Type By Default', - callback: () => { - RerouteNode.setDefaultTextVisibility( - !RerouteNode.defaultVisibility - ) - } - } - ) - return [] - } - computeSize(): [number, number] { - return [ - this.properties.showOutputText && this.outputs && this.outputs.length - ? Math.max( - 75, - LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + - 40 - ) - : 75, - 26 - ] - } - - // @ts-expect-error fixme ts strict error - static setDefaultTextVisibility(visible) { - RerouteNode.defaultVisibility = visible - if (visible) { - localStorage['Comfy.RerouteNode.DefaultVisibility'] = 'true' - } else { - delete localStorage['Comfy.RerouteNode.DefaultVisibility'] - } - } - } - - // Load default visibility - RerouteNode.setDefaultTextVisibility( - !!localStorage['Comfy.RerouteNode.DefaultVisibility'] - ) - - LiteGraph.registerNodeType( - 'Reroute', - Object.assign(RerouteNode, { - title_mode: LiteGraph.NO_TITLE, - title: 'Reroute', - collapsable: false - }) - ) - - RerouteNode.category = 'utils' - } -}) diff --git a/src/extensions/core/slotDefaults.ts b/src/extensions/core/slotDefaults.ts index eba9d3a519..2f124b1984 100644 --- a/src/extensions/core/slotDefaults.ts +++ b/src/extensions/core/slotDefaults.ts @@ -44,7 +44,7 @@ app.registerExtension({ } if (!(type in this.slot_types_default_out)) { - this.slot_types_default_out[type] = ['Reroute'] + this.slot_types_default_out[type] = [] } if (this.slot_types_default_out[type].includes(nodeId)) continue this.slot_types_default_out[type].push(nodeId) @@ -65,7 +65,7 @@ app.registerExtension({ for (const el of outputs) { const type = el as string if (!(type in this.slot_types_default_in)) { - this.slot_types_default_in[type] = ['Reroute'] // ["Reroute", "Primitive"]; primitive doesn't always work :'() + this.slot_types_default_in[type] = [] } if (this.slot_types_default_in[type].includes(nodeId)) continue diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index 9144f2c51c..c4ff4c7164 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -243,10 +243,6 @@ "name": "Batch count limit", "tooltip": "The maximum number of tasks added to the queue at one button click" }, - "Comfy_RerouteBeta": { - "name": "Opt-in to the reroute beta test", - "tooltip": "No longer has any effect; reroutes are always enabled." - }, "Comfy_Sidebar_Location": { "name": "Sidebar location", "options": { diff --git a/src/locales/fr/settings.json b/src/locales/fr/settings.json index a0e227d0cf..2190019da6 100644 --- a/src/locales/fr/settings.json +++ b/src/locales/fr/settings.json @@ -243,10 +243,6 @@ "name": "Taille de l'historique de la file d'attente", "tooltip": "Le nombre maximum de tâches qui s'affichent dans l'historique de la file d'attente." }, - "Comfy_RerouteBeta": { - "name": "Participer au test bêta de reroute", - "tooltip": "Active les nouveaux reroutes natifs.\n\nLes reroutes peuvent être ajoutés en maintenant alt et en glissant à partir d'une ligne de lien, ou sur le menu de lien.\n\nLa désactivation de cette option est non destructive - les reroutes sont cachés." - }, "Comfy_Sidebar_Location": { "name": "Emplacement de la barre latérale", "options": { diff --git a/src/locales/ja/settings.json b/src/locales/ja/settings.json index 02ea5d6e89..a1963cf1bd 100644 --- a/src/locales/ja/settings.json +++ b/src/locales/ja/settings.json @@ -243,10 +243,6 @@ "name": "キュー履歴サイズ", "tooltip": "キュー履歴に表示されるタスクの最大数。" }, - "Comfy_RerouteBeta": { - "name": "リルートベータテストに参加する", - "tooltip": "新しいネイティブリルートを有効にします。\n\nリルートは、リンクラインからドラッグしながらaltを押すか、リンクメニューで追加できます。\n\nこのオプションを無効にしても破壊的ではなく、リルートは隠されます。" - }, "Comfy_Sidebar_Location": { "name": "サイドバーの位置", "options": { diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index 35b9e88922..8418cfbcc1 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -243,10 +243,6 @@ "name": "실행 큐 기록 갯수", "tooltip": "실행 큐 기록에 표시되는 최대 작업 수입니다." }, - "Comfy_RerouteBeta": { - "name": "경로재설정 베타 테스트 참여", - "tooltip": "새로운 기본 경로재설정을 활성화합니다.\n\n링크 라인에서 Alt를 누른 채 드래그하거나 링크 메뉴에서 경로재설정을 추가할 수 있습니다.\n\n이 옵션을 비활성화해도 경로재설정은 삭제되지 않고 숨겨집니다." - }, "Comfy_Sidebar_Location": { "name": "사이드바 위치", "options": { diff --git a/src/locales/ru/settings.json b/src/locales/ru/settings.json index 33ae725e9b..66d1dff49c 100644 --- a/src/locales/ru/settings.json +++ b/src/locales/ru/settings.json @@ -243,10 +243,6 @@ "name": "Размер истории очереди", "tooltip": "Максимальное количество задач, отображаемых в истории очереди." }, - "Comfy_RerouteBeta": { - "name": "Участвовать в бета-тестировании перенаправления", - "tooltip": "Включает новые нативные перенаправления.\n\nПеренаправления можно добавлять, удерживая alt и перетаскивая от линии ссылки или в меню ссылки.\n\nОтключение этой опции не разрушительно — перенаправления скрыты." - }, "Comfy_Sidebar_Location": { "name": "Расположение боковой панели", "options": { diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index 231955ecdd..24ce3353bb 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -243,10 +243,6 @@ "name": "队列历史大小", "tooltip": "队列历史中显示的最大任务数量。" }, - "Comfy_RerouteBeta": { - "name": "选择加入转接点Beta", - "tooltip": "启用新的转接点。\n\n通过按住alt并划过连线来添加转接点,或在连线中点菜单中添加。\n\n禁用此选项不会造成破坏 - 转接点将被隐藏。" - }, "Comfy_Sidebar_Location": { "name": "侧边栏位置", "options": { diff --git a/src/schemas/comfyWorkflowSchema.ts b/src/schemas/comfyWorkflowSchema.ts index b5ee16cb4c..d20fd74d17 100644 --- a/src/schemas/comfyWorkflowSchema.ts +++ b/src/schemas/comfyWorkflowSchema.ts @@ -278,6 +278,9 @@ export type NodeInput = z.infer export type NodeOutput = z.infer export type ComfyLink = z.infer export type ComfyNode = z.infer +export type Reroute = z.infer +export type WorkflowJSON04 = z.infer +export type WorkflowJSON10 = z.infer export type ComfyWorkflowJSON = z.infer< typeof zComfyWorkflow | typeof zComfyWorkflow1 > diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 90662b7cba..ebea7e8996 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -44,6 +44,7 @@ import { ExtensionManager } from '@/types/extensionTypes' import { ColorAdjustOptions, adjustColor } from '@/utils/colorUtil' import { graphToPrompt } from '@/utils/executionUtil' import { executeWidgetsCallback, isImageNode } from '@/utils/litegraphUtil' +import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute' import { deserialiseAndCreate } from '@/utils/vintageClipboard' import { type ComfyApi, api } from './api' @@ -1061,6 +1062,11 @@ export class ComfyApp { graphData = validatedGraphData ?? graphData } + // Migrate legacy reroute nodes to the new format + if (graphData.version === 0.4) { + graphData = migrateLegacyRerouteNodes(graphData) + } + useWorkflowService().beforeLoadNewGraph() const missingNodeTypes: MissingNodeType[] = [] @@ -1069,7 +1075,6 @@ export class ComfyApp { 'beforeConfigureGraph', graphData, missingNodeTypes - // TODO: missingModels ) const embeddedModels: ModelFile[] = [] @@ -1239,7 +1244,6 @@ export class ComfyApp { useExtensionService().invokeExtensions('loadedGraphNode', node) } - // TODO: Properly handle if both nodes and models are missing (sequential dialogs?) if (missingNodeTypes.length && showMissingNodesDialog) { this.#showMissingNodesError(missingNodeTypes) } diff --git a/src/services/workflowService.ts b/src/services/workflowService.ts index f659478b6f..75812b129d 100644 --- a/src/services/workflowService.ts +++ b/src/services/workflowService.ts @@ -1,9 +1,12 @@ -import { LGraphCanvas } from '@comfyorg/litegraph' -import type { Vector2 } from '@comfyorg/litegraph' +import { LGraph, LGraphCanvas } from '@comfyorg/litegraph' +import type { SerialisableGraph, Vector2 } from '@comfyorg/litegraph' import { toRaw } from 'vue' import { t } from '@/i18n' -import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema' +import { + ComfyWorkflowJSON, + WorkflowJSON04 +} from '@/schemas/comfyWorkflowSchema' import { app } from '@/scripts/app' import { blankGraph, defaultGraph } from '@/scripts/defaultGraph' import { downloadBlob } from '@/scripts/utils' @@ -12,6 +15,7 @@ import { useToastStore } from '@/stores/toastStore' import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore' import { useWorkspaceStore } from '@/stores/workspaceStore' import { appendJsonExt } from '@/utils/formatUtil' +import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute' import { useDialogService } from './dialogService' @@ -324,17 +328,19 @@ export const useWorkflowService = () => { ) => { const loadedWorkflow = await workflow.load() const data = loadedWorkflow.initialState + const workflowJSON = + data.version === 0.4 + ? migrateLegacyRerouteNodes(data as WorkflowJSON04) + : data const old = localStorage.getItem('litegrapheditor_clipboard') - // @ts-expect-error: zod issue. Should be fixed after enable ts-strict globally - const graph = new LGraph(data) + // unknown conversion: ComfyWorkflowJSON is stricter than LiteGraph's + // serialisation schema. + const graph = new LGraph(workflowJSON as unknown as SerialisableGraph) const canvasElement = document.createElement('canvas') const canvas = new LGraphCanvas(canvasElement, graph, { skip_events: true, skip_render: true }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Temporary fix for Litegraph issue. - canvas.reroutesEnabled = true canvas.selectItems() canvas.copyToClipboard() app.canvas.pasteFromClipboard(options) diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts index fc67ebb52a..4db86b5763 100644 --- a/src/stores/nodeDefStore.ts +++ b/src/stores/nodeDefStore.ts @@ -140,18 +140,6 @@ export const SYSTEM_NODE_DEFS: Record = { python_module: 'nodes', description: 'Primitive values like numbers, strings, and booleans.' }, - Reroute: { - name: 'Reroute', - display_name: 'Reroute', - category: 'utils', - input: { required: { '': ['*', {}] }, optional: {} }, - output: ['*'], - output_name: [''], - output_is_list: [false], - output_node: false, - python_module: 'nodes', - description: 'Reroute the connection to another node.' - }, Note: { name: 'Note', display_name: 'Note', diff --git a/src/utils/migration/migrateReroute.ts b/src/utils/migration/migrateReroute.ts new file mode 100644 index 0000000000..250b9eb00f --- /dev/null +++ b/src/utils/migration/migrateReroute.ts @@ -0,0 +1,193 @@ +import type { + ComfyLink, + ComfyNode, + NodeId, + Reroute, + WorkflowJSON04 +} from '@/schemas/comfyWorkflowSchema' + +type RerouteNode = ComfyNode & { + type: 'Reroute' +} + +type LinkExtension = { + id: number + parentId: number +} + +type RerouteEntry = { + reroute: Reroute + rerouteNode: RerouteNode +} + +/** + * Identifies all legacy Reroute nodes in a workflow + */ +function findLegacyRerouteNodes(workflow: WorkflowJSON04): RerouteNode[] { + return workflow.nodes.filter( + (node) => node.type === 'Reroute' + ) as RerouteNode[] +} + +/** + * Gets the center position of a node + */ +function getNodeCenter(node: ComfyNode): [number, number] { + return [node.pos[0] + node.size[0] / 2, node.pos[1] + node.size[1] / 2] +} + +/** + * Creates native reroute points from legacy Reroute nodes + */ +export function createReroutePoints( + rerouteNodes: RerouteNode[] +): Map { + const rerouteMap = new Map() + + let rerouteIdCounter = 1 + rerouteNodes.forEach((node) => { + const rerouteId = rerouteIdCounter++ + rerouteMap.set(node.id, { + reroute: { + id: rerouteId, + pos: getNodeCenter(node), + linkIds: [] + }, + rerouteNode: node + }) + }) + + return rerouteMap +} + +/** + * Creates new links and link extensions for the migrated workflow + */ +export function createNewLinks( + workflow: WorkflowJSON04, + rerouteMap: Map +): { + links: ComfyLink[] + linkExtensions: LinkExtension[] +} { + const links: ComfyLink[] = [] + const linkExtensions: LinkExtension[] = [] + + const rerouteMapByRerouteId = new Map( + Array.from(rerouteMap.values()).map((entry) => [entry.reroute.id, entry]) + ) + const linksMap = new Map( + Array.from(workflow.links).map((link) => [link[0], link]) + ) + + // Process each link in the workflow + for (const link of workflow.links) { + const [ + linkId, + sourceNodeId, + _sourceSlot, + targetNodeId, + _targetSlot, + _dataType + ] = link + + // Check if this link connects to or from a reroute node + const sourceEntry = rerouteMap.get(sourceNodeId) + const targetEntry = rerouteMap.get(targetNodeId) + const sourceIsReroute = !!sourceEntry + const targetIsReroute = !!targetEntry + + if (!sourceIsReroute && !targetIsReroute) { + // If neither end is a reroute, keep the link as is + links.push(link) + } else if (sourceIsReroute && !targetIsReroute) { + // This is a link from a reroute node to a regular node + linkExtensions.push({ + id: linkId, + parentId: sourceEntry.reroute.id + }) + } else if (sourceIsReroute && targetIsReroute) { + targetEntry.reroute.parentId = sourceEntry.reroute.id + } + } + + // Populate linkIds on reroute nodes + for (const linkExtension of linkExtensions) { + let entry = rerouteMapByRerouteId.get(linkExtension.parentId) + + while (entry) { + const reroute = entry.reroute + reroute.linkIds ??= [] + reroute.linkIds.push(linkExtension.id) + + if (reroute.parentId) { + entry = rerouteMapByRerouteId.get(reroute.parentId) + } else { + const rerouteNode = entry.rerouteNode + const rerouteInputLink = linksMap.get( + rerouteNode?.inputs?.[0]?.link ?? -1 + ) + const rerouteOutputLink = linksMap.get(linkExtension.id) + + if (rerouteInputLink && rerouteOutputLink) { + const [_, sourceNodeId, sourceSlot] = rerouteInputLink + const [linkId, __, ___, targetNodeId, targetSlot, dataType] = + rerouteOutputLink + + links.push([ + linkId, + sourceNodeId, + sourceSlot, + targetNodeId, + targetSlot, + dataType + ]) + } + entry = undefined + } + } + } + + return { links, linkExtensions } +} + +/** + * Main function to migrate legacy reroute nodes to native reroute points + */ +export const migrateLegacyRerouteNodes = ( + workflow: WorkflowJSON04 +): WorkflowJSON04 => { + // Find all legacy Reroute nodes + const legacyRerouteNodes = findLegacyRerouteNodes(workflow) + + // If no reroute nodes, return the workflow unchanged + if (legacyRerouteNodes.length === 0) { + return workflow + } + + // Create a deep copy of the workflow to avoid mutating the original + const newWorkflow = JSON.parse(JSON.stringify(workflow)) as WorkflowJSON04 + + // Initialize extra structure if needed + if (!newWorkflow.extra) { + newWorkflow.extra = {} + } + + // Create native reroute points + const rerouteMap = createReroutePoints(legacyRerouteNodes) + + // Create new links and link extensions + const { links, linkExtensions } = createNewLinks(workflow, rerouteMap) + + // Update the workflow + newWorkflow.links = links + newWorkflow.nodes = newWorkflow.nodes.filter( + (node) => node.type !== 'Reroute' + ) + newWorkflow.extra.reroutes = Array.from(rerouteMap.values()).map( + (entry) => entry.reroute + ) + newWorkflow.extra.linkExtensions = linkExtensions + + return newWorkflow +} diff --git a/tests-ui/tests/utils/migration/migrateReroute.test.ts b/tests-ui/tests/utils/migration/migrateReroute.test.ts new file mode 100644 index 0000000000..6facdbab9b --- /dev/null +++ b/tests-ui/tests/utils/migration/migrateReroute.test.ts @@ -0,0 +1,35 @@ +import fs from 'node:fs' +import path from 'node:path' +import { describe, expect, it } from 'vitest' + +import type { WorkflowJSON04 } from '@/schemas/comfyWorkflowSchema' +import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute' + +describe('migrateReroute', () => { + describe('migrateReroute snapshots', () => { + // Helper function to load workflow JSON files + const loadWorkflow = (filePath: string): WorkflowJSON04 => { + const fullPath = path.resolve(__dirname, filePath) + const fileContent = fs.readFileSync(fullPath, 'utf-8') + return JSON.parse(fileContent) as WorkflowJSON04 + } + + it.each(['branching.json', 'single_connected.json'])( + 'should correctly migrate %s', + (fileName) => { + // Load the legacy workflow + const legacyWorkflow = loadWorkflow( + `workflows/reroute/legacy/${fileName}` + ) + + // Migrate the workflow + const migratedWorkflow = migrateLegacyRerouteNodes(legacyWorkflow) + + // Compare with snapshot + expect(JSON.stringify(migratedWorkflow, null, 2)).toMatchFileSnapshot( + `workflows/reroute/native/${fileName}` + ) + } + ) + }) +}) diff --git a/tests-ui/tests/utils/migration/workflows/reroute/legacy/branching.json b/tests-ui/tests/utils/migration/workflows/reroute/legacy/branching.json new file mode 100644 index 0000000000..7fb12d62fa --- /dev/null +++ b/tests-ui/tests/utils/migration/workflows/reroute/legacy/branching.json @@ -0,0 +1,288 @@ +{ + "last_node_id": 27, + "last_link_id": 34, + "nodes": [ + { + "id": 12, + "type": "VAEDecode", + "pos": [ + 620, + 260 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": null + }, + { + "name": "vae", + "type": "VAE", + "link": 21 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 47.948699951171875, + 239.2628173828125 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [] + }, + { + "name": "CLIP", + "type": "CLIP", + "slot_index": 1, + "links": [] + }, + { + "name": "VAE", + "type": "VAE", + "slot_index": 2, + "links": [ + 13, + 31 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "v1-5-pruned-emaonly.safetensors" + ] + }, + { + "id": 13, + "type": "Reroute", + "pos": [ + 510, + 280 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 32 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "slot_index": 0, + "links": [ + 21 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 25, + "type": "Reroute", + "pos": [ + 404.7915344238281, + 280.9454650878906 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 31 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 32, + 33 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 27, + "type": "Reroute", + "pos": [ + 514, + 386 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 33 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 34 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 26, + "type": "VAEDecode", + "pos": [ + 625, + 373 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": null + }, + { + "name": "vae", + "type": "VAE", + "link": 34 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + } + ], + "links": [ + [ + 21, + 13, + 0, + 12, + 1, + "VAE" + ], + [ + 31, + 4, + 2, + 25, + 0, + "*" + ], + [ + 32, + 25, + 0, + 13, + 0, + "*" + ], + [ + 33, + 25, + 0, + 27, + 0, + "*" + ], + [ + 34, + 27, + 0, + 26, + 1, + "VAE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 2.3195551508147507, + "offset": [ + 96.55985005696607, + -41.449812921703376 + ] + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/tests-ui/tests/utils/migration/workflows/reroute/legacy/single_connected.json b/tests-ui/tests/utils/migration/workflows/reroute/legacy/single_connected.json new file mode 100644 index 0000000000..5559743aca --- /dev/null +++ b/tests-ui/tests/utils/migration/workflows/reroute/legacy/single_connected.json @@ -0,0 +1,154 @@ +{ + "last_node_id": 24, + "last_link_id": 30, + "nodes": [ + { + "id": 12, + "type": "VAEDecode", + "pos": [ + 620, + 260 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": null + }, + { + "name": "vae", + "type": "VAE", + "link": 21 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 13, + "type": "Reroute", + "pos": [ + 510, + 280 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 13 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "slot_index": 0, + "links": [ + 21 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 160, + 240 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [] + }, + { + "name": "CLIP", + "type": "CLIP", + "slot_index": 1, + "links": [] + }, + { + "name": "VAE", + "type": "VAE", + "slot_index": 2, + "links": [ + 13 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "v1-5-pruned-emaonly.safetensors" + ] + } + ], + "links": [ + [ + 13, + 4, + 2, + 13, + 0, + "*" + ], + [ + 21, + 13, + 0, + 12, + 1, + "VAE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 2.7130434782608694, + "offset": [ + -35, + -40.86698717948718 + ] + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/tests-ui/tests/utils/migration/workflows/reroute/native/branching.json b/tests-ui/tests/utils/migration/workflows/reroute/native/branching.json new file mode 100644 index 0000000000..70784b5760 --- /dev/null +++ b/tests-ui/tests/utils/migration/workflows/reroute/native/branching.json @@ -0,0 +1,202 @@ +{ + "last_node_id": 27, + "last_link_id": 34, + "nodes": [ + { + "id": 12, + "type": "VAEDecode", + "pos": [ + 620, + 260 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": null + }, + { + "name": "vae", + "type": "VAE", + "link": 21 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 47.948699951171875, + 239.2628173828125 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [] + }, + { + "name": "CLIP", + "type": "CLIP", + "slot_index": 1, + "links": [] + }, + { + "name": "VAE", + "type": "VAE", + "slot_index": 2, + "links": [ + 13, + 31 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "v1-5-pruned-emaonly.safetensors" + ] + }, + { + "id": 26, + "type": "VAEDecode", + "pos": [ + 625, + 373 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": null + }, + { + "name": "vae", + "type": "VAE", + "link": 34 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + } + ], + "links": [ + [ + 21, + 4, + 2, + 12, + 1, + "VAE" + ], + [ + 34, + 4, + 2, + 26, + 1, + "VAE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 2.3195551508147507, + "offset": [ + 96.55985005696607, + -41.449812921703376 + ] + }, + "reroutes": [ + { + "id": 1, + "pos": [ + 547.5, + 293 + ], + "linkIds": [ + 21 + ], + "parentId": 2 + }, + { + "id": 2, + "pos": [ + 442.2915344238281, + 293.9454650878906 + ], + "linkIds": [ + 21, + 34 + ] + }, + { + "id": 3, + "pos": [ + 551.5, + 399 + ], + "linkIds": [ + 34 + ], + "parentId": 2 + } + ], + "linkExtensions": [ + { + "id": 21, + "parentId": 1 + }, + { + "id": 34, + "parentId": 3 + } + ] + }, + "version": 0.4 +} \ No newline at end of file diff --git a/tests-ui/tests/utils/migration/workflows/reroute/native/single_connected.json b/tests-ui/tests/utils/migration/workflows/reroute/native/single_connected.json new file mode 100644 index 0000000000..95cb451f2b --- /dev/null +++ b/tests-ui/tests/utils/migration/workflows/reroute/native/single_connected.json @@ -0,0 +1,128 @@ +{ + "last_node_id": 24, + "last_link_id": 30, + "nodes": [ + { + "id": 12, + "type": "VAEDecode", + "pos": [ + 620, + 260 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": null + }, + { + "name": "vae", + "type": "VAE", + "link": 21 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 160, + 240 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [] + }, + { + "name": "CLIP", + "type": "CLIP", + "slot_index": 1, + "links": [] + }, + { + "name": "VAE", + "type": "VAE", + "slot_index": 2, + "links": [ + 13 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "v1-5-pruned-emaonly.safetensors" + ] + } + ], + "links": [ + [ + 21, + 4, + 2, + 12, + 1, + "VAE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 2.7130434782608694, + "offset": [ + -35, + -40.86698717948718 + ] + }, + "reroutes": [ + { + "id": 1, + "pos": [ + 547.5, + 293 + ], + "linkIds": [ + 21 + ] + } + ], + "linkExtensions": [ + { + "id": 21, + "parentId": 1 + } + ] + }, + "version": 0.4 +} \ No newline at end of file