refactor: migrate ES private fields to TypeScript private for Vue Proxy compatibility (#8440)

## Summary

Migrates ECMAScript private fields (`#`) to TypeScript private
(`private`) across LiteGraph to fix Vue Proxy reactivity
incompatibility.

## Problem

ES private fields (`#field`) are incompatible with Vue's Proxy-based
reactivity system - accessing `#field` through a Proxy throws
`TypeError: Cannot read private member from an object whose class did
not declare it`.

## Solution

- Converted all `#field` to `private _field` across 10 phases
- Added `toJSON()` methods to `LGraph`, `NodeSlot`, `NodeInputSlot`, and
`NodeOutputSlot` to prevent circular reference errors during
serialization (TypeScript private fields are visible to `JSON.stringify`
unlike true ES private fields)
- Made `DragAndScale.element.data` non-enumerable to break canvas
circular reference chain

## Testing

- All 4027 unit tests pass
- Added 9 new serialization tests to catch future circular reference
issues
- Browser tests (undo/redo, save workflows) verified working

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8440-refactor-migrate-ES-private-fields-to-TypeScript-private-for-Vue-Proxy-compatibility-2f76d73d365081a3bd82d429a3e0fcb7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-29 18:18:58 -08:00
committed by GitHub
parent 82bacb82a7
commit 067d80c4ed
28 changed files with 653 additions and 705 deletions

View File

@@ -292,10 +292,10 @@ export class GroupNodeConfig {
this.processNode(node, seenInputs, seenOutputs)
}
for (const p of this.#convertedToProcess) {
for (const p of this._convertedToProcess) {
p()
}
this.#convertedToProcess = []
this._convertedToProcess = []
if (!this.nodeDef) return
await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, this.nodeDef)
useNodeDefStore().addNodeDef(this.nodeDef)
@@ -773,7 +773,7 @@ export class GroupNodeConfig {
}
}
#convertedToProcess: (() => void)[] = []
private _convertedToProcess: (() => void)[] = []
processNodeInputs(
node: GroupNodeData,
seenInputs: Record<string, number>,
@@ -804,7 +804,7 @@ export class GroupNodeConfig {
)
// Converted inputs have to be processed after all other nodes as they'll be at the end of the list
this.#convertedToProcess.push(() =>
this._convertedToProcess.push(() =>
this.processConvertedWidgets(
inputs,
node,

View File

@@ -103,7 +103,7 @@ export class PrimitiveNode extends LGraphNode {
override onAfterGraphConfigured() {
if (this.outputs[0].links?.length && !this.widgets?.length) {
this.#onFirstConnection()
this._onFirstConnection()
// Populate widget values from config data
if (this.widgets && this.widgets_values) {
@@ -116,7 +116,7 @@ export class PrimitiveNode extends LGraphNode {
}
// Merge values if required
this.#mergeWidgetConfig()
this._mergeWidgetConfig()
}
}
@@ -133,11 +133,11 @@ export class PrimitiveNode extends LGraphNode {
const links = this.outputs[0].links
if (connected) {
if (links?.length && !this.widgets?.length) {
this.#onFirstConnection()
this._onFirstConnection()
}
} else {
// We may have removed a link that caused the constraints to change
this.#mergeWidgetConfig()
this._mergeWidgetConfig()
if (!links?.length) {
this.onLastDisconnect()
@@ -159,7 +159,7 @@ export class PrimitiveNode extends LGraphNode {
}
if (this.outputs[slot].links?.length) {
const valid = this.#isValidConnection(input)
const valid = this._isValidConnection(input)
if (valid) {
// On connect of additional outputs, copy our value to their widget
this.applyToGraph([{ target_id: target_node.id, target_slot } as LLink])
@@ -170,7 +170,7 @@ export class PrimitiveNode extends LGraphNode {
return true
}
#onFirstConnection(recreating?: boolean) {
private _onFirstConnection(recreating?: boolean) {
// First connection can fire before the graph is ready on initial load so random things can be missing
if (!this.outputs[0].links || !this.graph) {
this.onLastDisconnect()
@@ -204,7 +204,7 @@ export class PrimitiveNode extends LGraphNode {
this.outputs[0].name = type
this.outputs[0].widget = widget
this.#createWidget(
this._createWidget(
widget[CONFIG] ?? config,
theirNode,
widget.name,
@@ -213,7 +213,7 @@ export class PrimitiveNode extends LGraphNode {
)
}
#createWidget(
private _createWidget(
inputData: InputSpec,
node: LGraphNode,
widgetName: string,
@@ -307,8 +307,8 @@ export class PrimitiveNode extends LGraphNode {
recreateWidget() {
const values = this.widgets?.map((w) => w.value)
this.#removeWidgets()
this.#onFirstConnection(true)
this._removeWidgets()
this._onFirstConnection(true)
if (values?.length && this.widgets) {
for (let i = 0; i < this.widgets.length; i++)
this.widgets[i].value = values[i]
@@ -316,7 +316,7 @@ export class PrimitiveNode extends LGraphNode {
return this.widgets?.[0]
}
#mergeWidgetConfig() {
private _mergeWidgetConfig() {
// Merge widget configs if the node has multiple outputs
const output = this.outputs[0]
const links = output.links ?? []
@@ -348,11 +348,11 @@ export class PrimitiveNode extends LGraphNode {
const theirInput = theirNode.inputs[link.target_slot]
// Call is valid connection so it can merge the configs when validating
this.#isValidConnection(theirInput, hasConfig)
this._isValidConnection(theirInput, hasConfig)
}
}
#isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
private _isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
// Only allow connections where the configs match
const output = this.outputs?.[0]
const config2 = (input.widget?.[GET_CONFIG] as () => InputSpec)?.()
@@ -367,7 +367,7 @@ export class PrimitiveNode extends LGraphNode {
)
}
#removeWidgets() {
private _removeWidgets() {
if (this.widgets) {
// Allow widgets to cleanup
for (const w of this.widgets) {
@@ -398,7 +398,7 @@ export class PrimitiveNode extends LGraphNode {
this.outputs[0].name = 'connect to widget input'
delete this.outputs[0].widget
this.#removeWidgets()
this._removeWidgets()
}
}

View File

@@ -31,17 +31,17 @@ export class CanvasPointer {
/** Maximum offset from click location */
static get maxClickDrift() {
return this.#maxClickDrift
return this._maxClickDrift
}
static set maxClickDrift(value) {
this.#maxClickDrift = value
this.#maxClickDrift2 = value * value
this._maxClickDrift = value
this._maxClickDrift2 = value * value
}
static #maxClickDrift = 6
private static _maxClickDrift = 6
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
static #maxClickDrift2 = this.#maxClickDrift ** 2
private static _maxClickDrift2 = this._maxClickDrift ** 2
/** Assume that "wheel" events with both deltaX and deltaY less than this value are trackpad gestures. */
static trackpadThreshold = 60
@@ -153,18 +153,18 @@ export class CanvasPointer {
* Therefore, simply setting this value twice will execute the first callback.
*/
get finally() {
return this.#finally
return this._finally
}
set finally(value) {
try {
this.#finally?.()
this._finally?.()
} finally {
this.#finally = value
this._finally = value
}
}
#finally?: () => unknown
private _finally?: () => unknown
constructor(element: Element) {
this.element = element
@@ -197,7 +197,7 @@ export class CanvasPointer {
// Primary button released - treat as pointerup.
if (!(e.buttons & eDown.buttons)) {
this.#completeClick(e)
this._completeClick(e)
this.reset()
return
}
@@ -209,8 +209,8 @@ export class CanvasPointer {
const longerThanBufferTime =
e.timeStamp - eDown.timeStamp > CanvasPointer.bufferTime
if (longerThanBufferTime || !this.#hasSamePosition(e, eDown)) {
this.#setDragStarted(e)
if (longerThanBufferTime || !this._hasSamePosition(e, eDown)) {
this._setDragStarted(e)
}
}
@@ -221,13 +221,13 @@ export class CanvasPointer {
up(e: CanvasPointerEvent): boolean {
if (e.button !== this.eDown?.button) return false
this.#completeClick(e)
this._completeClick(e)
const { dragStarted } = this
this.reset()
return !dragStarted
}
#completeClick(e: CanvasPointerEvent): void {
private _completeClick(e: CanvasPointerEvent): void {
const { eDown } = this
if (!eDown) return
@@ -236,11 +236,11 @@ export class CanvasPointer {
if (this.dragStarted) {
// A move event already started drag
this.onDragEnd?.(e)
} else if (!this.#hasSamePosition(e, eDown)) {
} else if (!this._hasSamePosition(e, eDown)) {
// Teleport without a move event (e.g. tab out, move, tab back)
this.#setDragStarted()
this._setDragStarted()
this.onDragEnd?.(e)
} else if (this.onDoubleClick && this.#isDoubleClick()) {
} else if (this.onDoubleClick && this._isDoubleClick()) {
// Double-click event
this.onDoubleClick(e)
this.eLastDown = undefined
@@ -258,10 +258,10 @@ export class CanvasPointer {
* @param tolerance2 The maximum distance (squared) before the positions are considered different
* @returns `true` if the two events were no more than {@link maxClickDrift} apart, otherwise `false`
*/
#hasSamePosition(
private _hasSamePosition(
a: PointerEvent,
b: PointerEvent,
tolerance2 = CanvasPointer.#maxClickDrift2
tolerance2 = CanvasPointer._maxClickDrift2
): boolean {
const drift = dist2(a.clientX, a.clientY, b.clientX, b.clientY)
return drift <= tolerance2
@@ -271,21 +271,21 @@ export class CanvasPointer {
* Checks whether the pointer is currently past the max click drift threshold.
* @returns `true` if the latest pointer event is past the the click drift threshold
*/
#isDoubleClick(): boolean {
private _isDoubleClick(): boolean {
const { eDown, eLastDown } = this
if (!eDown || !eLastDown) return false
// Use thrice the drift distance for double-click gap
const tolerance2 = (3 * CanvasPointer.#maxClickDrift) ** 2
const tolerance2 = (3 * CanvasPointer._maxClickDrift) ** 2
const diff = eDown.timeStamp - eLastDown.timeStamp
return (
diff > 0 &&
diff < CanvasPointer.doubleClickTime &&
this.#hasSamePosition(eDown, eLastDown, tolerance2)
this._hasSamePosition(eDown, eLastDown, tolerance2)
)
}
#setDragStarted(eMove?: CanvasPointerEvent): void {
private _setDragStarted(eMove?: CanvasPointerEvent): void {
this.dragStarted = true
this.onDragStart?.(this, eMove)
delete this.onDragStart
@@ -303,14 +303,14 @@ export class CanvasPointer {
const timeSinceLastEvent = Math.max(0, now - this.lastWheelEventTime)
this.lastWheelEventTime = now
if (this.#isHighResWheelEvent(e, now)) {
if (this._isHighResWheelEvent(e, now)) {
this.detectedDevice = 'mouse'
} else if (this.#isWithinCooldown(timeSinceLastEvent)) {
if (this.#shouldBufferLinuxEvent(e)) {
this.#bufferLinuxEvent(e, now)
} else if (this._isWithinCooldown(timeSinceLastEvent)) {
if (this._shouldBufferLinuxEvent(e)) {
this._bufferLinuxEvent(e, now)
}
} else {
this.#updateDeviceMode(e, now)
this._updateDeviceMode(e, now)
this.hasReceivedWheelEvent = true
}
@@ -321,7 +321,7 @@ export class CanvasPointer {
* Validates buffered high res wheel events and switches to mouse mode if pattern matches.
* @returns `true` if switched to mouse mode
*/
#isHighResWheelEvent(event: WheelEvent, now: number): boolean {
private _isHighResWheelEvent(event: WheelEvent, now: number): boolean {
if (!this.bufferedLinuxEvent || this.bufferedLinuxEventTime <= 0) {
return false
}
@@ -329,15 +329,15 @@ export class CanvasPointer {
const timeSinceBuffer = now - this.bufferedLinuxEventTime
if (timeSinceBuffer > CanvasPointer.maxHighResBufferTime) {
this.#clearLinuxBuffer()
this._clearLinuxBuffer()
return false
}
if (
event.deltaX === 0 &&
this.#isLinuxWheelPattern(this.bufferedLinuxEvent.deltaY, event.deltaY)
this._isLinuxWheelPattern(this.bufferedLinuxEvent.deltaY, event.deltaY)
) {
this.#clearLinuxBuffer()
this._clearLinuxBuffer()
return true
}
@@ -347,7 +347,7 @@ export class CanvasPointer {
/**
* Checks if we're within the cooldown period where mode switching is disabled.
*/
#isWithinCooldown(timeSinceLastEvent: number): boolean {
private _isWithinCooldown(timeSinceLastEvent: number): boolean {
const isFirstEvent = !this.hasReceivedWheelEvent
const cooldownExpired = timeSinceLastEvent >= CanvasPointer.trackpadMaxGap
return !isFirstEvent && !cooldownExpired
@@ -356,23 +356,23 @@ export class CanvasPointer {
/**
* Updates the device mode based on event patterns.
*/
#updateDeviceMode(event: WheelEvent, now: number): void {
if (this.#isTrackpadPattern(event)) {
private _updateDeviceMode(event: WheelEvent, now: number): void {
if (this._isTrackpadPattern(event)) {
this.detectedDevice = 'trackpad'
} else if (this.#isMousePattern(event)) {
} else if (this._isMousePattern(event)) {
this.detectedDevice = 'mouse'
} else if (
this.detectedDevice === 'trackpad' &&
this.#shouldBufferLinuxEvent(event)
this._shouldBufferLinuxEvent(event)
) {
this.#bufferLinuxEvent(event, now)
this._bufferLinuxEvent(event, now)
}
}
/**
* Clears the buffered Linux wheel event and associated timer.
*/
#clearLinuxBuffer(): void {
private _clearLinuxBuffer(): void {
this.bufferedLinuxEvent = undefined
this.bufferedLinuxEventTime = 0
if (this.linuxBufferTimeoutId !== undefined) {
@@ -385,7 +385,7 @@ export class CanvasPointer {
* Checks if the event matches trackpad input patterns.
* @param event The wheel event to check
*/
#isTrackpadPattern(event: WheelEvent): boolean {
private _isTrackpadPattern(event: WheelEvent): boolean {
// Two-finger panning: non-zero deltaX AND deltaY
if (event.deltaX !== 0 && event.deltaY !== 0) return true
@@ -399,7 +399,7 @@ export class CanvasPointer {
* Checks if the event matches mouse wheel input patterns.
* @param event The wheel event to check
*/
#isMousePattern(event: WheelEvent): boolean {
private _isMousePattern(event: WheelEvent): boolean {
const absoluteDeltaY = Math.abs(event.deltaY)
// Primary threshold for switching from trackpad to mouse
@@ -417,7 +417,7 @@ export class CanvasPointer {
* Checks if the event should be buffered as a potential Linux wheel event.
* @param event The wheel event to check
*/
#shouldBufferLinuxEvent(event: WheelEvent): boolean {
private _shouldBufferLinuxEvent(event: WheelEvent): boolean {
const absoluteDeltaY = Math.abs(event.deltaY)
const isInLinuxRange = absoluteDeltaY >= 10 && absoluteDeltaY < 60
const isVerticalOnly = event.deltaX === 0
@@ -436,7 +436,7 @@ export class CanvasPointer {
* @param event The event to buffer
* @param now The current timestamp
*/
#bufferLinuxEvent(event: WheelEvent, now: number): void {
private _bufferLinuxEvent(event: WheelEvent, now: number): void {
if (this.linuxBufferTimeoutId !== undefined) {
clearTimeout(this.linuxBufferTimeoutId)
}
@@ -446,7 +446,7 @@ export class CanvasPointer {
// Set timeout to clear buffer after 10ms
this.linuxBufferTimeoutId = setTimeout(() => {
this.#clearLinuxBuffer()
this._clearLinuxBuffer()
}, CanvasPointer.maxHighResBufferTime)
}
@@ -455,7 +455,7 @@ export class CanvasPointer {
* @param deltaY1 The first deltaY value
* @param deltaY2 The second deltaY value
*/
#isLinuxWheelPattern(deltaY1: number, deltaY2: number): boolean {
private _isLinuxWheelPattern(deltaY1: number, deltaY2: number): boolean {
const absolute1 = Math.abs(deltaY1)
const absolute2 = Math.abs(deltaY2)

View File

@@ -81,7 +81,7 @@ export class DragAndScale {
* Returns `true` if the current state has changed from the previous state.
* @returns `true` if the current state has changed from the previous state, otherwise `false`.
*/
#stateHasChanged(): boolean {
private _stateHasChanged(): boolean {
const current = this.state
const previous = this.lastState
@@ -95,7 +95,7 @@ export class DragAndScale {
computeVisibleArea(viewport: Rect | undefined): void {
const { scale, offset, visible_area } = this
if (this.#stateHasChanged()) {
if (this._stateHasChanged()) {
this.onChanged?.(scale, offset)
copyState(this.state, this.lastState)
}

View File

@@ -244,7 +244,7 @@ export class LGraph
}
/** Internal only. Not required for serialisation; calculated on deserialise. */
#lastFloatingLinkId: number = 0
private _lastFloatingLinkId: number = 0
private readonly floatingLinksInternal: Map<LinkId, LLink> = new Map()
get floatingLinks(): ReadonlyMap<LinkId, LLink> {
@@ -365,7 +365,7 @@ export class LGraph
this.reroutes.clear()
this.floatingLinksInternal.clear()
this.#lastFloatingLinkId = 0
this._lastFloatingLinkId = 0
// other scene stuff
this._groups = []
@@ -1304,7 +1304,7 @@ export class LGraph
addFloatingLink(link: LLink): LLink {
if (link.id === -1) {
link.id = ++this.#lastFloatingLinkId
link.id = ++this._lastFloatingLinkId
}
this.floatingLinksInternal.set(link.id, link)
@@ -2175,8 +2175,16 @@ export class LGraph
}
}
/**
* Custom JSON serialization to prevent circular reference errors.
* Called automatically by JSON.stringify().
*/
toJSON(): ISerialisedGraph {
return this.serialize()
}
/** @returns The drag and scale state of the first attached canvas, otherwise `undefined`. */
#getDragAndScale(): DragAndScaleState | undefined {
private _getDragAndScale(): DragAndScaleState | undefined {
const ds = this.list_of_graphcanvas?.at(0)?.ds
if (ds) return { scale: ds.scale, offset: ds.offset }
}
@@ -2216,7 +2224,7 @@ export class LGraph
// Save scale and offset
const extra = { ...this.extra }
if (LiteGraph.saveViewportWithGraph) extra.ds = this.#getDragAndScale()
if (LiteGraph.saveViewportWithGraph) extra.ds = this._getDragAndScale()
if (!extra.ds) delete extra.ds
const data: ReturnType<typeof this.asSerialisable> = {
@@ -2406,8 +2414,8 @@ export class LGraph
const floatingLink = LLink.create(linkData)
this.addFloatingLink(floatingLink)
if (floatingLink.id > this.#lastFloatingLinkId)
this.#lastFloatingLinkId = floatingLink.id
if (floatingLink.id > this._lastFloatingLinkId)
this._lastFloatingLinkId = floatingLink.id
}
}

View File

@@ -316,17 +316,17 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
selectionChanged: false
}
#subgraph?: Subgraph
private _subgraph?: Subgraph
get subgraph(): Subgraph | undefined {
return this.#subgraph
return this._subgraph
}
set subgraph(value: Subgraph | undefined) {
if (value !== this.#subgraph) {
this.#subgraph = value
if (value !== this._subgraph) {
this._subgraph = value
if (value)
this.dispatch('litegraph:set-graph', {
oldGraph: this.#subgraph,
oldGraph: this._subgraph,
newGraph: value
})
}
@@ -361,7 +361,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.canvas.dispatchEvent(new CustomEvent(type, { detail }))
}
#updateCursorStyle() {
private _updateCursorStyle() {
if (!this.state.shouldSetCursor) return
const crosshairItems =
@@ -398,7 +398,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
set read_only(value: boolean) {
this.state.readOnly = value
this.#updateCursorStyle()
this._updateCursorStyle()
}
get isDragging(): boolean {
@@ -415,7 +415,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
set hoveringOver(value: CanvasItem) {
this.state.hoveringOver = value
this.#updateCursorStyle()
this._updateCursorStyle()
}
/** @deprecated Replace all references with {@link pointer}.{@link CanvasPointer.isDown isDown}. */
@@ -435,7 +435,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
set dragging_canvas(value: boolean) {
this.state.draggingCanvas = value
this.#updateCursorStyle()
this._updateCursorStyle()
}
/**
@@ -450,16 +450,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
return `normal ${LiteGraph.NODE_SUBTEXT_SIZE}px ${LiteGraph.NODE_FONT}`
}
#maximumFrameGap = 0
private _maximumFrameGap = 0
/** Maximum frames per second to render. 0: unlimited. Default: 0 */
public get maximumFps() {
return this.#maximumFrameGap > Number.EPSILON
? this.#maximumFrameGap / 1000
return this._maximumFrameGap > Number.EPSILON
? this._maximumFrameGap / 1000
: 0
}
public set maximumFps(value) {
this.#maximumFrameGap = value > Number.EPSILON ? 1000 / value : 0
this._maximumFrameGap = value > Number.EPSILON ? 1000 / value : 0
}
/**
@@ -660,12 +660,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* The IDs of the nodes that are currently visible on the canvas. More
* performant than {@link visible_nodes} for visibility checks.
*/
#visible_node_ids: Set<NodeId> = new Set()
private _visible_node_ids: Set<NodeId> = new Set()
node_over?: LGraphNode
node_capturing_input?: LGraphNode | null
highlighted_links: Dictionary<boolean> = {}
#visibleReroutes: Set<Reroute> = new Set()
private _visibleReroutes: Set<Reroute> = new Set()
dirty_canvas: boolean = true
dirty_bgcanvas: boolean = true
@@ -725,9 +725,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
NODEPANEL_IS_OPEN?: boolean
/** Once per frame check of snap to grid value. @todo Update on change. */
#snapToGrid?: number
private _snapToGrid?: number
/** Set on keydown, keyup. @todo */
#shiftDown: boolean = false
private _shiftDown: boolean = false
/** Link rendering adapter for litegraph-to-canvas integration */
linkRenderer: LitegraphLinkAdapter | null = null
@@ -735,7 +735,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
/** If true, enable drag zoom. Ctrl+Shift+Drag Up/Down: zoom canvas. */
dragZoomEnabled: boolean = false
/** The start position of the drag zoom and original read-only state. */
#dragZoomStart: { pos: Point; scale: number; readOnly: boolean } | null = null
private _dragZoomStart: {
pos: Point
scale: number
readOnly: boolean
} | null = null
/** If true, enable live selection during drag. Nodes are selected/deselected in real-time. */
liveSelection: boolean = false
@@ -810,7 +814,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
this.linkConnector.events.addEventListener('link-created', () =>
this.#dirty()
this._dirty()
)
// @deprecated Workaround: Keep until connecting_links is removed.
@@ -1808,7 +1812,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.dragging_canvas = false
this.#dirty()
this._dirty()
this.dirty_area = null
this.node_in_panel = null
@@ -1836,7 +1840,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.linkRenderer = new LitegraphLinkAdapter(false)
this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
this.#dirty()
this._dirty()
}
openSubgraph(subgraph: Subgraph, fromNode: SubgraphNode): void {
@@ -1873,7 +1877,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @returns The canvas element
* @throws If {@link canvas} is an element ID that does not belong to a valid HTML canvas element
*/
#validateCanvas(
private _validateCanvas(
canvas: string | HTMLCanvasElement
): HTMLCanvasElement & { data?: LGraphCanvas } {
if (typeof canvas === 'string') {
@@ -1892,7 +1896,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param skip_events If true, events on the previous canvas will not be removed. Has no effect on the first invocation.
*/
setCanvas(canvas: string | HTMLCanvasElement, skip_events?: boolean) {
const element = this.#validateCanvas(canvas)
const element = this._validateCanvas(canvas)
if (element === this.canvas) return
// maybe detach events from old_canvas
if (!element && this.canvas && !skip_events) this.unbindEvents()
@@ -1905,7 +1909,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// TODO: classList.add
element.className += ' lgraphcanvas'
element.data = this
Object.defineProperty(element, 'data', {
value: this,
writable: true,
configurable: true,
enumerable: false
})
// Background canvas: To render objects behind nodes (background, links, groups)
this.bgcanvas = document.createElement('canvas')
@@ -2026,12 +2035,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
/** Marks the entire canvas as dirty. */
#dirty(): void {
private _dirty(): void {
this.dirty_canvas = true
this.dirty_bgcanvas = true
}
#linkConnectorDrop(): void {
private _linkConnectorDrop(): void {
const { graph, linkConnector, pointer } = this
if (!graph) throw new NullGraphError()
@@ -2070,10 +2079,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const window = this.getCanvasWindow()
if (this.is_rendering) {
if (this.#maximumFrameGap > 0) {
if (this._maximumFrameGap > 0) {
// Manual FPS limit
const gap =
this.#maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time)
this._maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time)
setTimeout(renderFrame.bind(this), Math.max(1, gap))
} else {
// FPS limited by refresh rate
@@ -2161,7 +2170,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
!e.altKey &&
e.buttons
) {
this.#dragZoomStart = {
this._dragZoomStart = {
pos: [e.x, e.y],
scale: this.ds.scale,
readOnly: this.read_only
@@ -2208,9 +2217,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// left button mouse / single finger
if (e.button === 0 && !pointer.isDouble) {
this.#processPrimaryButton(e, node)
this._processPrimaryButton(e, node)
} else if (e.button === 1) {
this.#processMiddleButton(e, node)
this._processMiddleButton(e, node)
} else if (
(e.button === 2 || pointer.isDouble) &&
this.allow_interaction &&
@@ -2246,7 +2255,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
reroute = graph.getRerouteOnPos(
e.canvasX,
e.canvasY,
this.#visibleReroutes
this._visibleReroutes
)
}
if (reroute) {
@@ -2302,18 +2311,24 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param y The y coordinate in canvas space
* @returns The positionable item or undefined
*/
#getPositionableOnPos(x: number, y: number): Positionable | undefined {
private _getPositionableOnPos(
x: number,
y: number
): Positionable | undefined {
const ioNode = this.subgraph?.getIoNodeOnPos(x, y)
if (ioNode) return ioNode
for (const reroute of this.#visibleReroutes) {
for (const reroute of this._visibleReroutes) {
if (reroute.containsPoint([x, y])) return reroute
}
return this.graph?.getGroupTitlebarOnPos(x, y)
}
#processPrimaryButton(e: CanvasPointerEvent, node: LGraphNode | undefined) {
private _processPrimaryButton(
e: CanvasPointerEvent,
node: LGraphNode | undefined
) {
const { pointer, graph, linkConnector, subgraph } = this
if (!graph) throw new NullGraphError()
@@ -2329,7 +2344,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
!e.altKey &&
LiteGraph.leftMouseClickBehavior === 'panning'
) {
this.#setupNodeSelectionDrag(e, pointer, node)
this._setupNodeSelectionDrag(e, pointer, node)
return
}
@@ -2360,16 +2375,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (this.allow_dragnodes) {
pointer.onDragStart = (pointer) => {
this.#startDraggingItems(cloned, pointer)
this._startDraggingItems(cloned, pointer)
}
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
pointer.onDragEnd = (e) => this._processDraggedItems(e)
}
return
}
// Node clicked
if (node && (this.allow_interaction || node.flags.allow_interaction)) {
this.#processNodeClick(e, ctrlOrMeta, node)
this._processNodeClick(e, ctrlOrMeta, node)
} else {
// Subgraph IO nodes
if (subgraph) {
@@ -2387,8 +2402,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
ioNode.onPointerDown(e, pointer, linkConnector)
pointer.onClick ??= () => canvas.processSelect(ioNode, e)
pointer.onDragStart ??= () =>
canvas.#startDraggingItems(ioNode, pointer, true)
pointer.onDragEnd ??= (eUp) => canvas.#processDraggedItems(eUp)
canvas._startDraggingItems(ioNode, pointer, true)
pointer.onDragEnd ??= (eUp) => canvas._processDraggedItems(eUp)
return true
}
}
@@ -2404,7 +2419,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// Fallback to checking visible reroutes directly
for (const reroute of this.#visibleReroutes) {
for (const reroute of this._visibleReroutes) {
const overReroute =
foundReroute === reroute || reroute.containsPoint([x, y])
if (!reroute.isSlotHovered && !overReroute) continue
@@ -2413,19 +2428,19 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
pointer.onClick = () => this.processSelect(reroute, e)
if (!e.shiftKey) {
pointer.onDragStart = (pointer) =>
this.#startDraggingItems(reroute, pointer, true)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
this._startDraggingItems(reroute, pointer, true)
pointer.onDragEnd = (e) => this._processDraggedItems(e)
}
}
if (reroute.isOutputHovered || (overReroute && e.shiftKey)) {
linkConnector.dragFromReroute(graph, reroute)
this.#linkConnectorDrop()
this._linkConnectorDrop()
}
if (reroute.isInputHovered) {
linkConnector.dragFromRerouteToOutput(graph, reroute)
this.#linkConnectorDrop()
this._linkConnectorDrop()
}
reroute.hideSlots()
@@ -2470,14 +2485,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (e.shiftKey && !e.altKey) {
linkConnector.dragFromLinkSegment(graph, linkSegment)
this.#linkConnectorDrop()
this._linkConnectorDrop()
return
} else if (e.altKey && !e.shiftKey) {
const newReroute = graph.createReroute([x, y], linkSegment)
pointer.onDragStart = (pointer) =>
this.#startDraggingItems(newReroute, pointer)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
this._startDraggingItems(newReroute, pointer)
pointer.onDragEnd = (e) => this._processDraggedItems(e)
return
}
} else if (
@@ -2519,7 +2534,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
eMove.canvasY - group.pos[1] - offsetY
]
// Unless snapping.
if (this.#snapToGrid) snapPoint(pos, this.#snapToGrid)
if (this._snapToGrid) snapPoint(pos, this._snapToGrid)
const resized = group.resize(pos[0], pos[1])
if (resized) this.dirty_bgcanvas = true
@@ -2542,9 +2557,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
pointer.onClick = () => this.processSelect(group, e)
pointer.onDragStart = (pointer) => {
group.recomputeInsideNodes()
this.#startDraggingItems(group, pointer, true)
this._startDraggingItems(group, pointer, true)
}
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
pointer.onDragEnd = (e) => this._processDraggedItems(e)
}
}
@@ -2582,12 +2597,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
pointer.finally = () => (this.dragging_canvas = false)
this.dragging_canvas = true
} else {
this.#setupNodeSelectionDrag(e, pointer)
this._setupNodeSelectionDrag(e, pointer)
}
}
}
#setupNodeSelectionDrag(
private _setupNodeSelectionDrag(
e: CanvasPointerEvent,
pointer: CanvasPointer,
node?: LGraphNode | undefined
@@ -2602,7 +2617,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
pointer.onClick = (eUp) => {
// Click, not drag
const clickedItem =
node ?? this.#getPositionableOnPos(eUp.canvasX, eUp.canvasY)
node ?? this._getPositionableOnPos(eUp.canvasX, eUp.canvasY)
this.processSelect(clickedItem, eUp)
}
pointer.onDragStart = () => (this.dragging_rectangle = dragRect)
@@ -2617,7 +2632,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
} else {
// Classic mode: select only when drag ends
pointer.onDragEnd = (upEvent) =>
this.#handleMultiSelect(upEvent, dragRect)
this._handleMultiSelect(upEvent, dragRect)
}
pointer.finally = () => (this.dragging_rectangle = null)
@@ -2629,7 +2644,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param ctrlOrMeta Ctrl or meta key is pressed
* @param node The node to process a click event for
*/
#processNodeClick(
private _processNodeClick(
e: CanvasPointerEvent,
ctrlOrMeta: boolean,
node: LGraphNode
@@ -2685,13 +2700,13 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// Drag multiple output links
if (e.shiftKey && hasRelevantOutputLinks(output, graph)) {
linkConnector.moveOutputLink(graph, output)
this.#linkConnectorDrop()
this._linkConnectorDrop()
return
}
// New output link
linkConnector.dragNewFromOutput(graph, node, output)
this.#linkConnectorDrop()
this._linkConnectorDrop()
if (LiteGraph.shift_click_do_break_link_from) {
if (e.shiftKey) {
@@ -2744,7 +2759,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
linkConnector.dragNewFromInput(graph, node, input)
}
this.#linkConnectorDrop()
this._linkConnectorDrop()
this.dirty_bgcanvas = true
return
@@ -2874,7 +2889,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// Apply snapping to position changes
if (this.#snapToGrid) {
if (this._snapToGrid) {
if (
resizeDirection.includes('N') ||
resizeDirection.includes('W')
@@ -2882,7 +2897,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const originalX = newBounds.x
const originalY = newBounds.y
snapPoint(newBounds.pos, this.#snapToGrid)
snapPoint(newBounds.pos, this._snapToGrid)
// Adjust size to compensate for snapped position
if (resizeDirection.includes('N')) {
@@ -2893,7 +2908,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
}
snapPoint(newBounds.size, this.#snapToGrid)
snapPoint(newBounds.size, this._snapToGrid)
}
// Apply snapping to size changes
@@ -2918,11 +2933,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
node.pos = newBounds.pos
node.setSize(newBounds.size)
this.#dirty()
this._dirty()
}
pointer.onDragEnd = () => {
this.#dirty()
this._dirty()
graph.afterChange(node)
}
pointer.finally = () => {
@@ -2938,8 +2953,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// Drag node
pointer.onDragStart = (pointer) =>
this.#startDraggingItems(node, pointer, true)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
this._startDraggingItems(node, pointer, true)
pointer.onDragEnd = (e) => this._processDraggedItems(e)
}
this.dirty_canvas = true
@@ -3008,7 +3023,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param e The pointerdown event
* @param node The node to process a click event for
*/
#processMiddleButton(e: CanvasPointerEvent, node: LGraphNode | undefined) {
private _processMiddleButton(
e: CanvasPointerEvent,
node: LGraphNode | undefined
) {
const { pointer } = this
if (
@@ -3105,14 +3123,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
}
#processDragZoom(e: PointerEvent): void {
private _processDragZoom(e: PointerEvent): void {
// stop canvas zoom action
if (!e.buttons) {
this.#finishDragZoom()
this._finishDragZoom()
return
}
const start = this.#dragZoomStart
const start = this._dragZoomStart
if (!start) throw new TypeError('Drag-zoom state object was null')
if (!this.graph) throw new NullGraphError()
@@ -3126,10 +3144,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.graph.change()
}
#finishDragZoom(): void {
const start = this.#dragZoomStart
private _finishDragZoom(): void {
const start = this._dragZoomStart
if (!start) return
this.#dragZoomStart = null
this._dragZoomStart = null
this.read_only = start.readOnly
}
@@ -3141,9 +3159,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.dragZoomEnabled &&
e.ctrlKey &&
e.shiftKey &&
this.#dragZoomStart
this._dragZoomStart
) {
this.#processDragZoom(e)
this._processDragZoom(e)
return
}
@@ -3210,7 +3228,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
} else if (this.dragging_canvas) {
this.ds.offset[0] += delta[0] / this.ds.scale
this.ds.offset[1] += delta[1] / this.ds.scale
this.#dirty()
this._dirty()
} else if (
(this.allow_interaction || node?.flags.allow_interaction) &&
!this.read_only
@@ -3258,7 +3276,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.node_over = node
this.dirty_canvas = true
for (const reroute of this.#visibleReroutes) {
for (const reroute of this._visibleReroutes) {
reroute.hideSlots()
this.dirty_bgcanvas = true
}
@@ -3382,10 +3400,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
} else {
// Reroutes
underPointer = this.#updateReroutes(underPointer)
underPointer = this._updateReroutes(underPointer)
// Not over a node
const segment = this.#getLinkCentreOnPos(e)
const segment = this._getLinkCentreOnPos(e)
if (this.over_link_center !== segment) {
underPointer |= CanvasItem.Link
this.over_link_center = segment
@@ -3435,7 +3453,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
}
this.#dirty()
this._dirty()
}
}
@@ -3449,14 +3467,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* Updates the hover / snap state of all visible reroutes.
* @returns The original value of {@link underPointer}, with any found reroute items added.
*/
#updateReroutes(underPointer: CanvasItem): CanvasItem {
private _updateReroutes(underPointer: CanvasItem): CanvasItem {
const { graph, pointer, linkConnector } = this
if (!graph) throw new NullGraphError()
// Update reroute hover state
if (!pointer.isDown) {
let anyChanges = false
for (const reroute of this.#visibleReroutes) {
for (const reroute of this._visibleReroutes) {
anyChanges ||= reroute.updateVisibility(this.graph_mouse)
if (reroute.isSlotHovered) underPointer |= CanvasItem.RerouteSlot
@@ -3464,7 +3482,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (anyChanges) this.dirty_bgcanvas = true
} else if (linkConnector.isConnecting) {
// Highlight the reroute that the mouse is over
for (const reroute of this.#visibleReroutes) {
for (const reroute of this._visibleReroutes) {
if (reroute.containsPoint(this.graph_mouse)) {
if (linkConnector.isRerouteValidDrop(reroute)) {
linkConnector.overReroute = reroute
@@ -3489,7 +3507,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param pointer The pointer event that initiated the drag, e.g. pointerdown
* @param sticky If `true`, the item is added to the selection - see {@link processSelect}
*/
#startDraggingItems(
private _startDraggingItems(
item: Positionable,
pointer: CanvasPointer,
sticky = false
@@ -3511,7 +3529,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* Handles shared clean up and placement after items have been dragged.
* @param e The event that completed the drag, e.g. pointerup, pointermove
*/
#processDraggedItems(e: CanvasPointerEvent): void {
private _processDraggedItems(e: CanvasPointerEvent): void {
const { graph } = this
if (e.shiftKey || LiteGraph.alwaysSnapToGrid)
graph?.snapToGrid(this.selectedItems)
@@ -3533,7 +3551,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const { graph, pointer } = this
if (!graph) return
this.#finishDragZoom()
this._finishDragZoom()
LGraphCanvas.active_canvas = this
@@ -3677,7 +3695,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
return
}
#noItemsSelected(): void {
private _noItemsSelected(): void {
const event = new CustomEvent('litegraph:no-items-selected', {
bubbles: true
})
@@ -3688,7 +3706,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* process a key event
*/
processKey(e: KeyboardEvent): void {
this.#shiftDown = e.shiftKey
this._shiftDown = e.shiftKey
const { graph } = this
if (!graph) return
@@ -3735,7 +3753,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// @ts-expect-error EventTarget.localName is not in standard types
if (e.target.localName != 'input' && e.target.localName != 'textarea') {
if (this.selectedItems.size === 0) {
this.#noItemsSelected()
this._noItemsSelected()
return
}
@@ -4097,7 +4115,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param dragRect The drag rectangle to normalize (modified in place)
* @returns The normalized rectangle
*/
#normalizeDragRect(dragRect: Rect): Rect {
private _normalizeDragRect(dragRect: Rect): Rect {
const w = Math.abs(dragRect[2])
const h = Math.abs(dragRect[3])
if (dragRect[2] < 0) dragRect[0] -= w
@@ -4112,7 +4130,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param rect The rectangle to check against
* @returns Set of positionable items that overlap with the rectangle
*/
#getItemsInRect(rect: Rect): Set<Positionable> {
private _getItemsInRect(rect: Rect): Set<Positionable> {
const { graph, subgraph } = this
if (!graph) throw new NullGraphError()
@@ -4166,9 +4184,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
dragRect[2],
dragRect[3]
]
this.#normalizeDragRect(normalizedRect)
this._normalizeDragRect(normalizedRect)
const itemsInRect = this.#getItemsInRect(normalizedRect)
const itemsInRect = this._getItemsInRect(normalizedRect)
const desired = new Set<Positionable>()
if (e.shiftKey && !e.altKey) {
@@ -4215,16 +4233,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param e The pointer up event
* @param dragRect The drag rectangle
*/
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect): void {
private _handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect): void {
const normalizedRect: Rect = [
dragRect[0],
dragRect[1],
dragRect[2],
dragRect[3]
]
this.#normalizeDragRect(normalizedRect)
this._normalizeDragRect(normalizedRect)
const itemsInRect = this.#getItemsInRect(normalizedRect)
const itemsInRect = this._getItemsInRect(normalizedRect)
const { selectedItems } = this
if (e.shiftKey) {
@@ -4588,7 +4606,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
*/
setZoom(value: number, zooming_center: Point) {
this.ds.changeScale(value, zooming_center)
this.#dirty()
this._dirty()
}
/**
@@ -4671,7 +4689,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @returns `true` if the node is visible, otherwise `false`
*/
isNodeVisible(node: LGraphNode): boolean {
return this.#visible_node_ids.has(node.id)
return this._visible_node_ids.has(node.id)
}
/**
@@ -4692,7 +4710,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (this.dirty_canvas || force_canvas) {
this.computeVisibleNodes(undefined, this.visible_nodes)
// Update visible node IDs
this.#visible_node_ids = new Set(
this._visible_node_ids = new Set(
this.visible_nodes.map((node) => node.id)
)
@@ -4746,8 +4764,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// TODO: Set snapping value when changed instead of once per frame
this.#snapToGrid =
this.#shiftDown || LiteGraph.alwaysSnapToGrid
this._snapToGrid =
this._shiftDown || LiteGraph.alwaysSnapToGrid
? this.graph?.getSnapToGridSize()
: undefined
@@ -4789,7 +4807,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// draw nodes
const { visible_nodes } = this
const drawSnapGuides =
this.#snapToGrid &&
this._snapToGrid &&
(this.isDragging || layoutStore.isDraggingVueNodes.value)
for (const node of visible_nodes) {
@@ -4829,7 +4847,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (linkConnector.isConnecting) {
// current connection (the one being dragged by the mouse)
const { renderLinks } = linkConnector
const highlightPos = this.#getHighlightPosition()
const highlightPos = this._getHighlightPosition()
ctx.lineWidth = this.connections_width
for (const renderLink of renderLinks) {
@@ -4883,7 +4901,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// Gradient half-border over target node
this.#renderSnapHighlight(ctx, highlightPos)
this._renderSnapHighlight(ctx, highlightPos)
}
// on top of link center
@@ -4909,7 +4927,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
/** @returns If the pointer is over a link centre marker, the link segment it belongs to. Otherwise, `undefined`. */
#getLinkCentreOnPos(e: CanvasPointerEvent): LinkSegment | undefined {
private _getLinkCentreOnPos(e: CanvasPointerEvent): LinkSegment | undefined {
// Skip hit detection if center markers are disabled
if (this.linkMarkerShape === LinkMarkerShape.None) {
return undefined
@@ -4928,7 +4946,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
/** Get the target snap / highlight point in graph space */
#getHighlightPosition(): Readonly<Point> {
private _getHighlightPosition(): Readonly<Point> {
return LiteGraph.snaps_for_comfy
? (this.linkConnector.state.snapLinksPos ??
this._highlight_pos ??
@@ -4941,7 +4959,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* Partial border over target node and a highlight over the slot itself.
* @param ctx Canvas 2D context
*/
#renderSnapHighlight(
private _renderSnapHighlight(
ctx: CanvasRenderingContext2D,
highlightPos: Readonly<Point>
): void {
@@ -5592,7 +5610,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// Normalise boundingRect to pos to snap
snapGuide[0] += offsetX
snapGuide[1] += offsetY
if (this.#snapToGrid) snapPoint(snapGuide, this.#snapToGrid)
if (this._snapToGrid) snapPoint(snapGuide, this._snapToGrid)
snapGuide[0] -= offsetX
snapGuide[1] -= offsetY
@@ -5672,7 +5690,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const output = start_node.outputs[outputId]
if (!output) continue
this.#renderAllLinkSegments(
this._renderAllLinkSegments(
ctx,
link,
startPos,
@@ -5701,7 +5719,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
? getSlotPosition(inputNode, link.target_slot, true)
: inputNode.getInputPos(link.target_slot)
this.#renderAllLinkSegments(
this._renderAllLinkSegments(
ctx,
link,
output.pos,
@@ -5728,7 +5746,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
? getSlotPosition(outputNode, link.origin_slot, false)
: outputNode.getOutputPos(link.origin_slot)
this.#renderAllLinkSegments(
this._renderAllLinkSegments(
ctx,
link,
startPos,
@@ -5742,10 +5760,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
if (graph.floatingLinks.size > 0) {
this.#renderFloatingLinks(ctx, graph, visibleReroutes, now)
this._renderFloatingLinks(ctx, graph, visibleReroutes, now)
}
const rerouteSet = this.#visibleReroutes
const rerouteSet = this._visibleReroutes
rerouteSet.clear()
// Render reroutes, ordered by number of non-floating links
@@ -5754,7 +5772,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
rerouteSet.add(reroute)
if (
this.#snapToGrid &&
this._snapToGrid &&
this.isDragging &&
this.selectedItems.has(reroute)
) {
@@ -5776,7 +5794,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
: this.editor_alpha
}
#renderFloatingLinks(
private _renderFloatingLinks(
ctx: CanvasRenderingContext2D,
graph: LGraph,
visibleReroutes: Reroute[],
@@ -5805,7 +5823,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const endDirection = node.inputs[link.target_slot]?.dir
firstReroute._dragging = true
this.#renderAllLinkSegments(
this._renderAllLinkSegments(
ctx,
link,
startPos,
@@ -5827,7 +5845,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const startDirection = node.outputs[link.origin_slot]?.dir
link._dragging = true
this.#renderAllLinkSegments(
this._renderAllLinkSegments(
ctx,
link,
startPos,
@@ -5843,7 +5861,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
ctx.globalAlpha = globalAlpha
}
#renderAllLinkSegments(
private _renderAllLinkSegments(
ctx: CanvasRenderingContext2D,
link: LLink,
startPos: Point,
@@ -6146,7 +6164,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
ctx.save()
ctx.globalAlpha = 0.5 * this.editor_alpha
const drawSnapGuides =
this.#snapToGrid &&
this._snapToGrid &&
(this.isDragging || layoutStore.isDraggingVueNodes.value)
for (const group of groups) {
@@ -6513,7 +6531,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
},
optPass || {}
)
const dirty = () => this.#dirty()
const dirty = () => this._dirty()
const that = this
const { graph } = this
@@ -7522,7 +7540,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
function inner() {
setValue(input?.value)
}
const dirty = () => this.#dirty()
const dirty = () => this._dirty()
function setValue(value: string | number | undefined) {
if (
@@ -8356,7 +8374,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
reroute = this.graph.getRerouteOnPos(
event.canvasX,
event.canvasY,
this.#visibleReroutes
this._visibleReroutes
)
}
if (reroute) {
@@ -8646,4 +8664,17 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const mutations = this.initLayoutMutations()
this.applyNodePositionUpdates(nodesToReposition, mutations)
}
/**
* Custom JSON serialization to prevent circular reference errors.
* LGraphCanvas should not be serialized directly - serialize the graph instead.
*/
toJSON(): { ds: { scale: number; offset: [number, number] } } {
return {
ds: {
scale: this.ds.scale,
offset: [...this.ds.offset] as [number, number]
}
}
}
}

View File

@@ -273,8 +273,8 @@ export class LGraphNode
inputs: INodeInputSlot[] = []
outputs: INodeOutputSlot[] = []
#concreteInputs: NodeInputSlot[] = []
#concreteOutputs: NodeOutputSlot[] = []
private _concreteInputs: NodeInputSlot[] = []
private _concreteOutputs: NodeOutputSlot[] = []
properties: Dictionary<NodeProperty | undefined> = {}
properties_info: INodePropertyInfo[] = []
@@ -438,24 +438,24 @@ export class LGraphNode
}
/** @inheritdoc {@link renderArea} */
#renderArea = new Rectangle()
private _renderArea = new Rectangle()
/**
* Rect describing the node area, including shadows and any protrusions.
* Determines if the node is visible. Calculated once at the start of every frame.
*/
get renderArea(): ReadOnlyRect {
return this.#renderArea
return this._renderArea
}
/** @inheritdoc {@link boundingRect} */
#boundingRect: Rectangle = new Rectangle()
private _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(): ReadOnlyRectangle {
return this.#boundingRect
return this._boundingRect
}
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
@@ -753,7 +753,9 @@ export class LGraphNode
onPropertyChange?(this: LGraphNode): void
updateOutputData?(this: LGraphNode, origin_slot: number): void
#getErrorStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined {
private _getErrorStrokeStyle(
this: LGraphNode
): IDrawBoundingOptions | undefined {
if (this.has_errors) {
return {
padding: 12,
@@ -763,7 +765,9 @@ export class LGraphNode
}
}
#getSelectedStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined {
private _getSelectedStrokeStyle(
this: LGraphNode
): IDrawBoundingOptions | undefined {
if (this.selected) {
return {
padding: this.has_errors ? 20 : undefined
@@ -778,8 +782,8 @@ export class LGraphNode
this.size = [LiteGraph.NODE_WIDTH, 60]
this.pos = [10, 10]
this.strokeStyles = {
error: this.#getErrorStrokeStyle,
selected: this.#getSelectedStrokeStyle
error: this._getErrorStrokeStyle,
selected: this._getSelectedStrokeStyle
}
// Initialize property manager with tracked properties
this.changeTracker = new LGraphNodeProperties(this)
@@ -2067,11 +2071,11 @@ export class LGraphNode
* Called automatically at the start of every frame.
*/
updateArea(ctx?: CanvasRenderingContext2D): void {
const bounds = this.#boundingRect
const bounds = this._boundingRect
this.measure(bounds, ctx)
this.onBounding?.(bounds)
const renderArea = this.#renderArea
const renderArea = this._renderArea
renderArea.set(bounds)
// 4 offset for collapsed node connection points
renderArea[0] -= 4
@@ -2293,7 +2297,7 @@ export class LGraphNode
optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }
): INodeInputSlot | -1
findInputSlotFree(optsIn?: FindFreeSlotOptions) {
return this.#findFreeSlot(this.inputs, optsIn)
return this._findFreeSlot(this.inputs, optsIn)
}
/**
@@ -2308,14 +2312,14 @@ export class LGraphNode
optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }
): INodeOutputSlot | -1
findOutputSlotFree(optsIn?: FindFreeSlotOptions) {
return this.#findFreeSlot(this.outputs, optsIn)
return this._findFreeSlot(this.outputs, optsIn)
}
/**
* Finds the next free slot
* @param slots The slots to search, i.e. this.inputs or this.outputs
*/
#findFreeSlot<TSlot extends INodeInputSlot | INodeOutputSlot>(
private _findFreeSlot<TSlot extends INodeInputSlot | INodeOutputSlot>(
slots: TSlot[],
options?: FindFreeSlotOptions
): TSlot | number {
@@ -2357,7 +2361,7 @@ export class LGraphNode
preferFreeSlot?: boolean,
doNotUseOccupied?: boolean
) {
return this.#findSlotByType(
return this._findSlotByType(
this.inputs,
type,
returnObj,
@@ -2387,7 +2391,7 @@ export class LGraphNode
preferFreeSlot?: boolean,
doNotUseOccupied?: boolean
) {
return this.#findSlotByType(
return this._findSlotByType(
this.outputs,
type,
returnObj,
@@ -2433,14 +2437,14 @@ export class LGraphNode
doNotUseOccupied?: boolean
): number | INodeOutputSlot | INodeInputSlot {
return input
? this.#findSlotByType(
? this._findSlotByType(
this.inputs,
type,
returnObj,
preferFreeSlot,
doNotUseOccupied
)
: this.#findSlotByType(
: this._findSlotByType(
this.outputs,
type,
returnObj,
@@ -2461,7 +2465,7 @@ export class LGraphNode
* @see {findInputSlotByType}
* @returns If a match is found, the slot if returnObj is true, otherwise the index. If no matches are found, -1
*/
#findSlotByType<TSlot extends INodeInputSlot | INodeOutputSlot>(
private _findSlotByType<TSlot extends INodeInputSlot | INodeOutputSlot>(
slots: TSlot[],
type: ISlotType,
returnObj?: boolean,
@@ -3310,8 +3314,8 @@ export class LGraphNode
// default vertical slots
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
const slotIndex = is_input
? this.#defaultVerticalInputs.indexOf(this.inputs[slot_number])
: this.#defaultVerticalOutputs.indexOf(this.outputs[slot_number])
? this._defaultVerticalInputs.indexOf(this.inputs[slot_number])
: this._defaultVerticalOutputs.indexOf(this.outputs[slot_number])
out[0] = is_input ? nodeX + offset : nodeX + this.size[0] + 1 - offset
out[1] =
@@ -3324,7 +3328,7 @@ export class LGraphNode
/**
* @internal The inputs that are not positioned with absolute coordinates.
*/
get #defaultVerticalInputs() {
private get _defaultVerticalInputs() {
return this.inputs.filter(
(slot) => !slot.pos && !(this.widgets?.length && isWidgetInputSlot(slot))
)
@@ -3333,7 +3337,7 @@ export class LGraphNode
/**
* @internal The outputs that are not positioned with absolute coordinates.
*/
get #defaultVerticalOutputs() {
private get _defaultVerticalOutputs() {
return this.outputs.filter((slot: INodeOutputSlot) => !slot.pos)
}
@@ -3341,7 +3345,7 @@ export class LGraphNode
* Get the context needed for slot position calculations
* @internal
*/
#getSlotPositionContext(): SlotPositionContext {
private _getSlotPositionContext(): SlotPositionContext {
return {
nodeX: this.pos[0],
nodeY: this.pos[1],
@@ -3373,7 +3377,7 @@ export class LGraphNode
* @returns Position of the centre of the input slot in graph co-ordinates.
*/
getInputSlotPos(input: INodeInputSlot): Point {
return calculateInputSlotPosFromSlot(this.#getSlotPositionContext(), input)
return calculateInputSlotPosFromSlot(this._getSlotPositionContext(), input)
}
/**
@@ -3916,13 +3920,13 @@ export class LGraphNode
*/
drawCollapsedSlots(ctx: CanvasRenderingContext2D): void {
// Render the first connected slot only.
for (const slot of this.#concreteInputs) {
for (const slot of this._concreteInputs) {
if (slot.link != null) {
slot.drawCollapsed(ctx)
break
}
}
for (const slot of this.#concreteOutputs) {
for (const slot of this._concreteOutputs) {
if (slot.links?.length) {
slot.drawCollapsed(ctx)
break
@@ -3934,7 +3938,7 @@ export class LGraphNode
return [...this.inputs, ...this.outputs]
}
#measureSlot(
private _measureSlot(
slot: NodeInputSlot | NodeOutputSlot,
slotIndex: number,
isInput: boolean
@@ -3951,27 +3955,27 @@ export class LGraphNode
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
}
#measureSlots(): ReadOnlyRect | null {
private _measureSlots(): ReadOnlyRect | null {
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {
for (const [slotIndex, slot] of this._concreteInputs.entries()) {
// Unrecognized nodes (Nodes with error) has inputs but no widgets. Treat
// converted inputs as normal inputs.
/** Widget input slots are handled in {@link layoutWidgetInputSlots} */
if (this.widgets?.length && isWidgetInputSlot(slot)) continue
this.#measureSlot(slot, slotIndex, true)
this._measureSlot(slot, slotIndex, true)
slots.push(slot)
}
for (const [slotIndex, slot] of this.#concreteOutputs.entries()) {
this.#measureSlot(slot, slotIndex, false)
for (const [slotIndex, slot] of this._concreteOutputs.entries()) {
this._measureSlot(slot, slotIndex, false)
slots.push(slot)
}
return slots.length ? createBounds(slots, 0) : null
}
#getMouseOverSlot(slot: INodeSlot): INodeSlot | null {
private _getMouseOverSlot(slot: INodeSlot): INodeSlot | null {
const isInput = isINodeInputSlot(slot)
const mouseOverId = this.mouseOver?.[isInput ? 'inputId' : 'outputId'] ?? -1
if (mouseOverId === -1) {
@@ -3980,11 +3984,11 @@ export class LGraphNode
return isInput ? this.inputs[mouseOverId] : this.outputs[mouseOverId]
}
#isMouseOverSlot(slot: INodeSlot): boolean {
return this.#getMouseOverSlot(slot) === slot
private _isMouseOverSlot(slot: INodeSlot): boolean {
return this._getMouseOverSlot(slot) === slot
}
#isMouseOverWidget(widget: IBaseWidget | undefined): boolean {
private _isMouseOverWidget(widget: IBaseWidget | undefined): boolean {
if (!widget) return false
return this.mouseOver?.overWidget === widget
}
@@ -4016,9 +4020,9 @@ export class LGraphNode
ctx: CanvasRenderingContext2D,
{ fromSlot, colorContext, editorAlpha, lowQuality }: DrawSlotsOptions
) {
for (const slot of [...this.#concreteInputs, ...this.#concreteOutputs]) {
for (const slot of [...this._concreteInputs, ...this._concreteOutputs]) {
const isValidTarget = fromSlot && slot.isValidTarget(fromSlot)
const isMouseOverSlot = this.#isMouseOverSlot(slot)
const isMouseOverSlot = this._isMouseOverSlot(slot)
// change opacity of incompatible slots when dragging a connection
const isValid = !fromSlot || isValidTarget
@@ -4033,7 +4037,7 @@ export class LGraphNode
isMouseOverSlot ||
isValidTarget ||
!slot.isWidgetInputSlot ||
this.#isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
this._isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
slot.isConnected ||
slot.alwaysVisible
) {
@@ -4054,7 +4058,7 @@ export class LGraphNode
* - {@link IBaseWidget.y}
* @param widgetStartY The y-coordinate of the first widget
*/
#arrangeWidgets(widgetStartY: number): void {
private _arrangeWidgets(widgetStartY: number): void {
if (!this.widgets || !this.widgets.length) return
const bodyHeight = this.bodyHeight
@@ -4132,7 +4136,7 @@ export class LGraphNode
/**
* Arranges the layout of the node's widget input slots.
*/
#arrangeWidgetInputSlots(): void {
private _arrangeWidgetInputSlots(): void {
if (!this.widgets) return
const slotByWidgetName = new Map<
@@ -4154,10 +4158,10 @@ export class LGraphNode
const slot = slotByWidgetName.get(widget.name)
if (!slot) continue
const actualSlot = this.#concreteInputs[slot.index]
const actualSlot = this._concreteInputs[slot.index]
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
actualSlot.pos = [offset, widget.y + offset]
this.#measureSlot(actualSlot, slot.index, true)
this._measureSlot(actualSlot, slot.index, true)
}
} else {
// For Vue positioning, just measure the slots without setting pos
@@ -4165,7 +4169,7 @@ export class LGraphNode
const slot = slotByWidgetName.get(widget.name)
if (!slot) continue
this.#measureSlot(this.#concreteInputs[slot.index], slot.index, true)
this._measureSlot(this._concreteInputs[slot.index], slot.index, true)
}
}
}
@@ -4178,10 +4182,10 @@ export class LGraphNode
* have been removed from the ecosystem.
*/
_setConcreteSlots(): void {
this.#concreteInputs = this.inputs.map((slot) =>
this._concreteInputs = this.inputs.map((slot) =>
toClass(NodeInputSlot, slot, this)
)
this.#concreteOutputs = this.outputs.map((slot) =>
this._concreteOutputs = this.outputs.map((slot) =>
toClass(NodeOutputSlot, slot, this)
)
}
@@ -4190,12 +4194,12 @@ export class LGraphNode
* Arranges node elements in preparation for rendering (slots & widgets).
*/
arrange(): void {
const slotsBounds = this.#measureSlots()
const slotsBounds = this._measureSlots()
const widgetStartY = slotsBounds
? slotsBounds[1] + slotsBounds[3] - this.pos[1]
: 0
this.#arrangeWidgets(widgetStartY)
this.#arrangeWidgetInputSlots()
this._arrangeWidgets(widgetStartY)
this._arrangeWidgetInputSlots()
}
/**

View File

@@ -25,24 +25,24 @@ export class LGraphNodeProperties {
node: LGraphNode
/** Set of property paths that have been instrumented */
#instrumentedPaths = new Set<string>()
private _instrumentedPaths = new Set<string>()
constructor(node: LGraphNode) {
this.node = node
this.#setupInstrumentation()
this._setupInstrumentation()
}
/**
* Sets up property instrumentation for all tracked properties
*/
#setupInstrumentation(): void {
private _setupInstrumentation(): void {
for (const path of DEFAULT_TRACKED_PROPERTIES) {
this.#instrumentProperty(path)
this._instrumentProperty(path)
}
}
#resolveTargetObject(parts: string[]): {
private _resolveTargetObject(parts: string[]): {
targetObject: Record<string, unknown>
propertyName: string
} {
@@ -73,14 +73,14 @@ export class LGraphNodeProperties {
/**
* Instruments a single property to track changes
*/
#instrumentProperty(path: string): void {
private _instrumentProperty(path: string): void {
const parts = path.split('.')
if (parts.length > 1) {
this.#ensureNestedPath(path)
this._ensureNestedPath(path)
}
const { targetObject, propertyName } = this.#resolveTargetObject(parts)
const { targetObject, propertyName } = this._resolveTargetObject(parts)
const hasProperty = Object.prototype.hasOwnProperty.call(
targetObject,
@@ -96,7 +96,7 @@ export class LGraphNodeProperties {
set: (newValue: unknown) => {
const oldValue = value
value = newValue
this.#emitPropertyChange(path, oldValue, newValue)
this._emitPropertyChange(path, oldValue, newValue)
// Update enumerable: true for non-undefined values, false for undefined
const shouldBeEnumerable = newValue !== undefined
@@ -121,24 +121,24 @@ export class LGraphNodeProperties {
Object.defineProperty(
targetObject,
propertyName,
this.#createInstrumentedDescriptor(path, currentValue)
this._createInstrumentedDescriptor(path, currentValue)
)
}
this.#instrumentedPaths.add(path)
this._instrumentedPaths.add(path)
}
/**
* Creates a property descriptor that emits change events
*/
#createInstrumentedDescriptor(
private _createInstrumentedDescriptor(
propertyPath: string,
initialValue: unknown
): PropertyDescriptor {
return this.#createInstrumentedDescriptorTyped(propertyPath, initialValue)
return this._createInstrumentedDescriptorTyped(propertyPath, initialValue)
}
#createInstrumentedDescriptorTyped<TValue>(
private _createInstrumentedDescriptorTyped<TValue>(
propertyPath: string,
initialValue: TValue
): PropertyDescriptor {
@@ -149,7 +149,7 @@ export class LGraphNodeProperties {
set: (newValue: TValue) => {
const oldValue = value
value = newValue
this.#emitPropertyChange(propertyPath, oldValue, newValue)
this._emitPropertyChange(propertyPath, oldValue, newValue)
},
enumerable: true,
configurable: true
@@ -159,15 +159,15 @@ export class LGraphNodeProperties {
/**
* Emits a property change event if the node is connected to a graph
*/
#emitPropertyChange(
private _emitPropertyChange(
propertyPath: string,
oldValue: unknown,
newValue: unknown
): void {
this.#emitPropertyChangeTyped(propertyPath, oldValue, newValue)
this._emitPropertyChangeTyped(propertyPath, oldValue, newValue)
}
#emitPropertyChangeTyped<TValue>(
private _emitPropertyChangeTyped<TValue>(
propertyPath: string,
oldValue: TValue,
newValue: TValue
@@ -183,7 +183,7 @@ export class LGraphNodeProperties {
/**
* Ensures parent objects exist for nested properties
*/
#ensureNestedPath(path: string): void {
private _ensureNestedPath(path: string): void {
const parts = path.split('.')
// LGraphNode supports dynamic property access at runtime
let current: Record<string, unknown> = this.node as unknown as Record<
@@ -208,7 +208,7 @@ export class LGraphNodeProperties {
* Checks if a property is being tracked
*/
isTracked(path: string): boolean {
return this.#instrumentedPaths.has(path)
return this._instrumentedPaths.has(path)
}
/**

View File

@@ -1,301 +1,43 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
LGraph {
"_groups": [
LGraphGroup {
"_bounding": Rectangle [
{
"config": {},
"definitions": undefined,
"extra": {
"reroutes": undefined,
},
"floatingLinks": undefined,
"groups": [
{
"bounding": [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": Float64Array [
20,
20,
],
"_size": Float64Array [
1,
3,
],
"color": "#6029aa",
"flags": {},
"font": undefined,
"font_size": 14,
"graph": [Circular],
"id": 123,
"selected": undefined,
"setDirtyCanvas": [Function],
"title": "A group to test with",
},
],
"_input_nodes": undefined,
"_last_trigger_time": undefined,
"_links": Map {},
"_nodes": [
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": Float64Array [
10,
10,
],
"_posSize": Rectangle [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": Float64Array [
140,
60,
],
"action_call": undefined,
"action_triggered": undefined,
"badgePosition": "top-left",
"badges": [],
"bgcolor": undefined,
"block_delete": undefined,
"boxcolor": undefined,
"changeTracker": undefined,
"clip_area": undefined,
"clonable": undefined,
"color": undefined,
"console": undefined,
"exec_version": undefined,
"execute_triggered": undefined,
"flags": {},
"freeWidgetSpace": undefined,
"gotFocusAt": undefined,
"graph": [Circular],
"has_errors": true,
"id": 1,
"ignore_remove": undefined,
"inputs": [],
"last_serialization": {
"id": 1,
},
"locked": undefined,
"lostFocusAt": undefined,
"mode": 0,
"mouseOver": undefined,
"order": 0,
"outputs": [],
"progress": undefined,
"properties": {},
"properties_info": [],
"redraw_on_mouse": undefined,
"removable": undefined,
"resizable": undefined,
"selected": undefined,
"serialize_widgets": undefined,
"showAdvanced": undefined,
"strokeStyles": {
"error": [Function],
"selected": [Function],
},
"title": undefined,
"title_buttons": [],
"type": "",
"widgets": undefined,
"widgets_start_y": undefined,
"widgets_up": undefined,
},
],
"_nodes_by_id": {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": Float64Array [
10,
10,
],
"_posSize": Rectangle [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": Float64Array [
140,
60,
],
"action_call": undefined,
"action_triggered": undefined,
"badgePosition": "top-left",
"badges": [],
"bgcolor": undefined,
"block_delete": undefined,
"boxcolor": undefined,
"changeTracker": undefined,
"clip_area": undefined,
"clonable": undefined,
"color": undefined,
"console": undefined,
"exec_version": undefined,
"execute_triggered": undefined,
"flags": {},
"freeWidgetSpace": undefined,
"gotFocusAt": undefined,
"graph": [Circular],
"has_errors": true,
"id": 1,
"ignore_remove": undefined,
"inputs": [],
"last_serialization": {
"id": 1,
},
"locked": undefined,
"lostFocusAt": undefined,
"mode": 0,
"mouseOver": undefined,
"order": 0,
"outputs": [],
"progress": undefined,
"properties": {},
"properties_info": [],
"redraw_on_mouse": undefined,
"removable": undefined,
"resizable": undefined,
"selected": undefined,
"serialize_widgets": undefined,
"showAdvanced": undefined,
"strokeStyles": {
"error": [Function],
"selected": [Function],
},
"title": undefined,
"title_buttons": [],
"type": "",
"widgets": undefined,
"widgets_start_y": undefined,
"widgets_up": undefined,
},
},
"_nodes_executable": [],
"_nodes_in_order": [
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": Float64Array [
10,
10,
],
"_posSize": Rectangle [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": Float64Array [
140,
60,
],
"action_call": undefined,
"action_triggered": undefined,
"badgePosition": "top-left",
"badges": [],
"bgcolor": undefined,
"block_delete": undefined,
"boxcolor": undefined,
"changeTracker": undefined,
"clip_area": undefined,
"clonable": undefined,
"color": undefined,
"console": undefined,
"exec_version": undefined,
"execute_triggered": undefined,
"flags": {},
"freeWidgetSpace": undefined,
"gotFocusAt": undefined,
"graph": [Circular],
"has_errors": true,
"id": 1,
"ignore_remove": undefined,
"inputs": [],
"last_serialization": {
"id": 1,
},
"locked": undefined,
"lostFocusAt": undefined,
"mode": 0,
"mouseOver": undefined,
"order": 0,
"outputs": [],
"progress": undefined,
"properties": {},
"properties_info": [],
"redraw_on_mouse": undefined,
"removable": undefined,
"resizable": undefined,
"selected": undefined,
"serialize_widgets": undefined,
"showAdvanced": undefined,
"strokeStyles": {
"error": [Function],
"selected": [Function],
},
"title": undefined,
"title_buttons": [],
"type": "",
"widgets": undefined,
"widgets_start_y": undefined,
"widgets_up": undefined,
},
],
"_subgraphs": Map {},
"_version": 3,
"catch_errors": true,
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},
"filter": undefined,
"fixedtime": 0,
"fixedtime_lapse": 0.01,
"floatingLinksInternal": Map {},
"globaltime": 0,
"id": "b4e984f1-b421-4d24-b8b4-ff895793af13",
"iteration": 0,
"last_update_time": 0,
"links": Map {},
"list_of_graphcanvas": null,
"nodes_actioning": [],
"nodes_executedAction": [],
"nodes_executing": [],
"onTrigger": undefined,
"reroutesInternal": Map {},
"last_link_id": 0,
"last_node_id": 1,
"links": [],
"nodes": [
{
"id": 1,
"mode": 0,
"pos": [
10,
10,
],
},
],
"revision": 0,
"runningtime": 0,
"starttime": 0,
"state": {
"lastGroupId": 123,
"lastLinkId": 0,
"lastNodeId": 1,
"lastRerouteId": 0,
},
"status": 1,
"vars": {},
"version": 0.4,
}
`;

View File

@@ -51,11 +51,11 @@ export class InputIndicators implements Disposable {
const element = canvas.canvas
const options = { capture: true, signal } satisfies AddEventListenerOptions
element.addEventListener('pointerdown', this.#onPointerDownOrMove, options)
element.addEventListener('pointermove', this.#onPointerDownOrMove, options)
element.addEventListener('pointerup', this.#onPointerUp, options)
element.addEventListener('keydown', this.#onKeyDownOrUp, options)
document.addEventListener('keyup', this.#onKeyDownOrUp, options)
element.addEventListener('pointerdown', this._onPointerDownOrMove, options)
element.addEventListener('pointermove', this._onPointerDownOrMove, options)
element.addEventListener('pointerup', this._onPointerUp, options)
element.addEventListener('keydown', this._onKeyDownOrUp, options)
document.addEventListener('keyup', this._onKeyDownOrUp, options)
const origDrawFrontCanvas = canvas.drawFrontCanvas.bind(canvas)
signal.addEventListener('abort', () => {
@@ -68,7 +68,7 @@ export class InputIndicators implements Disposable {
}
}
#onPointerDownOrMove = this.onPointerDownOrMove.bind(this)
private _onPointerDownOrMove = this.onPointerDownOrMove.bind(this)
onPointerDownOrMove(e: MouseEvent): void {
this.mouse0Down = (e.buttons & 1) === 1
this.mouse1Down = (e.buttons & 4) === 4
@@ -80,14 +80,14 @@ export class InputIndicators implements Disposable {
this.canvas.setDirty(true)
}
#onPointerUp = this.onPointerUp.bind(this)
private _onPointerUp = this.onPointerUp.bind(this)
onPointerUp(): void {
this.mouse0Down = false
this.mouse1Down = false
this.mouse2Down = false
}
#onKeyDownOrUp = this.onKeyDownOrUp.bind(this)
private _onKeyDownOrUp = this.onKeyDownOrUp.bind(this)
onKeyDownOrUp(e: KeyboardEvent): void {
this.ctrlDown = e.ctrlKey
this.altDown = e.altKey

View File

@@ -115,10 +115,10 @@ export class LinkConnector {
/** The reroute beneath the pointer, if it is a valid connection target. */
overReroute?: Reroute
readonly #setConnectingLinks: (value: ConnectingLink[]) => void
private readonly _setConnectingLinks: (value: ConnectingLink[]) => void
constructor(setConnectingLinks: (value: ConnectingLink[]) => void) {
this.#setConnectingLinks = setConnectingLinks
this._setConnectingLinks = setConnectingLinks
}
get isConnecting() {
@@ -253,7 +253,7 @@ export class LinkConnector {
state.connectingTo = 'input'
state.draggingExistingLinks = true
this.#setLegacyLinks(false)
this._setLegacyLinks(false)
}
/** Drag all links from an output to a new output. */
@@ -364,7 +364,7 @@ export class LinkConnector {
state.multi = true
state.connectingTo = 'output'
this.#setLegacyLinks(true)
this._setLegacyLinks(true)
}
/**
@@ -387,7 +387,7 @@ export class LinkConnector {
state.connectingTo = 'input'
this.#setLegacyLinks(false)
this._setLegacyLinks(false)
}
/**
@@ -410,7 +410,7 @@ export class LinkConnector {
state.connectingTo = 'output'
this.#setLegacyLinks(true)
this._setLegacyLinks(true)
}
dragNewFromSubgraphInput(
@@ -431,7 +431,7 @@ export class LinkConnector {
this.state.connectingTo = 'input'
this.#setLegacyLinks(false)
this._setLegacyLinks(false)
}
dragNewFromSubgraphOutput(
@@ -452,7 +452,7 @@ export class LinkConnector {
this.state.connectingTo = 'output'
this.#setLegacyLinks(true)
this._setLegacyLinks(true)
}
/**
@@ -489,7 +489,7 @@ export class LinkConnector {
this.state.connectingTo = 'input'
this.#setLegacyLinks(false)
this._setLegacyLinks(false)
return
}
@@ -516,7 +516,7 @@ export class LinkConnector {
this.state.connectingTo = 'input'
this.#setLegacyLinks(false)
this._setLegacyLinks(false)
}
/**
@@ -553,7 +553,7 @@ export class LinkConnector {
this.state.connectingTo = 'output'
this.#setLegacyLinks(false)
this._setLegacyLinks(false)
return
}
@@ -581,7 +581,7 @@ export class LinkConnector {
this.state.connectingTo = 'output'
this.#setLegacyLinks(true)
this._setLegacyLinks(true)
}
dragFromLinkSegment(network: LinkNetwork, linkSegment: LinkSegment): void {
@@ -603,7 +603,7 @@ export class LinkConnector {
state.connectingTo = 'input'
this.#setLegacyLinks(false)
this._setLegacyLinks(false)
}
/**
@@ -754,7 +754,7 @@ export class LinkConnector {
const output = node.getOutputOnPos([canvasX, canvasY])
if (output) {
this.#dropOnOutput(node, output)
this._dropOnOutput(node, output)
} else {
this.connectToNode(node, event)
}
@@ -765,7 +765,7 @@ export class LinkConnector {
// Input slot
if (inputOrSocket) {
this.#dropOnInput(node, inputOrSocket)
this._dropOnInput(node, inputOrSocket)
} else {
// Node background / title
this.connectToNode(node, event)
@@ -911,7 +911,7 @@ export class LinkConnector {
return
}
this.#dropOnOutput(node, output)
this._dropOnOutput(node, output)
} else if (connectingTo === 'input') {
// Dropping new input link
const input = node.findInputByType(firstLink.fromSlot.type)?.slot
@@ -922,11 +922,11 @@ export class LinkConnector {
return
}
this.#dropOnInput(node, input)
this._dropOnInput(node, input)
}
}
#dropOnInput(node: LGraphNode, input: INodeInputSlot): void {
private _dropOnInput(node: LGraphNode, input: INodeInputSlot): void {
for (const link of this.renderLinks) {
if (!link.canConnectToInput(node, input)) continue
@@ -934,7 +934,7 @@ export class LinkConnector {
}
}
#dropOnOutput(node: LGraphNode, output: INodeOutputSlot): void {
private _dropOnOutput(node: LGraphNode, output: INodeOutputSlot): void {
for (const link of this.renderLinks) {
if (!link.canConnectToOutput(node, output)) {
if (
@@ -1014,7 +1014,7 @@ export class LinkConnector {
}
/** Sets connecting_links, used by some extensions still. */
#setLegacyLinks(fromSlotIsInput: boolean): void {
private _setLegacyLinks(fromSlotIsInput: boolean): void {
const links = this.renderLinks.map((link) => {
const input = fromSlotIsInput ? (link.fromSlot as INodeInputSlot) : null
const output = fromSlotIsInput ? null : (link.fromSlot as INodeOutputSlot)
@@ -1033,7 +1033,7 @@ export class LinkConnector {
afterRerouteId
} satisfies ConnectingLink
})
this.#setConnectingLinks(links)
this._setConnectingLinks(links)
}
/**

View File

@@ -10,10 +10,10 @@ import type { ReadOnlyRect, Size } from '@/lib/litegraph/src/interfaces'
* - 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
private _width: number = 0
private _height: number = 0
private _desiredWidth: number = 0
private _desiredHeight: number = 0
minWidth: number = 0
minHeight: number = 0
@@ -21,29 +21,29 @@ export class ConstrainedSize {
maxHeight: number = Infinity
get width() {
return this.#width
return this._width
}
get height() {
return this.#height
return this._height
}
get desiredWidth() {
return this.#desiredWidth
return this._desiredWidth
}
set desiredWidth(value: number) {
this.#desiredWidth = value
this.#width = clamp(value, this.minWidth, this.maxWidth)
this._desiredWidth = value
this._width = clamp(value, this.minWidth, this.maxWidth)
}
get desiredHeight() {
return this.#desiredHeight
return this._desiredHeight
}
set desiredHeight(value: number) {
this.#desiredHeight = value
this.#height = clamp(value, this.minHeight, this.maxHeight)
this._desiredHeight = value
this._height = clamp(value, this.minHeight, this.maxHeight)
}
constructor(width: number, height: number) {
@@ -70,6 +70,6 @@ export class ConstrainedSize {
}
toSize(): Size {
return [this.#width, this.#height]
return [this._width, this._height]
}
}

View File

@@ -19,8 +19,8 @@ import { isInRectangle } from '@/lib/litegraph/src/measure'
* - {@link size}: The size of the rectangle.
*/
export class Rectangle extends Float64Array {
#pos: Float64Array<ArrayBuffer> | undefined
#size: Float64Array<ArrayBuffer> | undefined
private _pos: Float64Array<ArrayBuffer> | undefined
private _size: Float64Array<ArrayBuffer> | undefined
constructor(
x: number = 0,
@@ -78,8 +78,8 @@ export class Rectangle extends Float64Array {
* Updating the values of the returned object will update this rectangle.
*/
get pos(): Point {
this.#pos ??= this.subarray(0, 2)
return this.#pos! as unknown as Point
this._pos ??= this.subarray(0, 2)
return this._pos! as unknown as Point
}
set pos(value: Readonly<Point>) {
@@ -93,8 +93,8 @@ export class Rectangle extends Float64Array {
* Updating the values of the returned object will update this rectangle.
*/
get size(): Size {
this.#size ??= this.subarray(2, 4)
return this.#size! as unknown as Size
this._size ??= this.subarray(2, 4)
return this._size! as unknown as Size
}
set size(value: Readonly<Size>) {

View File

@@ -23,15 +23,15 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
return !!this.widget
}
#widget: WeakRef<IBaseWidget> | undefined
private _widgetRef: WeakRef<IBaseWidget> | undefined
/** Internal use only; API is not finalised and may change at any time. */
get _widget(): IBaseWidget | undefined {
return this.#widget?.deref()
return this._widgetRef?.deref()
}
set _widget(widget: IBaseWidget | undefined) {
this.#widget = widget ? new WeakRef(widget) : undefined
this._widgetRef = widget ? new WeakRef(widget) : undefined
}
get collapsedPos(): Readonly<Point> {
@@ -79,4 +79,12 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
ctx.textAlign = textAlign
}
override toJSON(): INodeInputSlot {
return {
...super.toJSON(),
link: this.link,
widget: this.widget
}
}
}

View File

@@ -15,8 +15,6 @@ import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput
import { isSubgraphOutput } from '@/lib/litegraph/src/subgraph/subgraphUtils'
export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
#node: LGraphNode
links: LinkId[] | null
_data?: unknown
slot_index?: number
@@ -27,7 +25,7 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
get collapsedPos(): Readonly<Point> {
return [
this.#node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH,
this._node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH,
LiteGraph.NODE_TITLE_HEIGHT * -0.5
]
}
@@ -40,7 +38,6 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
this.links = slot.links
this._data = slot._data
this.slot_index = slot.slot_index
this.#node = node
}
override isValidTarget(
@@ -78,4 +75,12 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
ctx.textAlign = textAlign
ctx.strokeStyle = strokeStyle
}
override toJSON(): INodeOutputSlot {
return {
...super.toJSON(),
links: this.links,
slot_index: this.slot_index
}
}
}

View File

@@ -37,7 +37,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
pos?: Point
/** The offset from the parent node to the centre point of this slot. */
get #centreOffset(): Readonly<Point> {
private get _centreOffset(): Readonly<Point> {
const nodePos = this.node.pos
const { boundingRect } = this
@@ -55,9 +55,9 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
/** The center point of this slot when the node is collapsed. */
abstract get collapsedPos(): Readonly<Point>
#node: LGraphNode
protected _node: LGraphNode
get node(): LGraphNode {
return this.#node
return this._node
}
get highlightColor(): CanvasColour {
@@ -89,7 +89,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
super(name, type, rectangle)
Object.assign(this, rest)
this.#node = node
this._node = node
}
/**
@@ -126,7 +126,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
? this.highlightColor
: LiteGraph.NODE_TEXT_COLOR
const pos = this.#centreOffset
const pos = this._centreOffset
const slot_type = this.type
const slot_shape = (
slot_type === SlotType.Array ? SlotShape.Grid : this.shape
@@ -260,6 +260,25 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
ctx.lineWidth = originalLineWidth
}
/**
* Custom JSON serialization to prevent circular reference errors.
* Returns only serializable slot properties without the node back-reference.
*/
toJSON(): INodeSlot {
return {
name: this.name,
type: this.type,
label: this.label,
color_on: this.color_on,
color_off: this.color_off,
shape: this.shape,
dir: this.dir,
localized_name: this.localized_name,
pos: this.pos,
boundingRect: [...this.boundingRect] as [number, number, number, number]
}
}
drawCollapsed(ctx: CanvasRenderingContext2D) {
const [x, y] = this.collapsedPos

View File

@@ -0,0 +1,131 @@
import { describe, expect, it } from 'vitest'
import { LGraph, LGraphNode, LiteGraph } from './litegraph'
import type { NodeInputSlot } from './node/NodeInputSlot'
import type { NodeOutputSlot } from './node/NodeOutputSlot'
class TestNode extends LGraphNode {
static override title = 'TestNode'
constructor() {
super('TestNode')
}
}
LiteGraph.registerNodeType('test/TestNode', TestNode)
describe('Serialization - Circular Reference Prevention', () => {
describe('LGraph.toJSON()', () => {
it('should serialize without circular reference errors', () => {
const graph = new LGraph()
expect(() => JSON.stringify(graph)).not.toThrow()
})
it('should return serialize() output from toJSON()', () => {
const graph = new LGraph()
const serialized = graph.serialize()
const jsonOutput = graph.toJSON()
expect(jsonOutput).toEqual(serialized)
})
it('should not include list_of_graphcanvas in JSON output', () => {
const graph = new LGraph()
const json = JSON.stringify(graph)
const parsed = JSON.parse(json)
expect(parsed.list_of_graphcanvas).toBeUndefined()
})
})
describe('NodeSlot.toJSON()', () => {
it('NodeInputSlot should serialize without circular reference errors', () => {
const graph = new LGraph()
const node = LiteGraph.createNode('test/TestNode')!
graph.add(node)
node.addInput('test_input', 'TEST')
const inputSlot = node.inputs[0]
expect(() => JSON.stringify(inputSlot)).not.toThrow()
})
it('NodeOutputSlot should serialize without circular reference errors', () => {
const graph = new LGraph()
const node = LiteGraph.createNode('test/TestNode')!
graph.add(node)
node.addOutput('test_output', 'TEST')
const outputSlot = node.outputs[0]
expect(() => JSON.stringify(outputSlot)).not.toThrow()
})
it('NodeInputSlot.toJSON() should not include _node reference', () => {
const graph = new LGraph()
const node = LiteGraph.createNode('test/TestNode')!
graph.add(node)
node.addInput('test_input', 'TEST')
const inputSlot = node.inputs[0] as NodeInputSlot
const json = inputSlot.toJSON()
expect('_node' in json).toBe(false)
expect('node' in json).toBe(false)
expect(json.name).toBe('test_input')
expect(json.type).toBe('TEST')
})
it('NodeOutputSlot.toJSON() should not include _node reference', () => {
const graph = new LGraph()
const node = LiteGraph.createNode('test/TestNode')!
graph.add(node)
node.addOutput('test_output', 'TEST')
const outputSlot = node.outputs[0] as NodeOutputSlot
const json = outputSlot.toJSON()
expect('_node' in json).toBe(false)
expect('node' in json).toBe(false)
expect(json.name).toBe('test_output')
expect(json.type).toBe('TEST')
})
})
describe('Full graph with nodes - no circular references', () => {
it('should serialize a graph with connected nodes', () => {
const graph = new LGraph()
const node1 = LiteGraph.createNode('test/TestNode')!
const node2 = LiteGraph.createNode('test/TestNode')!
graph.add(node1)
graph.add(node2)
node1.addOutput('out', 'TEST')
node2.addInput('in', 'TEST')
node1.connect(0, node2, 0)
expect(() => JSON.stringify(graph)).not.toThrow()
})
it('should serialize graph.serialize() output without errors', () => {
const graph = new LGraph()
const node1 = LiteGraph.createNode('test/TestNode')!
const node2 = LiteGraph.createNode('test/TestNode')!
graph.add(node1)
graph.add(node2)
node1.addOutput('out', 'TEST')
node2.addInput('in', 'TEST')
node1.connect(0, node2, 0)
const serialized = graph.serialize()
expect(() => JSON.stringify(serialized)).not.toThrow()
expect(() => JSON.parse(JSON.stringify(serialized))).not.toThrow()
})
})
})

View File

@@ -50,7 +50,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
inputs: { linkId: number | null; name: string; type: ISlotType }[]
/** Backing field for {@link id}. */
#id: ExecutionId
private _id: ExecutionId
/**
* The path to the actual node through subgraph instances, represented as a list of all subgraph node IDs (instances),
@@ -62,7 +62,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
* - `3` is the node ID of the actual node in the subgraph definition
*/
get id() {
return this.#id
return this._id
}
get type() {
@@ -106,7 +106,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
if (!node.graph) throw new NullGraphError()
// Set the internal ID of the DTO
this.#id = [...this.subgraphNodePath, this.node.id].join(':')
this._id = [...this.subgraphNodePath, this.node.id].join(':')
this.graph = node.graph
this.inputs = this.node.inputs.map((x) => ({
linkId: x.link,
@@ -265,7 +265,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
// Upstreamed: Bypass nodes are bypassed using the first input with matching type
if (this.mode === LGraphEventMode.BYPASS) {
// Bypass nodes by finding first input with matching type
const matchingIndex = this.#getBypassSlotIndex(slot, type)
const matchingIndex = this._getBypassSlotIndex(slot, type)
// No input types match - bypass not possible
if (matchingIndex === -1) {
@@ -281,7 +281,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
const { node } = this
if (node.isSubgraphNode())
return this.#resolveSubgraphOutput(slot, type, visited)
return this._resolveSubgraphOutput(slot, type, visited)
if (node.isVirtualNode) {
const virtualLink = this.node.getInputLink(slot)
@@ -321,7 +321,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
* @param type The type of the final target input (so type list matches are accurate)
* @returns The index of the input slot on this node, otherwise `-1`.
*/
#getBypassSlotIndex(slot: number, type: ISlotType) {
private _getBypassSlotIndex(slot: number, type: ISlotType) {
const { inputs } = this
const oppositeInput = inputs[slot]
const outputType = this.node.outputs[slot].type
@@ -358,7 +358,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
* @param visited A set of unique IDs to guard against infinite recursion. See {@link resolveInput}.
* @returns A DTO for the node, and the origin ID / slot index of the output.
*/
#resolveSubgraphOutput(
private _resolveSubgraphOutput(
slot: number,
type: ISlotType,
visited: Set<string>

View File

@@ -38,12 +38,12 @@ export abstract class SubgraphIONodeBase<
static minWidth = 100
static roundedRadius = 10
readonly #boundingRect: Rectangle = new Rectangle()
private readonly _boundingRect: Rectangle = new Rectangle()
abstract readonly id: NodeId
get boundingRect(): Rectangle {
return this.#boundingRect
return this._boundingRect
}
selected: boolean = false
@@ -181,7 +181,7 @@ export abstract class SubgraphIONodeBase<
): void {
// Only allow renaming non-empty slots
if (slot !== this.emptySlot) {
this.#promptForSlotRename(slot, event)
this._promptForSlotRename(slot, event)
}
}
@@ -191,14 +191,14 @@ export abstract class SubgraphIONodeBase<
* @param event The event that triggered the context menu.
*/
protected showSlotContextMenu(slot: TSlot, event: CanvasPointerEvent): void {
const options: (IContextMenuValue | null)[] = this.#getSlotMenuOptions(slot)
const options: (IContextMenuValue | null)[] = this._getSlotMenuOptions(slot)
if (!(options.length > 0)) return
new LiteGraph.ContextMenu(options, {
event,
title: slot.name || 'Subgraph Output',
callback: (item: IContextMenuValue) => {
this.#onSlotMenuAction(item, slot, event)
this._onSlotMenuAction(item, slot, event)
}
})
}
@@ -208,7 +208,7 @@ export abstract class SubgraphIONodeBase<
* @param slot The slot to get the context menu options for.
* @returns The context menu options.
*/
#getSlotMenuOptions(slot: TSlot): (IContextMenuValue | null)[] {
private _getSlotMenuOptions(slot: TSlot): (IContextMenuValue | null)[] {
const options: (IContextMenuValue | null)[] = []
// Disconnect option if slot has connections
@@ -239,7 +239,7 @@ export abstract class SubgraphIONodeBase<
* @param slot The slot
* @param event The event that triggered the context menu.
*/
#onSlotMenuAction(
private _onSlotMenuAction(
selectedItem: IContextMenuValue,
slot: TSlot,
event: CanvasPointerEvent
@@ -260,7 +260,7 @@ export abstract class SubgraphIONodeBase<
// Rename the slot
case 'rename':
if (slot !== this.emptySlot) {
this.#promptForSlotRename(slot, event)
this._promptForSlotRename(slot, event)
}
break
}
@@ -273,7 +273,7 @@ export abstract class SubgraphIONodeBase<
* @param slot The slot to rename.
* @param event The event that triggered the rename.
*/
#promptForSlotRename(slot: TSlot, event: CanvasPointerEvent): void {
private _promptForSlotRename(slot: TSlot, event: CanvasPointerEvent): void {
this.subgraph.canvasAction((c) =>
c.prompt(
'Slot name',
@@ -362,7 +362,7 @@ export abstract class SubgraphIONodeBase<
}
configure(data: ExportedSubgraphIONode): void {
this.#boundingRect.set(data.bounding)
this._boundingRect.set(data.bounding)
this.pinned = data.pinned ?? false
}

View File

@@ -63,7 +63,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
override widgets: IBaseWidget[] = []
/** Manages lifecycle of all subgraph event listeners */
#eventAbortController = new AbortController()
private _eventAbortController = new AbortController()
constructor(
/** The (sub)graph that contains this subgraph instance. */
@@ -76,7 +76,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
// Update this node when the subgraph input / output slots are changed
const subgraphEvents = this.subgraph.events
const { signal } = this.#eventAbortController
const { signal } = this._eventAbortController
subgraphEvents.addEventListener(
'input-added',
@@ -89,12 +89,12 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const { inputNode, input } = subgraph.links[linkId].resolve(subgraph)
const widget = inputNode?.widgets?.find?.((w) => w.name == name)
if (widget)
this.#setWidget(subgraphInput, existingInput, widget, input?.widget)
this._setWidget(subgraphInput, existingInput, widget, input?.widget)
return
}
const input = this.addInput(name, type)
this.#addSubgraphInputListeners(subgraphInput, input)
this._addSubgraphInputListeners(subgraphInput, input)
},
{ signal }
)
@@ -179,7 +179,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
}
}
#addSubgraphInputListeners(
private _addSubgraphInputListeners(
subgraphInput: SubgraphInput,
input: INodeInputSlot & Partial<ISubgraphInput>
) {
@@ -201,7 +201,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
if (!widget) return
const widgetLocator = e.detail.input.widget
this.#setWidget(subgraphInput, input, widget, widgetLocator)
this._setWidget(subgraphInput, input, widget, widgetLocator)
},
{ signal }
)
@@ -288,7 +288,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
continue
}
this.#addSubgraphInputListeners(subgraphInput, input)
this._addSubgraphInputListeners(subgraphInput, input)
// Find the first widget that this slot is connected to
for (const linkId of subgraphInput.linkIds) {
@@ -318,13 +318,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const widget = inputNode.getWidgetFromSlot(targetInput)
if (!widget) continue
this.#setWidget(subgraphInput, input, widget, targetInput.widget)
this._setWidget(subgraphInput, input, widget, targetInput.widget)
break
}
}
}
#setWidget(
private _setWidget(
subgraphInput: Readonly<SubgraphInput>,
input: INodeInputSlot,
widget: Readonly<IBaseWidget>,
@@ -553,7 +553,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
override onRemoved(): void {
// Clean up all subgraph event listeners
this.#eventAbortController.abort()
this._eventAbortController.abort()
// Clean up all promoted widgets
for (const widget of this.widgets) {

View File

@@ -57,10 +57,10 @@ export abstract class BaseWidget<
maxWidth?: number
}
#node: LGraphNode
private _node: LGraphNode
/** The node that this widget belongs to. */
get node() {
return this.#node
return this._node
}
linkedWidgets?: IBaseWidget[]
@@ -97,20 +97,20 @@ export abstract class BaseWidget<
canvas: LGraphCanvas
): boolean
#value?: TWidget['value']
private _value?: TWidget['value']
get value(): TWidget['value'] {
return this.#value
return this._value
}
set value(value: TWidget['value']) {
this.#value = value
this._value = value
}
constructor(widget: TWidget & { node: LGraphNode })
constructor(widget: TWidget, node: LGraphNode)
constructor(widget: TWidget & { node: LGraphNode }, node?: LGraphNode) {
// Private fields
this.#node = node ?? widget.node
this._node = node ?? widget.node
// The set and get functions for DOM widget values are hacked on to the options object;
// attempting to set value before options will throw.

View File

@@ -294,7 +294,7 @@ export class PromptExecutionError extends Error {
}
export class ComfyApi extends EventTarget {
#registered = new Set()
private _registered = new Set()
api_host: string
api_base: string
/**
@@ -451,7 +451,7 @@ export class ComfyApi extends EventTarget {
) {
// Type assertion: strictFunctionTypes. So long as we emit events in a type-safe fashion, this is safe.
super.addEventListener(type, callback as EventListener, options)
this.#registered.add(type)
this._registered.add(type)
}
override removeEventListener<TEvent extends keyof ApiEvents>(
@@ -492,7 +492,7 @@ export class ComfyApi extends EventTarget {
/**
* Poll status for colab and other things that don't support websockets.
*/
#pollQueue() {
private _pollQueue() {
setInterval(async () => {
try {
const resp = await this.fetchApi('/prompt')
@@ -568,7 +568,7 @@ export class ComfyApi extends EventTarget {
this.socket.addEventListener('error', () => {
if (this.socket) this.socket.close()
if (!isReconnect && !opened) {
this.#pollQueue()
this._pollQueue()
}
})
@@ -691,7 +691,7 @@ export class ComfyApi extends EventTarget {
)
break
default:
if (this.#registered.has(msg.type)) {
if (this._registered.has(msg.type)) {
// Fallback for custom types - calls super direct.
super.dispatchEvent(
new CustomEvent(msg.type, { detail: msg.data })
@@ -956,7 +956,7 @@ export class ComfyApi extends EventTarget {
* @param {*} type The endpoint to post to
* @param {*} body Optional POST data
*/
async #postItem(type: string, body?: Record<string, unknown>) {
private async _postItem(type: string, body?: Record<string, unknown>) {
try {
await this.fetchApi('/' + type, {
method: 'POST',
@@ -976,7 +976,7 @@ export class ComfyApi extends EventTarget {
* @param {number} id The id of the item to delete
*/
async deleteItem(type: string, id: string) {
await this.#postItem(type, { delete: [id] })
await this._postItem(type, { delete: [id] })
}
/**
@@ -984,7 +984,7 @@ export class ComfyApi extends EventTarget {
* @param {string} type The type of list to clear, queue or history
*/
async clearItems(type: string) {
await this.#postItem(type, { clear: true })
await this._postItem(type, { clear: true })
}
/**
@@ -993,7 +993,7 @@ export class ComfyApi extends EventTarget {
* @param {string | null} [runningPromptId] Optional Running Prompt ID to interrupt
*/
async interrupt(runningPromptId: string | null) {
await this.#postItem(
await this._postItem(
'interrupt',
runningPromptId ? { prompt_id: runningPromptId } : undefined
)

View File

@@ -241,17 +241,17 @@ function dragElement(dragEl): () => void {
}
class ComfyList {
#type
#text
#reverse
private _type
private _text
private _reverse
element: HTMLDivElement
button?: HTMLButtonElement
// @ts-expect-error fixme ts strict error
constructor(text, type?, reverse?) {
this.#text = text
this.#type = type || text.toLowerCase()
this.#reverse = reverse || false
this._text = text
this._type = type || text.toLowerCase()
this._reverse = reverse || false
this.element = $el('div.comfy-list') as HTMLDivElement
this.element.style.display = 'none'
}
@@ -261,7 +261,7 @@ class ComfyList {
}
async load() {
const items = await api.getItems(this.#type)
const items = await api.getItems(this._type)
this.element.replaceChildren(
...Object.keys(items).flatMap((section) => [
$el('h4', {
@@ -269,12 +269,12 @@ class ComfyList {
}),
$el('div.comfy-list-items', [
// @ts-expect-error fixme ts strict error
...(this.#reverse ? items[section].reverse() : items[section]).map(
...(this._reverse ? items[section].reverse() : items[section]).map(
(item: LegacyQueueItem) => {
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
const removeAction = item.remove ?? {
name: 'Delete',
cb: () => api.deleteItem(this.#type, item.prompt[1])
cb: () => api.deleteItem(this._type, item.prompt[1])
}
return $el('div', { textContent: item.prompt[0] + ': ' }, [
$el('button', {
@@ -311,9 +311,9 @@ class ComfyList {
]),
$el('div.comfy-list-actions', [
$el('button', {
textContent: 'Clear ' + this.#text,
textContent: 'Clear ' + this._text,
onclick: async () => {
await api.clearItems(this.#type)
await api.clearItems(this._type)
await this.load()
}
}),
@@ -339,7 +339,7 @@ class ComfyList {
hide() {
this.element.style.display = 'none'
// @ts-expect-error fixme ts strict error
this.button.textContent = 'View ' + this.#text
this.button.textContent = 'View ' + this._text
}
toggle() {

View File

@@ -6,7 +6,7 @@ type DialogAction<T> = string | { value?: T; text: string }
export class ComfyAsyncDialog<
T = string | null
> extends ComfyDialog<HTMLDialogElement> {
#resolve: (value: T | null) => void = () => {}
private _resolve: (value: T | null) => void = () => {}
constructor(actions?: Array<DialogAction<T>>) {
super(
@@ -30,7 +30,7 @@ export class ComfyAsyncDialog<
super.show(html)
return new Promise((resolve) => {
this.#resolve = resolve
this._resolve = resolve
})
}
@@ -43,12 +43,12 @@ export class ComfyAsyncDialog<
this.element.showModal()
return new Promise((resolve) => {
this.#resolve = resolve
this._resolve = resolve
})
}
override close(result: T | null = null) {
this.#resolve(result)
this._resolve(result)
this.element.close()
super.close()
}

View File

@@ -21,8 +21,8 @@ type ComfyButtonProps = {
}
export class ComfyButton implements ComfyComponent<HTMLElement> {
#over = 0
#popupOpen = false
private _over = 0
private _popupOpen = false
isOver = false
iconElement = $el('i.mdi')
contentElement = $el('span')
@@ -123,7 +123,7 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
this.element.addEventListener('click', (e) => {
if (this.popup) {
// we are either a touch device or triggered by click not hover
if (!this.#over) {
if (!this._over) {
this.popup.toggle()
}
}
@@ -157,7 +157,7 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
internalClasses.push('disabled')
}
if (this.popup) {
if (this.#popupOpen) {
if (this._popupOpen) {
internalClasses.push('popup-open')
} else {
internalClasses.push('popup-closed')
@@ -172,16 +172,16 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
if (mode === 'hover') {
for (const el of [this.element, this.popup.element]) {
el.addEventListener('mouseenter', () => {
this.popup.open = !!++this.#over
this.popup.open = !!++this._over
})
el.addEventListener('mouseleave', () => {
this.popup.open = !!--this.#over
this.popup.open = !!--this._over
})
}
}
popup.addEventListener('change', () => {
this.#popupOpen = popup.open
this._popupOpen = popup.open
this.updateClasses()
})

View File

@@ -54,9 +54,9 @@ export class ComfyPopup extends EventTarget {
this.open = prop(this, 'open', false, (v, o) => {
if (v === o) return
if (v) {
this.#show()
this._show()
} else {
this.#hide()
this._hide()
}
})
}
@@ -65,24 +65,24 @@ export class ComfyPopup extends EventTarget {
this.open = !this.open
}
#hide() {
private _hide() {
this.element.classList.remove('open')
window.removeEventListener('resize', this.update)
window.removeEventListener('click', this.#clickHandler, { capture: true })
window.removeEventListener('keydown', this.#escHandler, { capture: true })
window.removeEventListener('click', this._clickHandler, { capture: true })
window.removeEventListener('keydown', this._escHandler, { capture: true })
this.dispatchEvent(new CustomEvent('close'))
this.dispatchEvent(new CustomEvent('change'))
}
#show() {
private _show() {
this.element.classList.add('open')
this.update()
window.addEventListener('resize', this.update)
window.addEventListener('click', this.#clickHandler, { capture: true })
window.addEventListener('click', this._clickHandler, { capture: true })
if (this.closeOnEscape) {
window.addEventListener('keydown', this.#escHandler, { capture: true })
window.addEventListener('keydown', this._escHandler, { capture: true })
}
this.dispatchEvent(new CustomEvent('open'))
@@ -90,7 +90,7 @@ export class ComfyPopup extends EventTarget {
}
// @ts-expect-error fixme ts strict error
#escHandler = (e) => {
private _escHandler = (e) => {
if (e.key === 'Escape') {
this.open = false
e.preventDefault()
@@ -99,7 +99,7 @@ export class ComfyPopup extends EventTarget {
}
// @ts-expect-error fixme ts strict error
#clickHandler = (e) => {
private _clickHandler = (e) => {
/** @type {any} */
const target = e.target
if (

View File

@@ -5,11 +5,11 @@ export class ComfyDialog<
> extends EventTarget {
element: T
textElement!: HTMLElement
#buttons: HTMLButtonElement[] | null
private _buttons: HTMLButtonElement[] | null
constructor(type = 'div', buttons: HTMLButtonElement[] | null = null) {
super()
this.#buttons = buttons
this._buttons = buttons
this.element = $el(type + '.comfy-modal', { parent: document.body }, [
$el('div.comfy-modal-content', [
$el('p', { $: (p) => (this.textElement = p) }),
@@ -20,7 +20,7 @@ export class ComfyDialog<
createButtons() {
return (
this.#buttons ?? [
this._buttons ?? [
$el('button', {
type: 'button',
textContent: 'Close',

View File

@@ -54,7 +54,7 @@ class ConversionContext {
/** Reroutes that has at least a valid link pass through it */
validReroutes: Set<Reroute>
#rerouteIdCounter = 0
private _rerouteIdCounter = 0
constructor(public workflow: WorkflowJSON04) {
this.nodeById = _.keyBy(workflow.nodes.map(_.cloneDeep), 'id')
@@ -76,7 +76,7 @@ class ConversionContext {
pos: getNodeCenter(node),
linkIds: []
}))
this.#rerouteIdCounter = reroutes.length + 1
this._rerouteIdCounter = reroutes.length + 1
this.rerouteByNodeId = _.keyBy(reroutes, 'nodeId')
this.rerouteById = _.keyBy(reroutes, 'id')
@@ -88,7 +88,7 @@ class ConversionContext {
/**
* Gets the chain of reroute nodes leading to the given node
*/
#getRerouteChain(node: RerouteNode): RerouteNode[] {
private _getRerouteChain(node: RerouteNode): RerouteNode[] {
const nodes: RerouteNode[] = []
let currentNode: RerouteNode = node
while (currentNode?.type === 'Reroute') {
@@ -106,7 +106,7 @@ class ConversionContext {
return nodes
}
#connectRerouteChain(rerouteNodes: RerouteNode[]): Reroute[] {
private _connectRerouteChain(rerouteNodes: RerouteNode[]): Reroute[] {
const reroutes = rerouteNodes.map((node) => this.rerouteByNodeId[node.id])
for (const reroute of reroutes) {
this.validReroutes.add(reroute)
@@ -121,7 +121,7 @@ class ConversionContext {
return reroutes
}
#createNewLink(
private _createNewLink(
startingLink: ComfyLinkObject,
endingLink: ComfyLinkObject,
rerouteNodes: RerouteNode[]
@@ -136,7 +136,7 @@ class ConversionContext {
parentId: reroute.id
})
const reroutes = this.#connectRerouteChain(rerouteNodes)
const reroutes = this._connectRerouteChain(rerouteNodes)
for (const reroute of reroutes) {
reroute.linkIds ??= []
reroute.linkIds.push(endingLink.id)
@@ -153,11 +153,11 @@ class ConversionContext {
}
}
#createNewInputFloatingLink(
private _createNewInputFloatingLink(
endingLink: ComfyLinkObject,
rerouteNodes: RerouteNode[]
): ComfyLinkObject {
const reroutes = this.#connectRerouteChain(rerouteNodes)
const reroutes = this._connectRerouteChain(rerouteNodes)
for (const reroute of reroutes) {
if (!reroute.linkIds?.length) {
reroute.floating = {
@@ -166,7 +166,7 @@ class ConversionContext {
}
}
return {
id: this.#rerouteIdCounter++,
id: this._rerouteIdCounter++,
origin_id: -1,
origin_slot: -1,
target_id: endingLink.target_id,
@@ -176,11 +176,11 @@ class ConversionContext {
}
}
#createNewOutputFloatingLink(
private _createNewOutputFloatingLink(
startingLink: ComfyLinkObject,
rerouteNodes: RerouteNode[]
): ComfyLinkObject {
const reroutes = this.#connectRerouteChain(rerouteNodes)
const reroutes = this._connectRerouteChain(rerouteNodes)
for (const reroute of reroutes) {
if (!reroute.linkIds?.length) {
reroute.floating = {
@@ -190,7 +190,7 @@ class ConversionContext {
}
return {
id: this.#rerouteIdCounter++,
id: this._rerouteIdCounter++,
origin_id: startingLink.origin_id,
origin_slot: startingLink.origin_slot,
target_id: -1,
@@ -200,7 +200,7 @@ class ConversionContext {
}
}
#reconnectLinks(nodes: ComfyNode[], links: ComfyLinkObject[]): void {
private _reconnectLinks(nodes: ComfyNode[], links: ComfyLinkObject[]): void {
// Remove all existing links on sockets
for (const node of nodes) {
for (const input of node.inputs ?? []) {
@@ -245,18 +245,18 @@ class ConversionContext {
const endingRerouteNode = this.nodeById[
endingLink.origin_id
] as RerouteNode
const rerouteNodes = this.#getRerouteChain(endingRerouteNode)
const rerouteNodes = this._getRerouteChain(endingRerouteNode)
const startingLink =
this.linkById[
rerouteNodes[rerouteNodes.length - 1]?.inputs?.[0]?.link ?? -1
]
if (startingLink) {
// Valid link found, create a new link
links.push(this.#createNewLink(startingLink, endingLink, rerouteNodes))
links.push(this._createNewLink(startingLink, endingLink, rerouteNodes))
} else {
// Floating link found, create a new floating link
floatingLinks.push(
this.#createNewInputFloatingLink(endingLink, rerouteNodes)
this._createNewInputFloatingLink(endingLink, rerouteNodes)
)
}
}
@@ -270,14 +270,14 @@ class ConversionContext {
})
for (const rerouteNode of floatingEndingRerouteNodes) {
const rerouteNodes = this.#getRerouteChain(rerouteNode)
const rerouteNodes = this._getRerouteChain(rerouteNode)
const startingLink =
this.linkById[
rerouteNodes[rerouteNodes.length - 1]?.inputs?.[0]?.link ?? -1
]
if (startingLink) {
floatingLinks.push(
this.#createNewOutputFloatingLink(startingLink, rerouteNodes)
this._createNewOutputFloatingLink(startingLink, rerouteNodes)
)
}
}
@@ -285,7 +285,7 @@ class ConversionContext {
const nodes = Object.values(this.nodeById).filter(
(node) => node.type !== 'Reroute'
)
this.#reconnectLinks(nodes, links)
this._reconnectLinks(nodes, links)
return {
...this.workflow,