Skip to content

Add Subgraphs #1000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 54 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
11de079
[Cleanup] Convert SubgraphIONode to Rectangle
webfiltered May 10, 2025
4e36169
Add container alignment functions
webfiltered Apr 30, 2025
e45974e
Add subgraph accessor
webfiltered May 2, 2025
42a5fd7
[TS] Add subgraph to canvas union type
webfiltered May 2, 2025
ffd1c25
[CodeHealth] Improve alignment function legibility
webfiltered May 4, 2025
1e83e18
nit
webfiltered May 5, 2025
26b5934
[API] Allow LGraph configure properties be changed
webfiltered May 10, 2025
6391c37
nit
webfiltered May 11, 2025
e000103
Emit configure events in LGraph.configure
webfiltered May 11, 2025
7101bef
nit
webfiltered May 11, 2025
3bdddd7
Fix node IDs cannot be created during graph config
webfiltered May 11, 2025
6c8161c
[Refactor] Use Rectangle for slot boundingRect
webfiltered May 12, 2025
135b975
[Refactor] Ensure Rectangle used for all slots
webfiltered May 12, 2025
fce5982
Add convert to subgraph context menu
webfiltered May 10, 2025
8c1bbec
[PARTIAL] Add Subgraphs
webfiltered May 12, 2025
3c952f9
[Perf] Prefer WeakSet where possible
webfiltered May 13, 2025
8a08f2c
Fix nested subgraph conversion missing ionode link
webfiltered May 13, 2025
aeb35ba
nit - Skip warning log
webfiltered May 13, 2025
e404529
Fix nested subgraph relinkage
webfiltered May 13, 2025
0fae386
[DEBUG] Draw link ID on centre markers
webfiltered May 13, 2025
854d3ab
[Cleanup] Remove hard-coded subgraph keybind
webfiltered May 15, 2025
f6e6bb4
Split subgraph load to permit hard ref usage
webfiltered May 15, 2025
e211478
[Cleanup] Remove tmp. canvas code from graph
webfiltered May 15, 2025
9152580
[Test] Update expectations
webfiltered May 15, 2025
96115ee
[Test] Fix readonly prop assign
webfiltered May 15, 2025
cf12399
Replace IO node IDs with constants
webfiltered May 19, 2025
05af130
Add subgraph IO node to resolved connections
webfiltered May 19, 2025
ccfa5cb
[Refactor] Move connections grouping to function
webfiltered May 19, 2025
002d84c
nit - Destructuring
webfiltered May 19, 2025
d9a1ef9
nit
webfiltered May 19, 2025
05c1e47
[Refactor] Prefer Rectangle accessors
webfiltered May 19, 2025
8922b36
Fix Rectangle containsPoint matches too much
webfiltered May 19, 2025
8d19fdb
Fix Rectangle containsRect area too large
webfiltered May 19, 2025
5c95dc3
Add Hoverable, hover effects to Subgraph IO nodes
webfiltered May 19, 2025
9edc442
[Refactor] Pull repeated code up to base class
webfiltered May 19, 2025
c191c17
[Cleanup] Remove redundant code - prefer Rectangle
webfiltered May 19, 2025
023910f
nit
webfiltered May 19, 2025
78a0a49
Add IO node hover effect to base class
webfiltered May 19, 2025
bdebc3d
Fix subgraph IO node click affects items below
webfiltered May 19, 2025
c5bd220
nit - Doc
webfiltered May 20, 2025
84cb274
[Refactor] SoC canvas dirty into event handler
webfiltered May 20, 2025
1cd4b33
Add cursor update when over IO nodes
webfiltered May 20, 2025
969fecc
[API] Remove unused code
webfiltered May 21, 2025
4f1a756
Fix link not made to new node, if first slot
webfiltered May 21, 2025
f581d1e
[PARTIAL] Fix subgraph new connections, nodes, etc
webfiltered May 21, 2025
6f5cf10
[Test] Update expectations
webfiltered May 21, 2025
1c8080c
nit
webfiltered May 21, 2025
450888d
Fix delete button shown for io nodes
webfiltered May 22, 2025
6f5f340
Add removable interface
webfiltered May 22, 2025
4742c6e
Fix noodles can be pulled from io node slot labels
webfiltered May 22, 2025
45fce1f
Add / replace subgraph convert context menu opts
webfiltered May 22, 2025
2f45228
Add new emoji to menu
webfiltered May 22, 2025
bb1932c
Remove pathToRootGraph completely
webfiltered May 22, 2025
2affe98
Fix escape key event always swallowed
webfiltered May 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
521 changes: 390 additions & 131 deletions src/LGraph.ts

