Add subgraph skeleton classes (#997)

Allows downstream consumers to use subgraph types ahead of impl.
This commit is contained in:
filtered
2025-05-02 09:16:19 +10:00
committed by GitHub
parent ee89fc575f
commit 199eeae269
11 changed files with 330 additions and 7 deletions

74
src/subgraph/Subgraph.ts Normal file
View File

@@ -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<ExportedSubgraph> {
/** 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<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,
}
}
}

View File

@@ -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<ExportedSubgraphIONode> {
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]]
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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<SubgraphIO> {
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 }
}
}