From a041cc8e0e7475033831dd41e6ea9ac8072711f3 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 5 Aug 2025 06:33:02 -0400 Subject: [PATCH] Literally put subgraph into LGraph --- src/lib/litegraph/src/LGraph.ts | 247 +++++++++++++++++++- src/lib/litegraph/src/subgraph/Subgraph.ts | 249 +-------------------- 2 files changed, 249 insertions(+), 247 deletions(-) diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 0ccf3421c4..206aa8cff1 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -1,5 +1,6 @@ import type { DragAndScaleState } from "./DragAndScale" import type { LGraphEventMap } from "./infrastructure/LGraphEventMap" +import type { SubgraphEventMap } from "./infrastructure/SubgraphEventMap" import type { Dictionary, IContextMenuValue, @@ -9,6 +10,9 @@ import type { OptionalProps, Point, Positionable, + DefaultConnectionColors, + INodeInputSlot, + INodeOutputSlot, } from "./interfaces" import type { ExportedSubgraph, @@ -17,6 +21,7 @@ import type { Serialisable, SerialisableGraph, SerialisableReroute, + ExposedWidget, } from "./types/serialisation" import type { UUID } from "@/lib/litegraph/src/utils/uuid" @@ -33,9 +38,10 @@ import { MapProxyHandler } from "./MapProxyHandler" import { alignOutsideContainer, alignToContainer, createBounds } from "./measure" import { Reroute, type RerouteId } from "./Reroute" import { stringOrEmpty } from "./strings" -import { type GraphOrSubgraph, Subgraph } from "./subgraph/Subgraph" import { SubgraphInput } from "./subgraph/SubgraphInput" import { SubgraphOutput } from "./subgraph/SubgraphOutput" +import { SubgraphInputNode } from "./subgraph/SubgraphInputNode" +import { SubgraphOutputNode } from "./subgraph/SubgraphOutputNode" import { findUsedSubgraphIds, getBoundaryLinks, groupResolvedByOutput, mapSubgraphInputsAndLinks, mapSubgraphOutputsAndLinks, multiClone, splitPositionables } from "./subgraph/subgraphUtils" import { Alignment, LGraphEventMode } from "./types/globalEnums" import { getAllNestedItems } from "./utils/collections" @@ -1950,3 +1956,242 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable { + override readonly events = new CustomEventTarget() + + /** Limits the number of levels / depth that subgraphs may be nested. Prevents uncontrolled programmatic nesting. */ + static MAX_NESTED_SUBGRAPHS = 1000 + + /** The display name of the subgraph. */ + name: string = "Unnamed Subgraph" + + readonly inputNode = new SubgraphInputNode(this) + readonly outputNode = new SubgraphOutputNode(this) + + /** Ordered list of inputs to the subgraph itself. Similar to a reroute, with the input side in the graph, and the output side in the subgraph. */ + readonly inputs: SubgraphInput[] = [] + /** Ordered list of outputs from the subgraph itself. Similar to a reroute, with the input side in the subgraph, and the output side in the graph. */ + readonly outputs: SubgraphOutput[] = [] + /** A list of node widgets displayed in the parent graph, on the subgraph object. */ + readonly widgets: ExposedWidget[] = [] + + #rootGraph: LGraph + override get rootGraph(): LGraph { + return this.#rootGraph + } + + constructor( + rootGraph: LGraph, + data: ExportedSubgraph, + ) { + if (!rootGraph) throw new Error("Root graph is required") + + super() + + this.#rootGraph = rootGraph + + const cloned = structuredClone(data) + this._configureBase(cloned) + this.#configureSubgraph(cloned) + } + + getIoNodeOnPos(x: number, y: number): SubgraphInputNode | SubgraphOutputNode | undefined { + const { inputNode, outputNode } = this + if (inputNode.containsPoint([x, y])) return inputNode + if (outputNode.containsPoint([x, y])) return outputNode + } + + #configureSubgraph(data: ISerialisedGraph & ExportedSubgraph | SerialisableGraph & ExportedSubgraph): void { + const { name, inputs, outputs, widgets } = data + + this.name = name + if (inputs) { + this.inputs.length = 0 + for (const input of inputs) { + const subgraphInput = new SubgraphInput(input, this.inputNode) + this.inputs.push(subgraphInput) + this.events.dispatch("input-added", { input: subgraphInput }) + } + } + + if (outputs) { + this.outputs.length = 0 + for (const output of outputs) { + this.outputs.push(new SubgraphOutput(output, this.outputNode)) + } + } + + if (widgets) { + this.widgets.length = 0 + for (const widget of widgets) { + this.widgets.push(widget) + } + } + + this.inputNode.configure(data.inputNode) + this.outputNode.configure(data.outputNode) + } + + override configure(data: ISerialisedGraph & ExportedSubgraph | SerialisableGraph & ExportedSubgraph, keep_old?: boolean): boolean | undefined { + const r = super.configure(data, keep_old) + + this.#configureSubgraph(data) + return r + } + + override attachCanvas(canvas: LGraphCanvas): void { + super.attachCanvas(canvas) + canvas.subgraph = this + } + + addInput(name: string, type: string): SubgraphInput { + this.events.dispatch("adding-input", { name, type }) + + const input = new SubgraphInput({ + id: createUuidv4(), + name, + type, + }, this.inputNode) + + this.inputs.push(input) + this.events.dispatch("input-added", { input }) + + return input + } + + addOutput(name: string, type: string): SubgraphOutput { + this.events.dispatch("adding-output", { name, type }) + + const output = new SubgraphOutput({ + id: createUuidv4(), + name, + type, + }, this.outputNode) + + this.outputs.push(output) + this.events.dispatch("output-added", { output }) + + return output + } + + /** + * Renames an input slot in the subgraph. + * @param input The input slot to rename. + * @param name The new name for the input slot. + */ + renameInput(input: SubgraphInput, name: string): void { + const index = this.inputs.indexOf(input) + if (index === -1) throw new Error("Input not found") + + const oldName = input.displayName + this.events.dispatch("renaming-input", { input, index, oldName, newName: name }) + + input.label = name + } + + /** + * Renames an output slot in the subgraph. + * @param output The output slot to rename. + * @param name The new name for the output slot. + */ + renameOutput(output: SubgraphOutput, name: string): void { + const index = this.outputs.indexOf(output) + if (index === -1) throw new Error("Output not found") + + const oldName = output.displayName + this.events.dispatch("renaming-output", { output, index, oldName, newName: name }) + + output.label = name + } + + /** + * Removes an input slot from the subgraph. + * @param input The input slot to remove. + */ + removeInput(input: SubgraphInput): void { + input.disconnect() + + const index = this.inputs.indexOf(input) + if (index === -1) throw new Error("Input not found") + + const mayContinue = this.events.dispatch("removing-input", { input, index }) + if (!mayContinue) return + + this.inputs.splice(index, 1) + + const { length } = this.inputs + for (let i = index; i < length; i++) { + this.inputs[i].decrementSlots("inputs") + } + } + + /** + * Removes an output slot from the subgraph. + * @param output The output slot to remove. + */ + removeOutput(output: SubgraphOutput): void { + output.disconnect() + + const index = this.outputs.indexOf(output) + if (index === -1) throw new Error("Output not found") + + const mayContinue = this.events.dispatch("removing-output", { output, index }) + if (!mayContinue) return + + this.outputs.splice(index, 1) + + const { length } = this.outputs + for (let i = index; i < length; i++) { + this.outputs[i].decrementSlots("outputs") + } + } + + draw(ctx: CanvasRenderingContext2D, colorContext: DefaultConnectionColors, fromSlot?: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput, editorAlpha?: number): void { + this.inputNode.draw(ctx, colorContext, fromSlot, editorAlpha) + this.outputNode.draw(ctx, colorContext, fromSlot, editorAlpha) + } + + /** + * Clones the subgraph, creating an identical copy with a new ID. + * @returns A new subgraph with the same configuration, but a new ID. + */ + clone(keepId: boolean = false): Subgraph { + const exported = this.asSerialisable() + if (!keepId) exported.id = createUuidv4() + + const subgraph = new Subgraph(this.rootGraph, exported) + subgraph.configure(exported) + return subgraph + } + + override asSerialisable(): ExportedSubgraph & Required> { + return { + id: this.id, + version: LGraph.serialisedSchemaVersion, + state: this.state, + revision: this.revision, + config: this.config, + name: this.name, + inputNode: this.inputNode.asSerialisable(), + outputNode: this.outputNode.asSerialisable(), + inputs: this.inputs.map(x => x.asSerialisable()), + outputs: this.outputs.map(x => x.asSerialisable()), + widgets: [...this.widgets], + nodes: this.nodes.map(node => node.serialize()), + groups: this.groups.map(group => group.serialize()), + links: [...this.links.values()].map(x => x.asSerialisable()), + extra: this.extra, + } + } +} diff --git a/src/lib/litegraph/src/subgraph/Subgraph.ts b/src/lib/litegraph/src/subgraph/Subgraph.ts index 1ea8419a58..ba57aea6b4 100644 --- a/src/lib/litegraph/src/subgraph/Subgraph.ts +++ b/src/lib/litegraph/src/subgraph/Subgraph.ts @@ -1,246 +1,3 @@ -import type { SubgraphEventMap } from "@/lib/litegraph/src/infrastructure/SubgraphEventMap" -import type { DefaultConnectionColors, INodeInputSlot, INodeOutputSlot } from "@/lib/litegraph/src/interfaces" -import type { LGraphCanvas } from "@/lib/litegraph/src/LGraphCanvas" -import type { ExportedSubgraph, ExposedWidget, ISerialisedGraph, Serialisable, SerialisableGraph } from "@/lib/litegraph/src/types/serialisation" - -import { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import { type BaseLGraph, LGraph } from "@/lib/litegraph/src/LGraph" -import { createUuidv4 } from "@/lib/litegraph/src/utils/uuid" - -import { SubgraphInput } from "./SubgraphInput" -import { SubgraphInputNode } from "./SubgraphInputNode" -import { SubgraphOutput } from "./SubgraphOutput" -import { SubgraphOutputNode } from "./SubgraphOutputNode" - -/** Internal; simplifies type definitions. */ -export type GraphOrSubgraph = LGraph | Subgraph - -/** A subgraph definition. */ -export class Subgraph extends LGraph implements BaseLGraph, Serialisable { - declare readonly events: CustomEventTarget - - /** Limits the number of levels / depth that subgraphs may be nested. Prevents uncontrolled programmatic nesting. */ - static MAX_NESTED_SUBGRAPHS = 1000 - - /** The display name of the subgraph. */ - name: string = "Unnamed Subgraph" - - readonly inputNode = new SubgraphInputNode(this) - readonly outputNode = new SubgraphOutputNode(this) - - /** Ordered list of inputs to the subgraph itself. Similar to a reroute, with the input side in the graph, and the output side in the subgraph. */ - readonly inputs: SubgraphInput[] = [] - /** Ordered list of outputs from the subgraph itself. Similar to a reroute, with the input side in the subgraph, and the output side in the graph. */ - readonly outputs: SubgraphOutput[] = [] - /** A list of node widgets displayed in the parent graph, on the subgraph object. */ - readonly widgets: ExposedWidget[] = [] - - #rootGraph: LGraph - override get rootGraph(): LGraph { - return this.#rootGraph - } - - constructor( - rootGraph: LGraph, - data: ExportedSubgraph, - ) { - if (!rootGraph) throw new Error("Root graph is required") - - super() - - this.#rootGraph = rootGraph - - const cloned = structuredClone(data) - this._configureBase(cloned) - this.#configureSubgraph(cloned) - } - - getIoNodeOnPos(x: number, y: number): SubgraphInputNode | SubgraphOutputNode | undefined { - const { inputNode, outputNode } = this - if (inputNode.containsPoint([x, y])) return inputNode - if (outputNode.containsPoint([x, y])) return outputNode - } - - #configureSubgraph(data: ISerialisedGraph & ExportedSubgraph | SerialisableGraph & ExportedSubgraph): void { - const { name, inputs, outputs, widgets } = data - - this.name = name - if (inputs) { - this.inputs.length = 0 - for (const input of inputs) { - const subgraphInput = new SubgraphInput(input, this.inputNode) - this.inputs.push(subgraphInput) - this.events.dispatch("input-added", { input: subgraphInput }) - } - } - - if (outputs) { - this.outputs.length = 0 - for (const output of outputs) { - this.outputs.push(new SubgraphOutput(output, this.outputNode)) - } - } - - if (widgets) { - this.widgets.length = 0 - for (const widget of widgets) { - this.widgets.push(widget) - } - } - - this.inputNode.configure(data.inputNode) - this.outputNode.configure(data.outputNode) - } - - override configure(data: ISerialisedGraph & ExportedSubgraph | SerialisableGraph & ExportedSubgraph, keep_old?: boolean): boolean | undefined { - const r = super.configure(data, keep_old) - - this.#configureSubgraph(data) - return r - } - - override attachCanvas(canvas: LGraphCanvas): void { - super.attachCanvas(canvas) - canvas.subgraph = this - } - - addInput(name: string, type: string): SubgraphInput { - this.events.dispatch("adding-input", { name, type }) - - const input = new SubgraphInput({ - id: createUuidv4(), - name, - type, - }, this.inputNode) - - this.inputs.push(input) - this.events.dispatch("input-added", { input }) - - return input - } - - addOutput(name: string, type: string): SubgraphOutput { - this.events.dispatch("adding-output", { name, type }) - - const output = new SubgraphOutput({ - id: createUuidv4(), - name, - type, - }, this.outputNode) - - this.outputs.push(output) - this.events.dispatch("output-added", { output }) - - return output - } - - /** - * Renames an input slot in the subgraph. - * @param input The input slot to rename. - * @param name The new name for the input slot. - */ - renameInput(input: SubgraphInput, name: string): void { - const index = this.inputs.indexOf(input) - if (index === -1) throw new Error("Input not found") - - const oldName = input.displayName - this.events.dispatch("renaming-input", { input, index, oldName, newName: name }) - - input.label = name - } - - /** - * Renames an output slot in the subgraph. - * @param output The output slot to rename. - * @param name The new name for the output slot. - */ - renameOutput(output: SubgraphOutput, name: string): void { - const index = this.outputs.indexOf(output) - if (index === -1) throw new Error("Output not found") - - const oldName = output.displayName - this.events.dispatch("renaming-output", { output, index, oldName, newName: name }) - - output.label = name - } - - /** - * Removes an input slot from the subgraph. - * @param input The input slot to remove. - */ - removeInput(input: SubgraphInput): void { - input.disconnect() - - const index = this.inputs.indexOf(input) - if (index === -1) throw new Error("Input not found") - - const mayContinue = this.events.dispatch("removing-input", { input, index }) - if (!mayContinue) return - - this.inputs.splice(index, 1) - - const { length } = this.inputs - for (let i = index; i < length; i++) { - this.inputs[i].decrementSlots("inputs") - } - } - - /** - * Removes an output slot from the subgraph. - * @param output The output slot to remove. - */ - removeOutput(output: SubgraphOutput): void { - output.disconnect() - - const index = this.outputs.indexOf(output) - if (index === -1) throw new Error("Output not found") - - const mayContinue = this.events.dispatch("removing-output", { output, index }) - if (!mayContinue) return - - this.outputs.splice(index, 1) - - const { length } = this.outputs - for (let i = index; i < length; i++) { - this.outputs[i].decrementSlots("outputs") - } - } - - draw(ctx: CanvasRenderingContext2D, colorContext: DefaultConnectionColors, fromSlot?: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput, editorAlpha?: number): void { - this.inputNode.draw(ctx, colorContext, fromSlot, editorAlpha) - this.outputNode.draw(ctx, colorContext, fromSlot, editorAlpha) - } - - /** - * Clones the subgraph, creating an identical copy with a new ID. - * @returns A new subgraph with the same configuration, but a new ID. - */ - clone(keepId: boolean = false): Subgraph { - const exported = this.asSerialisable() - if (!keepId) exported.id = createUuidv4() - - const subgraph = new Subgraph(this.rootGraph, exported) - subgraph.configure(exported) - return subgraph - } - - override asSerialisable(): ExportedSubgraph & Required> { - return { - id: this.id, - version: LGraph.serialisedSchemaVersion, - state: this.state, - revision: this.revision, - config: this.config, - name: this.name, - inputNode: this.inputNode.asSerialisable(), - outputNode: this.outputNode.asSerialisable(), - inputs: this.inputs.map(x => x.asSerialisable()), - outputs: this.outputs.map(x => x.asSerialisable()), - widgets: [...this.widgets], - nodes: this.nodes.map(node => node.serialize()), - groups: this.groups.map(group => group.serialize()), - links: [...this.links.values()].map(x => x.asSerialisable()), - extra: this.extra, - } - } -} +// Re-export Subgraph and GraphOrSubgraph from LGraph.ts to maintain compatibility +// This is a temporary fix to resolve circular dependency issues +export { Subgraph, type GraphOrSubgraph } from "@/lib/litegraph/src/LGraph" \ No newline at end of file