From 199eeae269b7dfe795a30fd22c146ae3e64c83f2 Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Fri, 2 May 2025 09:16:19 +1000 Subject: [PATCH] Add subgraph skeleton classes (#997) Allows downstream consumers to use subgraph types ahead of impl. --- src/LGraph.ts | 14 ++++- src/LGraphCanvas.ts | 3 ++ src/litegraph.ts | 7 +++ src/subgraph/Subgraph.ts | 74 +++++++++++++++++++++++++++ src/subgraph/SubgraphIONodeBase.ts | 82 ++++++++++++++++++++++++++++++ src/subgraph/SubgraphInput.ts | 24 +++++++++ src/subgraph/SubgraphInputNode.ts | 12 +++++ src/subgraph/SubgraphOutput.ts | 23 +++++++++ src/subgraph/SubgraphOutputNode.ts | 12 +++++ src/subgraph/SubgraphSlotBase.ts | 64 +++++++++++++++++++++++ src/types/serialisation.ts | 22 +++++--- 11 files changed, 330 insertions(+), 7 deletions(-) create mode 100644 src/subgraph/Subgraph.ts create mode 100644 src/subgraph/SubgraphIONodeBase.ts create mode 100644 src/subgraph/SubgraphInput.ts create mode 100644 src/subgraph/SubgraphInputNode.ts create mode 100644 src/subgraph/SubgraphOutput.ts create mode 100644 src/subgraph/SubgraphOutputNode.ts create mode 100644 src/subgraph/SubgraphSlotBase.ts diff --git a/src/LGraph.ts b/src/LGraph.ts index 78b59aac6..06cfe6b50 100644 --- a/src/LGraph.ts +++ b/src/LGraph.ts @@ -48,13 +48,17 @@ export interface LGraphConfig { links_ontop?: any } +export interface BaseLGraph { + readonly rootGraph: LGraph +} + /** * LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop. * supported callbacks: * + onNodeAdded: when a new node is added to the graph * + onNodeRemoved: when a node inside this graph is removed */ -export class LGraph implements LinkNetwork, Serialisable { +export class LGraph implements LinkNetwork, BaseLGraph, Serialisable { static serialisedSchemaVersion = 1 as const static STATUS_STOPPED = 1 @@ -147,6 +151,14 @@ export class LGraph implements LinkNetwork, Serialisable { return this.#reroutes } + get rootGraph(): LGraph { + return this + } + + get isRootGraph(): boolean { + return this.rootGraph === this + } + /** @deprecated See {@link state}.{@link LGraphState.lastNodeId lastNodeId} */ get last_node_id() { return this.state.lastNodeId diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 472c80351..6bf139fc8 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -61,6 +61,7 @@ import { import { NodeInputSlot } from "./node/NodeInputSlot" import { Reroute, type RerouteId } from "./Reroute" import { stringOrEmpty } from "./strings" +import { Subgraph } from "./subgraph/Subgraph" import { CanvasItem, LGraphEventMode, @@ -260,6 +261,8 @@ export class LGraphCanvas { shouldSetCursor: true, } + declare subgraph?: Subgraph + #updateCursorStyle() { if (!this.state.shouldSetCursor) return diff --git a/src/litegraph.ts b/src/litegraph.ts index f571be6ea..c93abfa82 100644 --- a/src/litegraph.ts +++ b/src/litegraph.ts @@ -9,6 +9,9 @@ import type { LGraphNode } from "./LGraphNode" import type { CanvasEventDetail } from "./types/events" import type { RenderShape, TitleMode } from "./types/globalEnums" +// Must remain above LiteGraphGlobal (circular dependency due to abstract factory behaviour in `configure`) +export type { Subgraph } from "./subgraph/Subgraph" + import { LiteGraphGlobal } from "./LiteGraphGlobal" import { loadPolyfills } from "./polyfills" @@ -142,9 +145,13 @@ export { TitleMode, } from "./types/globalEnums" export type { + ExportedSubgraph, + ExportedSubgraphInstance, + ExportedSubgraphIONode, ISerialisedGraph, SerialisableGraph, SerialisableLLink, + SubgraphIO, } from "./types/serialisation" export type { IWidget } from "./types/widgets" export { isColorable } from "./utils/type" diff --git a/src/subgraph/Subgraph.ts b/src/subgraph/Subgraph.ts new file mode 100644 index 000000000..a994b8616 --- /dev/null +++ b/src/subgraph/Subgraph.ts @@ -0,0 +1,74 @@ +import type { ExportedSubgraph, ExposedWidget, Serialisable, SerialisableGraph } from "@/types/serialisation" + +import { type BaseLGraph, LGraph } from "@/LGraph" + +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 { + /** The display name of the subgraph. */ + name: string + + 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[] + + override get rootGraph(): LGraph { + return this.parents[0] + } + + /** @inheritdoc */ + get pathToRootGraph(): readonly [LGraph, ...GraphOrSubgraph[]] { + return [...this.parents, this] + } + + constructor( + readonly parents: readonly [LGraph, ...GraphOrSubgraph[]], + data: ExportedSubgraph, + ) { + if (!parents.length) throw new Error("Subgraph must have at least one parent") + + const cloned = structuredClone(data) + const { name, inputs, outputs, widgets } = cloned + super() + + this.name = name + this.inputs = inputs?.map(x => new SubgraphInput(x, this.inputNode)) ?? [] + this.outputs = outputs?.map(x => new SubgraphOutput(x, this.outputNode)) ?? [] + this.widgets = widgets ?? [] + + this.configure(cloned) + } + + 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/subgraph/SubgraphIONodeBase.ts b/src/subgraph/SubgraphIONodeBase.ts new file mode 100644 index 000000000..4798db385 --- /dev/null +++ b/src/subgraph/SubgraphIONodeBase.ts @@ -0,0 +1,82 @@ +import type { Subgraph } from "./Subgraph" +import type { SubgraphInput } from "./SubgraphInput" +import type { SubgraphOutput } from "./SubgraphOutput" +import type { Point, Positionable, ReadOnlyRect, Rect } from "@/interfaces" +import type { NodeId } from "@/LGraphNode" +import type { ExportedSubgraphIONode, Serialisable } from "@/types/serialisation" + +import { isPointInRect, snapPoint } from "@/measure" + +export abstract class SubgraphIONodeBase implements Positionable, Serialisable { + static margin = 10 + static defaultWidth = 100 + static roundedRadius = 10 + + readonly #boundingRect: Float32Array = new Float32Array(4) + readonly #pos: Point = this.#boundingRect.subarray(0, 2) + readonly #size: Point = this.#boundingRect.subarray(2, 4) + + abstract readonly id: NodeId + + get boundingRect(): Rect { + return this.#boundingRect + } + + selected: boolean = false + pinned: boolean = false + + get pos() { + return this.#pos + } + + set pos(value) { + if (!value || value.length < 2) return + + this.#pos[0] = value[0] + this.#pos[1] = value[1] + } + + get size() { + return this.#size + } + + set size(value) { + if (!value || value.length < 2) return + + this.#size[0] = value[0] + this.#size[1] = value[1] + } + + abstract readonly slots: SubgraphInput[] | SubgraphOutput[] + + constructor( + /** The subgraph that this node belongs to. */ + readonly subgraph: Subgraph, + ) {} + + move(deltaX: number, deltaY: number): void { + this.pos[0] += deltaX + this.pos[1] += deltaY + } + + /** @inheritdoc */ + snapToGrid(snapTo: number): boolean { + return this.pinned ? false : snapPoint(this.pos, snapTo) + } + + containsPoint(point: Point): boolean { + return isPointInRect(point, this.boundingRect) + } + + asSerialisable(): ExportedSubgraphIONode { + return { + id: this.id, + bounding: serialiseRect(this.boundingRect), + pinned: this.pinned ? true : undefined, + } + } +} + +function serialiseRect(rect: ReadOnlyRect): [number, number, number, number] { + return [rect[0], rect[1], rect[2], rect[3]] +} diff --git a/src/subgraph/SubgraphInput.ts b/src/subgraph/SubgraphInput.ts new file mode 100644 index 000000000..b41e76907 --- /dev/null +++ b/src/subgraph/SubgraphInput.ts @@ -0,0 +1,24 @@ +import type { Point, ReadOnlyRect } from "@/interfaces" + +import { SubgraphSlot } from "./SubgraphSlotBase" + +export class SubgraphInput extends SubgraphSlot { + get labelPos(): Point { + const [x, y, , height] = this.boundingRect + return [x, y + height * 0.5] + } + + /** For inputs, x is the right edge of the input node. */ + override arrange(rect: ReadOnlyRect): void { + const [right, top, width, height] = rect + const { boundingRect: b, pos } = this + + b[0] = right - width + b[1] = top + b[2] = width + b[3] = height + + pos[0] = right - height * 0.5 + pos[1] = top + height * 0.5 + } +} diff --git a/src/subgraph/SubgraphInputNode.ts b/src/subgraph/SubgraphInputNode.ts new file mode 100644 index 000000000..f1dcbf9d0 --- /dev/null +++ b/src/subgraph/SubgraphInputNode.ts @@ -0,0 +1,12 @@ +import type { Positionable } from "@/interfaces" +import type { NodeId } from "@/LGraphNode" + +import { SubgraphIONodeBase } from "./SubgraphIONodeBase" + +export class SubgraphInputNode extends SubgraphIONodeBase implements Positionable { + readonly id: NodeId = -10 + + get slots() { + return this.subgraph.inputs + } +} diff --git a/src/subgraph/SubgraphOutput.ts b/src/subgraph/SubgraphOutput.ts new file mode 100644 index 000000000..7f9390da4 --- /dev/null +++ b/src/subgraph/SubgraphOutput.ts @@ -0,0 +1,23 @@ +import type { Point, ReadOnlyRect } from "@/interfaces" + +import { SubgraphSlot } from "./SubgraphSlotBase" + +export class SubgraphOutput extends SubgraphSlot { + get labelPos(): Point { + const [x, y, , height] = this.boundingRect + return [x + height, y + height * 0.5] + } + + override arrange(rect: ReadOnlyRect): void { + const [left, top, width, height] = rect + const { boundingRect: b, pos } = this + + b[0] = left + b[1] = top + b[2] = width + b[3] = height + + pos[0] = left + height * 0.5 + pos[1] = top + height * 0.5 + } +} diff --git a/src/subgraph/SubgraphOutputNode.ts b/src/subgraph/SubgraphOutputNode.ts new file mode 100644 index 000000000..395b74eeb --- /dev/null +++ b/src/subgraph/SubgraphOutputNode.ts @@ -0,0 +1,12 @@ +import type { Positionable } from "@/interfaces" +import type { NodeId } from "@/LGraphNode" + +import { SubgraphIONodeBase } from "./SubgraphIONodeBase" + +export class SubgraphOutputNode extends SubgraphIONodeBase implements Positionable { + readonly id: NodeId = -20 + + get slots() { + return this.subgraph.outputs + } +} diff --git a/src/subgraph/SubgraphSlotBase.ts b/src/subgraph/SubgraphSlotBase.ts new file mode 100644 index 000000000..0890ff7ed --- /dev/null +++ b/src/subgraph/SubgraphSlotBase.ts @@ -0,0 +1,64 @@ +import type { SubgraphIONodeBase } from "./SubgraphIONodeBase" +import type { Point, ReadOnlyRect, Rect } from "@/interfaces" +import type { LinkId } from "@/LLink" +import type { Serialisable, SubgraphIO } from "@/types/serialisation" + +import { LiteGraph } from "@/litegraph" +import { SlotBase } from "@/node/SlotBase" +import { createUuidv4, type UUID } from "@/utils/uuid" + +/** Shared base class for the slots used on Subgraph . */ +export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Serialisable { + static get defaultHeight() { + return LiteGraph.NODE_SLOT_HEIGHT + } + + readonly #pos: Point = new Float32Array(2) + + readonly id: UUID + readonly parent: SubgraphIONodeBase + override type: string + + readonly linkIds: LinkId[] = [] + + readonly boundingRect: Rect = [0, 0, 0, SubgraphSlot.defaultHeight] + + override get pos() { + return this.#pos + } + + override set pos(value) { + if (!value || value.length < 2) return + + this.#pos[0] = value[0] + this.#pos[1] = value[1] + } + + /** Whether this slot is connected to another slot. */ + override get isConnected() { + return this.linkIds.length > 0 + } + + /** The display name of this slot. */ + get displayName() { + return this.label ?? this.localized_name ?? this.name + } + + abstract get labelPos(): Point + + constructor(slot: SubgraphIO, parent: SubgraphIONodeBase) { + super(slot.name, slot.type, slot.boundingRect) + + Object.assign(this, slot) + this.id = slot.id ?? createUuidv4() + this.type = slot.type + this.parent = parent + } + + abstract arrange(rect: ReadOnlyRect): void + + asSerialisable(): SubgraphIO { + const { id, name, type, linkIds, localized_name, label, dir, shape, color_off, color_on, pos, boundingRect } = this + return { id, name, type, linkIds, localized_name, label, dir, shape, color_off, color_on, pos, boundingRect } + } +} diff --git a/src/types/serialisation.ts b/src/types/serialisation.ts index 501ee5f82..1bc9e8735 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -40,7 +40,7 @@ export interface BaseExportedGraph { /** Definitions of re-usable objects that are referenced elsewhere in this exported graph. */ definitions?: { /** The base definition of subgraphs used in this workflow. That is, what you see when you open / edit a subgraph. */ - subgraphs?: Record + subgraphs?: ExportedSubgraph[] } } @@ -91,7 +91,7 @@ export interface ISerialisedNode { } /** Properties of nodes that are used by subgraph instances. */ -type NodeSubgraphSharedProps = Omit +type NodeSubgraphSharedProps = Omit /** A single instance of a subgraph; where it is used on a graph, any customisation to shape / colour etc. */ export interface ExportedSubgraphInstance extends NodeSubgraphSharedProps { @@ -126,16 +126,18 @@ export interface ISerialisedGraph extends BaseExportedGraph { export interface ExportedSubgraph extends SerialisableGraph { /** The display name of the subgraph. */ name: string + inputNode: ExportedSubgraphIONode + outputNode: ExportedSubgraphIONode /** 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. */ - inputs: SubgraphIO[] + inputs?: SubgraphIO[] /** 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. */ - outputs: SubgraphIO[] + outputs?: SubgraphIO[] /** A list of node widgets displayed in the parent graph, on the subgraph object. */ - widgets: ExposedWidget[] + widgets?: ExposedWidget[] } /** Properties shared by subgraph and node I/O slots. */ -type SubgraphIOShared = Omit +type SubgraphIOShared = Omit /** Subgraph I/O slots */ export interface SubgraphIO extends SubgraphIOShared { @@ -143,6 +145,8 @@ export interface SubgraphIO extends SubgraphIOShared { id: UUID /** The data type this slot uses. Unlike nodes, this does not support legacy numeric types. */ type: string + /** Links connected to this slot, or `undefined` if not connected. An ouptut slot should only ever have one link. */ + linkIds?: LinkId[] } /** A reference to a node widget shown in the parent graph */ @@ -209,3 +213,9 @@ export interface SerialisableLLink { /** ID of the last reroute (from input to output) that this link passes through, otherwise `undefined` */ parentId?: RerouteId } + +export interface ExportedSubgraphIONode { + id: NodeId + bounding: [number, number, number, number] + pinned?: boolean +}