Add Subgraphs (#1000)

This commit is contained in:
filtered
2025-06-28 15:21:56 -07:00
committed by GitHub
parent 3e7f9627b4
commit bcaaa00770
54 changed files with 3662 additions and 462 deletions

View File

@@ -0,0 +1,75 @@
import type { ReadOnlyRect, ReadOnlySize, Size } from "@/interfaces"
import { clamp } from "@/litegraph"
/**
* Basic width and height, with min/max constraints.
*
* - The {@link width} and {@link height} properties are readonly
* - Size is set via {@link desiredWidth} and {@link desiredHeight} properties
* - Width and height are then updated, clamped to min/max values
*/
export class ConstrainedSize {
#width: number = 0
#height: number = 0
#desiredWidth: number = 0
#desiredHeight: number = 0
minWidth: number = 0
minHeight: number = 0
maxWidth: number = Infinity
maxHeight: number = Infinity
get width() {
return this.#width
}
get height() {
return this.#height
}
get desiredWidth() {
return this.#desiredWidth
}
set desiredWidth(value: number) {
this.#desiredWidth = value
this.#width = clamp(value, this.minWidth, this.maxWidth)
}
get desiredHeight() {
return this.#desiredHeight
}
set desiredHeight(value: number) {
this.#desiredHeight = value
this.#height = clamp(value, this.minHeight, this.maxHeight)
}
constructor(width: number, height: number) {
this.desiredWidth = width
this.desiredHeight = height
}
static fromSize(size: ReadOnlySize): ConstrainedSize {
return new ConstrainedSize(size[0], size[1])
}
static fromRect(rect: ReadOnlyRect): ConstrainedSize {
return new ConstrainedSize(rect[2], rect[3])
}
setSize(size: ReadOnlySize): void {
this.desiredWidth = size[0]
this.desiredHeight = size[1]
}
setValues(width: number, height: number): void {
this.desiredWidth = width
this.desiredHeight = height
}
toSize(): Size {
return [this.#width, this.#height]
}
}

View File

@@ -0,0 +1,6 @@
export class InvalidLinkError extends Error {
constructor(message: string = "Attempted to access a link that was invalid.", cause?: Error) {
super(message, { cause })
this.name = "InvalidLinkError"
}
}

View File

@@ -1,9 +1,19 @@
import type { ConnectingLink } from "@/interfaces"
import type { LGraph } from "@/LGraph"
import type { LGraphGroup } from "@/LGraphGroup"
import type { LGraphNode } from "@/LGraphNode"
import type { Subgraph } from "@/subgraph/Subgraph"
import type { CanvasPointerEvent } from "@/types/events"
export interface LGraphCanvasEventMap {
/** The active graph has changed. */
"litegraph:set-graph": {
/** The new active graph. */
newGraph: LGraph | Subgraph
/** The old active graph, or `null` if there was no active graph. */
oldGraph: LGraph | Subgraph | null | undefined
}
"litegraph:canvas":
| { subType: "before-change" | "after-change" }
| {

View File

@@ -0,0 +1,47 @@
import type { ReadOnlyRect } from "@/interfaces"
import type { LGraph } from "@/LGraph"
import type { LLink, ResolvedConnection } from "@/LLink"
import type { Subgraph } from "@/subgraph/Subgraph"
import type { ExportedSubgraph, ISerialisedGraph, SerialisableGraph } from "@/types/serialisation"
export interface LGraphEventMap {
"configuring": {
/** The data that was used to configure the graph. */
data: ISerialisedGraph | SerialisableGraph
/** If `true`, the graph will be cleared prior to adding the configuration. */
clearGraph: boolean
}
"configured": never
"subgraph-created": {
/** The subgraph that was created. */
subgraph: Subgraph
/** The raw data that was used to create the subgraph. */
data: ExportedSubgraph
}
/** Dispatched when a group of items are converted to a subgraph. */
"convert-to-subgraph": {
/** The type of subgraph to create. */
subgraph: Subgraph
/** The boundary around every item that was moved into the subgraph. */
bounds: ReadOnlyRect
/** The raw data that was used to create the subgraph. */
exportedSubgraph: ExportedSubgraph
/** The links that were used to create the subgraph. */
boundaryLinks: LLink[]
/** Links that go from outside the subgraph in, via an input on the subgraph node. */
resolvedInputLinks: ResolvedConnection[]
/** Links that go from inside the subgraph out, via an output on the subgraph node. */
resolvedOutputLinks: ResolvedConnection[]
/** The floating links that were used to create the subgraph. */
boundaryFloatingLinks: LLink[]
/** The internal links that were used to create the subgraph. */
internalLinks: LLink[]
}
"open-subgraph": {
subgraph: Subgraph
closingGraph: LGraph | Subgraph
}
}

View File

@@ -6,6 +6,8 @@ import type { ToInputRenderLink } from "@/canvas/ToInputRenderLink"
import type { LGraphNode } from "@/LGraphNode"
import type { LLink } from "@/LLink"
import type { Reroute } from "@/Reroute"
import type { SubgraphInputNode } from "@/subgraph/SubgraphInputNode"
import type { SubgraphOutputNode } from "@/subgraph/SubgraphOutputNode"
import type { CanvasPointerEvent } from "@/types/events"
import type { IWidget } from "@/types/widgets"
@@ -37,6 +39,10 @@ export interface LinkConnectorEventMap {
node: LGraphNode
event: CanvasPointerEvent
}
"dropped-on-io-node": {
node: SubgraphInputNode | SubgraphOutputNode
event: CanvasPointerEvent
}
"dropped-on-canvas": CanvasPointerEvent
"dropped-on-widget": {

View File

@@ -6,7 +6,8 @@ import { isInRectangle } from "@/measure"
* A rectangle, represented as a float64 array of 4 numbers: [x, y, width, height].
*
* This class is a subclass of Float64Array, and so has all the methods of that class. Notably,
* {@link Rectangle.from} can be used to convert a {@link ReadOnlyRect}.
* {@link Rectangle.from} can be used to convert a {@link ReadOnlyRect}. Typing of this however,
* is broken due to the base TS lib returning Float64Array rather than `this`.
*
* Sub-array properties ({@link Float64Array.subarray}):
* - {@link pos}: The position of the top-left corner of the rectangle.
@@ -25,6 +26,29 @@ export class Rectangle extends Float64Array {
this[3] = height
}
static override from([x, y, width, height]: ReadOnlyRect): Rectangle {
return new Rectangle(x, y, width, height)
}
/**
* Creates a new rectangle positioned at the given centre, with the given width/height.
* @param centre The centre of the rectangle, as an `[x, y]` point
* @param width The width of the rectangle
* @param height The height of the rectangle. Default: {@link width}
* @returns A new rectangle whose centre is at {@link x}
*/
static fromCentre([x, y]: ReadOnlyPoint, width: number, height = width): Rectangle {
const left = x - width * 0.5
const top = y - height * 0.5
return new Rectangle(left, top, width, height)
}
static ensureRect(rect: ReadOnlyRect): Rectangle {
return rect instanceof Rectangle
? rect
: new Rectangle(rect[0], rect[1], rect[2], rect[3])
}
override subarray(begin: number = 0, end?: number): Float64Array<ArrayBuffer> {
const byteOffset = begin << 3
const length = end === undefined ? end : end - begin
@@ -163,7 +187,7 @@ export class Rectangle extends Float64Array {
* @returns `true` if the point is inside this rectangle, otherwise `false`.
*/
containsXy(x: number, y: number): boolean {
const { x: left, y: top, width, height } = this
const [left, top, width, height] = this
return x >= left &&
x < left + width &&
y >= top &&
@@ -175,23 +199,35 @@ export class Rectangle extends Float64Array {
* @param point The point to check
* @returns `true` if {@link point} is inside this rectangle, otherwise `false`.
*/
containsPoint(point: ReadOnlyPoint): boolean {
return this.x <= point[0] &&
this.y <= point[1] &&
this.x + this.width >= point[0] &&
this.y + this.height >= point[1]
containsPoint([x, y]: ReadOnlyPoint): boolean {
const [left, top, width, height] = this
return x >= left &&
x < left + width &&
y >= top &&
y < top + height
}
/**
* Checks if {@link rect} is inside this rectangle.
* @param rect The rectangle to check
* @returns `true` if {@link rect} is inside this rectangle, otherwise `false`.
* Checks if {@link other} is a smaller rectangle inside this rectangle.
* One **must** be larger than the other; identical rectangles are not considered to contain each other.
* @param other The rectangle to check
* @returns `true` if {@link other} is inside this rectangle, otherwise `false`.
*/
containsRect(rect: ReadOnlyRect): boolean {
return this.x <= rect[0] &&
this.y <= rect[1] &&
this.x + this.width >= rect[0] + rect[2] &&
this.y + this.height >= rect[1] + rect[3]
containsRect(other: ReadOnlyRect): boolean {
const { right, bottom } = this
const otherRight = other[0] + other[2]
const otherBottom = other[1] + other[3]
const identical = this.x === other[0] &&
this.y === other[1] &&
right === otherRight &&
bottom === otherBottom
return !identical &&
this.x <= other[0] &&
this.y <= other[1] &&
right >= otherRight &&
bottom >= otherBottom
}
/**
@@ -345,6 +381,10 @@ export class Rectangle extends Float64Array {
this[1] += currentHeight - height
}
clone(): Rectangle {
return new Rectangle(this[0], this[1], this[2], this[3])
}
/** Alias of {@link export}. */
toArray() { return this.export() }
@@ -353,7 +393,10 @@ export class Rectangle extends Float64Array {
return [this[0], this[1], this[2], this[3]]
}
/** Draws a debug outline of this rectangle. */
/**
* Draws a debug outline of this rectangle.
* @internal Convenience debug/development interface; not for production use.
*/
_drawDebug(ctx: CanvasRenderingContext2D, colour = "red") {
const { strokeStyle, lineWidth } = ctx
try {

View File

@@ -0,0 +1,9 @@
/**
* Error thrown when infinite recursion is detected.
*/
export class RecursionError extends Error {
constructor(subject: string) {
super(subject)
this.name = "RecursionError"
}
}

View File

@@ -0,0 +1,6 @@
export class SlotIndexError extends Error {
constructor(message: string = "Attempted to access a slot that was out of bounds.", cause?: Error) {
super(message, { cause })
this.name = "SlotIndexError"
}
}