Large diffs are not rendered by default.

438 changes: 329 additions & 109 deletions src/LGraphCanvas.ts

Large diffs are not rendered by default.

51 changes: 36 additions & 15 deletions src/LGraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,26 @@ import type {
ISlotType,
Point,
Positionable,
ReadOnlyPoint,
ReadOnlyRect,
Rect,
Size,
} from "./interfaces"
import type { LGraph } from "./LGraph"
import type { Reroute, RerouteId } from "./Reroute"
import type { SubgraphInputNode } from "./subgraph/SubgraphInputNode"
import type { SubgraphOutputNode } from "./subgraph/SubgraphOutputNode"
import type { CanvasMouseEvent } from "./types/events"
import type { ISerialisedNode } from "./types/serialisation"
import type { NodeLike } from "./types/NodeLike"
import type { ISerialisedNode, SubgraphIO } from "./types/serialisation"
import type { IBaseWidget, IWidgetOptions, TWidgetType, TWidgetValue } from "./types/widgets"

import { getNodeInputOnPos, getNodeOutputOnPos } from "./canvas/measureSlots"
import { NullGraphError } from "./infrastructure/NullGraphError"
import { Rectangle } from "./infrastructure/Rectangle"
import { BadgePosition, LGraphBadge } from "./LGraphBadge"
import { LGraphCanvas } from "./LGraphCanvas"
import { type LGraphNodeConstructor, LiteGraph } from "./litegraph"
import { type LGraphNodeConstructor, LiteGraph, type SubgraphNode } from "./litegraph"
import { LLink } from "./LLink"
import { createBounds, isInRect, isInRectangle, isPointInRect, snapPoint } from "./measure"
import { NodeInputSlot } from "./node/NodeInputSlot"
Expand Down Expand Up @@ -181,7 +186,7 @@ export interface LGraphNode {
* @param type a type for the node
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class LGraphNode implements Positionable, IPinnable, IColorable {
export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable {
// Static properties used by dynamic child classes
static title?: string
static MAX_CONSOLE?: number
Expand All @@ -206,6 +211,10 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
return `normal ${LiteGraph.NODE_SUBTEXT_SIZE}px ${LiteGraph.NODE_FONT}`
}

get displayType(): string {
return this.type
}

graph: LGraph | null = null
id: NodeId
type: string = ""
Expand Down Expand Up @@ -338,6 +347,14 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
selected?: boolean
showAdvanced?: boolean

declare comfyClass?: string
declare isVirtualNode?: boolean
applyToGraph?(extraLinks?: LLink[]): void

isSubgraphNode(): this is SubgraphNode {
return false
}

/** @inheritdoc {@link renderArea} */
#renderArea: Float32Array = new Float32Array(4)
/**
Expand All @@ -349,16 +366,23 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
}

/** @inheritdoc {@link boundingRect} */
#boundingRect: Float32Array = new Float32Array(4)
#boundingRect: Rectangle = new Rectangle()

/**
* Cached node position & area as `x, y, width, height`. Includes changes made by {@link onBounding}, if present.
*
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
*/
get boundingRect(): ReadOnlyRect {
get boundingRect(): Rectangle {
return this.#boundingRect
}

/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
get boundingOffset(): ReadOnlyPoint {
const { pos: [posX, posY], boundingRect: [bX, bY] } = this
return [posX - bX, posY - bY]
}

/** {@link pos} and {@link size} values are backed by this {@link Rect}. */
_posSize: Float32Array = new Float32Array(4)
_pos: Point = this._posSize.subarray(0, 2)
Expand Down Expand Up @@ -443,16 +467,16 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
this: LGraphNode,
target_slot: number,
type: unknown,
output: INodeOutputSlot,
node: LGraphNode,
output: INodeOutputSlot | SubgraphIO,
node: LGraphNode | SubgraphInputNode,
slot: number,
): boolean
onConnectOutput?(
this: LGraphNode,
slot: number,
type: unknown,
input: INodeInputSlot,
target_node: number | LGraphNode,
input: INodeInputSlot | SubgraphIO,
target_node: number | LGraphNode | SubgraphOutputNode,
target_slot: number,
): boolean
onResize?(this: LGraphNode, size: Size): void
Expand Down Expand Up @@ -2360,9 +2384,9 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
}

canConnectTo(
node: LGraphNode,
toSlot: INodeInputSlot,
fromSlot: INodeOutputSlot,
node: NodeLike,
toSlot: INodeInputSlot | SubgraphIO,
fromSlot: INodeOutputSlot | SubgraphIO,
) {
return this.id !== node.id && LiteGraph.isValidConnection(fromSlot.type, toSlot.type)
}
Expand Down Expand Up @@ -2572,7 +2596,6 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {

this.setDirtyCanvas(false, true)
graph.afterChange()
graph.connectionChange(this)

return link
}
Expand Down Expand Up @@ -2740,7 +2763,6 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
}

