mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-07 06:00:03 +00:00
Literally put subgraph into LGraph
This commit is contained in:
@@ -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<Serialisabl
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal; simplifies type definitions. */
|
||||
export type GraphOrSubgraph = LGraph | Subgraph
|
||||
|
||||
// ============================================================================
|
||||
// TEMPORARY: Subgraph class moved here to resolve circular dependency
|
||||
// This is a temporary solution until the architecture can be refactored
|
||||
// TODO: Move back to separate file once circular dependencies are resolved
|
||||
// ============================================================================
|
||||
|
||||
/** A subgraph definition. */
|
||||
export class Subgraph extends LGraph implements BaseLGraph, Serialisable<ExportedSubgraph> {
|
||||
override readonly events = new CustomEventTarget<SubgraphEventMap>()
|
||||
|
||||
/** 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<Pick<SerialisableGraph, "nodes" | "groups" | "extra">> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ExportedSubgraph> {
|
||||
declare readonly events: CustomEventTarget<SubgraphEventMap>
|
||||
|
||||
/** 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<Pick<SerialisableGraph, "nodes" | "groups" | "extra">> {
|
||||
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"
|
||||
Reference in New Issue
Block a user