this.setDirtyCanvas(false, true)
graph.connectionChange(this)
return true
}

Expand Down Expand Up @@ -2821,7 +2843,6 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
}

this.setDirtyCanvas(false, true)
graph?.connectionChange(this)
return true
}

Expand Down
72 changes: 65 additions & 7 deletions src/LLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import type {
} from "./interfaces"
import type { LGraphNode, NodeId } from "./LGraphNode"
import type { Reroute, RerouteId } from "./Reroute"
import type { Serialisable, SerialisableLLink } from "./types/serialisation"
import type { Serialisable, SerialisableLLink, SubgraphIO } from "./types/serialisation"

import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from "@/constants"

export type LinkId = number

Expand All @@ -22,18 +24,54 @@ export type SerialisedLLinkArray = [
type: ISlotType,
]

export interface ResolvedConnection {
// Resolved connection union; eliminates subgraph in/out as a possibility
export type ResolvedConnection = BaseResolvedConnection &
(
(ResolvedSubgraphInput & ResolvedNormalOutput) |
(ResolvedNormalInput & ResolvedSubgraphOutput) |
(ResolvedNormalInput & ResolvedNormalOutput)
)

interface BaseResolvedConnection {
link: LLink
inputNode?: LGraphNode
input?: INodeInputSlot
outputNode?: LGraphNode
output?: INodeOutputSlot
subgraphOutput?: SubgraphIO
subgraphInput?: SubgraphIO
}

interface ResolvedNormalInput {
inputNode: LGraphNode | undefined
outputNode: LGraphNode | undefined
input: INodeInputSlot | undefined
subgraphOutput?: undefined
}

interface ResolvedNormalOutput {
outputNode: LGraphNode | undefined
output: INodeOutputSlot | undefined
link: LLink
subgraphInput?: undefined
}

type BasicReadonlyNetwork = Pick<ReadonlyLinkNetwork, "getNodeById" | "links" | "getLink">
interface ResolvedSubgraphInput {
inputNode?: undefined
input?: undefined
subgraphOutput: SubgraphIO
}

interface ResolvedSubgraphOutput {
outputNode?: undefined
output?: undefined
subgraphInput: SubgraphIO
}

type BasicReadonlyNetwork = Pick<ReadonlyLinkNetwork, "getNodeById" | "links" | "getLink" | "inputNode" | "outputNode">

// this is the class in charge of storing link information
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
static _drawDebug = false

/** Link ID */
id: LinkId
parentId?: RerouteId
Expand Down Expand Up @@ -83,6 +121,16 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
return this.isFloatingOutput || this.isFloatingInput
}

/** `true` if this link is connected to a subgraph input node (the actual origin is in a different graph). */
get originIsIoNode(): boolean {
return this.origin_id === SUBGRAPH_INPUT_ID
}

/** `true` if this link is connected to a subgraph output node (the actual target is in a different graph). */
get targetIsIoNode(): boolean {
return this.target_id === SUBGRAPH_OUTPUT_ID
}

constructor(
id: LinkId,
type: ISlotType,
Expand Down Expand Up @@ -230,10 +278,20 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
*/
resolve(network: BasicReadonlyNetwork): ResolvedConnection {
const inputNode = this.target_id === -1 ? undefined : network.getNodeById(this.target_id) ?? undefined
const outputNode = this.origin_id === -1 ? undefined : network.getNodeById(this.origin_id) ?? undefined
const input = inputNode?.inputs[this.target_slot]
const subgraphInput = this.originIsIoNode ? network.inputNode?.slots[this.origin_slot] : undefined
if (subgraphInput) {
return { inputNode, input, subgraphInput, link: this }
}

const outputNode = this.origin_id === -1 ? undefined : network.getNodeById(this.origin_id) ?? undefined
const output = outputNode?.outputs[this.origin_slot]
return { inputNode, outputNode, input, output, link: this }
const subgraphOutput = this.targetIsIoNode ? network.outputNode?.slots[this.target_slot] : undefined
if (subgraphOutput) {
return { outputNode, output, subgraphInput: undefined, subgraphOutput, link: this }
}

return { inputNode, outputNode, input, output, subgraphInput, subgraphOutput, link: this }
}

configure(o: LLink | SerialisedLLinkArray) {
Expand Down
19 changes: 18 additions & 1 deletion src/LiteGraphGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { ContextMenu } from "./ContextMenu"
import { CurveEditor } from "./CurveEditor"
import { DragAndScale } from "./DragAndScale"
import { LabelPosition, SlotDirection, SlotShape, SlotType } from "./draw"
import { Rectangle } from "./infrastructure/Rectangle"
import { LGraph } from "./LGraph"
import { LGraphCanvas } from "./LGraphCanvas"
import { LGraphGroup } from "./LGraphGroup"
import { LGraphNode } from "./LGraphNode"
import { LLink } from "./LLink"
import { distance, isInsideRectangle, overlapBounding } from "./measure"
import { Reroute } from "./Reroute"
import { SubgraphIONodeBase } from "./subgraph/SubgraphIONodeBase"
import { SubgraphSlot } from "./subgraph/SubgraphSlotBase"
import {
LGraphEventMode,
LinkDirection,
Expand Down Expand Up @@ -324,7 +327,21 @@ export class LiteGraphGlobal {
ContextMenu = ContextMenu
CurveEditor = CurveEditor
Reroute = Reroute
InputIndicators = InputIndicators

constructor() {
Object.defineProperty(this, "Classes", { writable: false })
}

Classes = {
get SubgraphSlot() { return SubgraphSlot },
get SubgraphIONodeBase() { return SubgraphIONodeBase },

// Rich drawing
get Rectangle() { return Rectangle },

// Debug / helpers
get InputIndicators() { return InputIndicators },
}

onNodeTypeRegistered?(type: string, base_class: typeof LGraphNode): void
onNodeTypeReplaced?(type: string, base_class: typeof LGraphNode, prev: unknown): void
Expand Down
43 changes: 38 additions & 5 deletions src/canvas/LinkConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { ConnectingLink, ItemLocator, LinkNetwork, LinkSegment } from "@/in
import type { INodeInputSlot, INodeOutputSlot } from "@/interfaces"
import type { LGraphNode } from "@/LGraphNode"
import type { Reroute } from "@/Reroute"
import type { SubgraphInput } from "@/subgraph/SubgraphInput"
import type { SubgraphInputNode } from "@/subgraph/SubgraphInputNode"
import type { SubgraphOutput } from "@/subgraph/SubgraphOutput"
import type { SubgraphOutputNode } from "@/subgraph/SubgraphOutputNode"
import type { CanvasPointerEvent } from "@/types/events"
import type { IBaseWidget } from "@/types/widgets"

Expand All @@ -15,7 +19,9 @@ import { FloatingRenderLink } from "./FloatingRenderLink"
import { MovingInputLink } from "./MovingInputLink"
import { MovingLinkBase } from "./MovingLinkBase"
import { MovingOutputLink } from "./MovingOutputLink"
import { ToInputFromIoNodeLink } from "./ToInputFromIoNodeLink"
import { ToInputRenderLink } from "./ToInputRenderLink"
import { ToOutputFromIoNodeLink } from "./ToOutputFromIoNodeLink"
import { ToOutputFromRerouteLink } from "./ToOutputFromRerouteLink"
import { ToOutputRenderLink } from "./ToOutputRenderLink"

Expand All @@ -39,7 +45,14 @@ export interface LinkConnectorState {
}

/** Discriminated union to simplify type narrowing. */
type RenderLinkUnion = MovingInputLink | MovingOutputLink | FloatingRenderLink | ToInputRenderLink | ToOutputRenderLink
type RenderLinkUnion =
| MovingInputLink
| MovingOutputLink
| FloatingRenderLink
| ToInputRenderLink
| ToOutputRenderLink
| ToInputFromIoNodeLink
| ToOutputFromIoNodeLink

export interface LinkConnectorExport {
renderLinks: RenderLink[]
Expand Down Expand Up @@ -261,6 +274,24 @@ export class LinkConnector {
this.#setLegacyLinks(true)
}

dragNewFromSubgraphInput(network: LinkNetwork, inputNode: SubgraphInputNode, input: SubgraphInput, fromReroute?: Reroute): void {
if (this.isConnecting) throw new Error("Already dragging links.")

const renderLink = new ToInputFromIoNodeLink(network, inputNode, input, fromReroute)
this.renderLinks.push(renderLink)

this.state.connectingTo = "input"
}

dragNewFromSubgraphOutput(network: LinkNetwork, outputNode: SubgraphOutputNode, output: SubgraphOutput, fromReroute?: Reroute): void {
if (this.isConnecting) throw new Error("Already dragging links.")

const renderLink = new ToOutputFromIoNodeLink(network, outputNode, output, fromReroute)
this.renderLinks.push(renderLink)

this.state.connectingTo = "output"
}

/**
* Drags a new link from a reroute to an input slot.
* @param network The network that the link being connected belongs to
Expand Down Expand Up @@ -523,7 +554,8 @@ export class LinkConnector {
if (connectingTo === "output") {
// Dropping new output link
const output = node.findOutputByType(firstLink.fromSlot.type)?.slot
if (!output) {
console.debug("out", node, output, firstLink.fromSlot)
if (output === undefined) {
console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`)
return
}
Expand All @@ -532,7 +564,8 @@ export class LinkConnector {
} else if (connectingTo === "input") {
// Dropping new input link
const input = node.findInputByType(firstLink.fromSlot.type)?.slot
if (!input) {
console.debug("in", node, input, firstLink.fromSlot)
if (input === undefined) {
console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`)
return
}
Expand Down Expand Up @@ -616,7 +649,7 @@ export class LinkConnector {
const afterRerouteId = link instanceof MovingLinkBase ? link.link?.parentId : link.fromReroute?.id

return {
node: link.node,
node: link.node as LGraphNode,
slot: link.fromSlotIndex,
input,
output,
Expand Down Expand Up @@ -690,7 +723,7 @@ export class LinkConnector {

/** Validates that a single {@link RenderLink} can be dropped on the specified reroute. */
function canConnectInputLinkToReroute(
link: ToInputRenderLink | MovingInputLink | FloatingRenderLink,
link: ToInputRenderLink | MovingInputLink | FloatingRenderLink | ToInputFromIoNodeLink,
inputNode: LGraphNode,
input: INodeInputSlot,
reroute: Reroute,
Expand Down
Loading
Loading