diff --git a/src/lib/litegraph/src/CanvasPointer.ts b/src/lib/litegraph/src/CanvasPointer.ts index eba3a02003..0e1af314d1 100644 --- a/src/lib/litegraph/src/CanvasPointer.ts +++ b/src/lib/litegraph/src/CanvasPointer.ts @@ -1,7 +1,6 @@ -import type { CompassCorners } from "./interfaces" -import type { CanvasPointerEvent } from "./types/events" - -import { dist2 } from "./measure" +import type { CompassCorners } from './interfaces' +import { dist2 } from './measure' +import type { CanvasPointerEvent } from './types/events' /** * Allows click and drag actions to be declared ahead of time during a pointerdown event. @@ -174,7 +173,8 @@ export class CanvasPointer { // Dragging, but no callback to run if (this.dragStarted) return - const longerThanBufferTime = e.timeStamp - eDown.timeStamp > CanvasPointer.bufferTime + const longerThanBufferTime = + e.timeStamp - eDown.timeStamp > CanvasPointer.bufferTime if (longerThanBufferTime || !this.#hasSamePosition(e, eDown)) { this.#setDragStarted(e) } @@ -227,7 +227,7 @@ export class CanvasPointer { #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 @@ -244,9 +244,11 @@ export class CanvasPointer { // Use thrice the drift distance for double-click gap const tolerance2 = (3 * CanvasPointer.#maxClickDrift) ** 2 const diff = eDown.timeStamp - eLastDown.timeStamp - return diff > 0 && + return ( + diff > 0 && diff < CanvasPointer.doubleClickTime && this.#hasSamePosition(eDown, eLastDown, tolerance2) + ) } #setDragStarted(eMove?: CanvasPointerEvent): void { @@ -283,7 +285,7 @@ export class CanvasPointer { const { element, pointerId } = this this.pointerId = undefined - if (typeof pointerId === "number" && element.hasPointerCapture(pointerId)) { + if (typeof pointerId === 'number' && element.hasPointerCapture(pointerId)) { element.releasePointerCapture(pointerId) } } diff --git a/src/lib/litegraph/src/ContextMenu.ts b/src/lib/litegraph/src/ContextMenu.ts index b6b7b55f13..7e6647dc39 100644 --- a/src/lib/litegraph/src/ContextMenu.ts +++ b/src/lib/litegraph/src/ContextMenu.ts @@ -1,10 +1,15 @@ -import type { ContextMenuDivElement, IContextMenuOptions, IContextMenuValue } from "./interfaces" - -import { LiteGraph } from "./litegraph" +import type { + ContextMenuDivElement, + IContextMenuOptions, + IContextMenuValue +} from './interfaces' +import { LiteGraph } from './litegraph' // TODO: Replace this pattern with something more modern. export interface ContextMenu { - constructor: new (...args: ConstructorParameters>) => ContextMenu + constructor: new ( + ...args: ConstructorParameters> + ) => ContextMenu } /** @@ -30,7 +35,10 @@ export class ContextMenu { * - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback * - event: you can pass a MouseEvent, this way the ContextMenu appears in that position */ - constructor(values: readonly (string | IContextMenuValue | null)[], options: IContextMenuOptions) { + constructor( + values: readonly (string | IContextMenuValue | null)[], + options: IContextMenuOptions + ) { options ||= {} this.options = options @@ -38,79 +46,83 @@ export class ContextMenu { const parent = options.parentMenu if (parent) { if (!(parent instanceof ContextMenu)) { - console.error("parentMenu must be of class ContextMenu, ignoring it") + console.error('parentMenu must be of class ContextMenu, ignoring it') options.parentMenu = undefined } else { this.parentMenu = parent this.parentMenu.lock = true this.parentMenu.current_submenu = this } - if (parent.options?.className === "dark") { - options.className = "dark" + if (parent.options?.className === 'dark') { + options.className = 'dark' } } // use strings because comparing classes between windows doesnt work - const eventClass = options.event - ? options.event.constructor.name - : null + const eventClass = options.event ? options.event.constructor.name : null if ( - eventClass !== "MouseEvent" && - eventClass !== "CustomEvent" && - eventClass !== "PointerEvent" + eventClass !== 'MouseEvent' && + eventClass !== 'CustomEvent' && + eventClass !== 'PointerEvent' ) { - console.error(`Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. (${eventClass})`) + console.error( + `Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. (${eventClass})` + ) options.event = undefined } - const root: ContextMenuDivElement = document.createElement("div") - let classes = "litegraph litecontextmenu litemenubar-panel" + const root: ContextMenuDivElement = document.createElement('div') + let classes = 'litegraph litecontextmenu litemenubar-panel' if (options.className) classes += ` ${options.className}` root.className = classes - root.style.minWidth = "100" - root.style.minHeight = "100" + root.style.minWidth = '100' + root.style.minHeight = '100' // Close the context menu when a click occurs outside this context menu or its submenus const { signal } = this.controller const eventOptions = { capture: true, signal } if (!this.parentMenu) { - document.addEventListener("pointerdown", (e) => { - if (e.target instanceof Node && !this.containsNode(e.target)) { - this.close() - } - }, eventOptions) + document.addEventListener( + 'pointerdown', + (e) => { + if (e.target instanceof Node && !this.containsNode(e.target)) { + this.close() + } + }, + eventOptions + ) } // this prevents the default context browser menu to open in case this menu was created when pressing right button - root.addEventListener("pointerup", e => e.preventDefault(), eventOptions) + root.addEventListener('pointerup', (e) => e.preventDefault(), eventOptions) // Right button root.addEventListener( - "contextmenu", + 'contextmenu', (e) => { if (e.button === 2) e.preventDefault() }, - eventOptions, + eventOptions ) root.addEventListener( - "pointerdown", + 'pointerdown', (e) => { if (e.button == 2) { this.close() e.preventDefault() } }, - eventOptions, + eventOptions ) this.root = root // title if (options.title) { - const element = document.createElement("div") - element.className = "litemenu-title" + const element = document.createElement('div') + element.className = 'litemenu-title' element.innerHTML = options.title root.append(element) } @@ -120,23 +132,26 @@ export class ContextMenu { const value = values[i] let name = Array.isArray(values) ? value : String(i) - if (typeof name !== "string") { - name = name != null - ? (name.content === undefined ? String(name) : name.content) - : name + if (typeof name !== 'string') { + name = + name != null + ? name.content === undefined + ? String(name) + : name.content + : name } this.addItem(name, value, options) } // insert before checking position - const ownerDocument = (options.event?.target as Node | null | undefined)?.ownerDocument + const ownerDocument = (options.event?.target as Node | null | undefined) + ?.ownerDocument const root_document = ownerDocument || document if (root_document.fullscreenElement) root_document.fullscreenElement.append(root) - else - root_document.body.append(root) + else root_document.body.append(root) // compute best position let left = options.left || 0 @@ -154,7 +169,9 @@ export class ContextMenu { const body_rect = document.body.getBoundingClientRect() const root_rect = root.getBoundingClientRect() if (body_rect.height == 0) - console.error("document.body height is 0. That is dangerous, set html,body { height: 100%; }") + console.error( + 'document.body height is 0. That is dangerous, set html,body { height: 100%; }' + ) if (body_rect.width && left > body_rect.width - root_rect.width - 10) left = body_rect.width - root_rect.width - 10 @@ -180,66 +197,71 @@ export class ContextMenu { if (visited.has(this)) return false visited.add(this) - return this.current_submenu?.containsNode(node, visited) || this.root.contains(node) + return ( + this.current_submenu?.containsNode(node, visited) || + this.root.contains(node) + ) } addItem( name: string | null, value: string | IContextMenuValue | null, - options: IContextMenuOptions, + options: IContextMenuOptions ): HTMLElement { options ||= {} - const element: ContextMenuDivElement = document.createElement("div") - element.className = "litemenu-entry submenu" + const element: ContextMenuDivElement = document.createElement('div') + element.className = 'litemenu-entry submenu' let disabled = false if (value === null) { - element.classList.add("separator") + element.classList.add('separator') } else { - const innerHtml = name === null ? "" : String(name) - if (typeof value === "string") { + const innerHtml = name === null ? '' : String(name) + if (typeof value === 'string') { element.innerHTML = innerHtml } else { element.innerHTML = value?.title ?? innerHtml if (value.disabled) { disabled = true - element.classList.add("disabled") - element.setAttribute("aria-disabled", "true") + element.classList.add('disabled') + element.setAttribute('aria-disabled', 'true') } if (value.submenu || value.has_submenu) { - element.classList.add("has_submenu") - element.setAttribute("aria-haspopup", "true") - element.setAttribute("aria-expanded", "false") + element.classList.add('has_submenu') + element.setAttribute('aria-haspopup', 'true') + element.setAttribute('aria-expanded', 'false') } if (value.className) element.className += ` ${value.className}` } element.value = value - element.setAttribute("role", "menuitem") + element.setAttribute('role', 'menuitem') - if (typeof value === "function") { - element.dataset["value"] = String(name) + if (typeof value === 'function') { + element.dataset['value'] = String(name) element.onclick_callback = value } else { - element.dataset["value"] = String(value) + element.dataset['value'] = String(value) } } this.root.append(element) - if (!disabled) element.addEventListener("click", inner_onclick) + if (!disabled) element.addEventListener('click', inner_onclick) if (!disabled && options.autoopen) - element.addEventListener("pointerenter", inner_over) + element.addEventListener('pointerenter', inner_over) const setAriaExpanded = () => { - const entries = this.root.querySelectorAll("div.litemenu-entry.has_submenu") + const entries = this.root.querySelectorAll( + 'div.litemenu-entry.has_submenu' + ) if (entries) { for (const entry of entries) { - entry.setAttribute("aria-expanded", "false") + entry.setAttribute('aria-expanded', 'false') } } - element.setAttribute("aria-expanded", "true") + element.setAttribute('aria-expanded', 'true') } function inner_over(this: ContextMenuDivElement, e: MouseEvent) { @@ -273,13 +295,13 @@ export class ContextMenu { options, e, that, - options.node, + options.node ) if (r === true) close_parent = false } // special cases - if (typeof value === "object") { + if (typeof value === 'object') { if ( value.callback && !options.ignore_item_callbacks && @@ -292,12 +314,12 @@ export class ContextMenu { options, e, that, - options.extra, + options.extra ) if (r === true) close_parent = false } if (value.submenu) { - if (!value.submenu.options) throw "ContextMenu submenu needs options" + if (!value.submenu.options) throw 'ContextMenu submenu needs options' new that.constructor(value.submenu.options, { callback: value.submenu.callback, @@ -306,7 +328,7 @@ export class ContextMenu { ignore_item_callbacks: value.submenu.ignore_item_callbacks, title: value.submenu.title, extra: value.submenu.extra, - autoopen: options.autoopen, + autoopen: options.autoopen }) close_parent = false } @@ -326,11 +348,14 @@ export class ContextMenu { this.parentMenu.current_submenu = undefined if (e === undefined) { this.parentMenu.close() - } else if (e && !ContextMenu.isCursorOverElement(e, this.parentMenu.root)) { + } else if ( + e && + !ContextMenu.isCursorOverElement(e, this.parentMenu.root) + ) { ContextMenu.trigger( this.parentMenu.root, `${LiteGraph.pointerevents_method}leave`, - e, + e ) } } @@ -342,9 +367,9 @@ export class ContextMenu { static trigger( element: HTMLDivElement, event_name: string, - params: MouseEvent, + params: MouseEvent ): CustomEvent { - const evt = document.createEvent("CustomEvent") + const evt = document.createEvent('CustomEvent') evt.initCustomEvent(event_name, true, true, params) if (element.dispatchEvent) element.dispatchEvent(evt) // else nothing seems bound here so nothing to do @@ -353,9 +378,7 @@ export class ContextMenu { // returns the top most menu getTopMenu(): ContextMenu { - return this.options.parentMenu - ? this.options.parentMenu.getTopMenu() - : this + return this.options.parentMenu ? this.options.parentMenu.getTopMenu() : this } getFirstEvent(): MouseEvent | undefined { @@ -367,7 +390,7 @@ export class ContextMenu { /** @deprecated Unused. */ static isCursorOverElement( event: MouseEvent, - element: HTMLDivElement, + element: HTMLDivElement ): boolean { const left = event.clientX const top = event.clientY diff --git a/src/lib/litegraph/src/CurveEditor.ts b/src/lib/litegraph/src/CurveEditor.ts index 81a86efc6c..ea65f56695 100644 --- a/src/lib/litegraph/src/CurveEditor.ts +++ b/src/lib/litegraph/src/CurveEditor.ts @@ -1,7 +1,6 @@ -import type { Point, Rect } from "./interfaces" - -import { clamp, LGraphCanvas } from "./litegraph" -import { distance } from "./measure" +import type { Point, Rect } from './interfaces' +import { LGraphCanvas, clamp } from './litegraph' +import { distance } from './measure' // used by some widgets to render a curve editor @@ -48,7 +47,7 @@ export class CurveEditor { graphcanvas?: LGraphCanvas, background_color?: string, line_color?: string, - inactive = false, + inactive = false ): void { const points = this.points if (!points) return @@ -57,17 +56,17 @@ export class CurveEditor { const w = size[0] - this.margin * 2 const h = size[1] - this.margin * 2 - line_color = line_color || "#666" + line_color = line_color || '#666' ctx.save() ctx.translate(this.margin, this.margin) if (background_color) { - ctx.fillStyle = "#111" + ctx.fillStyle = '#111' ctx.fillRect(0, 0, w, h) - ctx.fillStyle = "#222" + ctx.fillStyle = '#222' ctx.fillRect(w * 0.5, 0, 1, h) - ctx.strokeStyle = "#333" + ctx.strokeStyle = '#333' ctx.strokeRect(0, 0, w, h) } ctx.strokeStyle = line_color @@ -80,9 +79,8 @@ export class CurveEditor { ctx.globalAlpha = 1 if (!inactive) { for (const [i, p] of points.entries()) { - ctx.fillStyle = this.selected == i - ? "#FFF" - : (this.nearest == i ? "#DDD" : "#AAA") + ctx.fillStyle = + this.selected == i ? '#FFF' : this.nearest == i ? '#DDD' : '#AAA' ctx.beginPath() ctx.arc(p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2) ctx.fill() @@ -98,7 +96,8 @@ export class CurveEditor { if (localpos[1] < 0) return // this.captureInput(true); - if (this.size == null) throw new Error("CurveEditor.size was null or undefined.") + if (this.size == null) + throw new Error('CurveEditor.size was null or undefined.') const w = this.size[0] - this.margin * 2 const h = this.size[1] - this.margin * 2 const x = localpos[0] - this.margin @@ -127,12 +126,13 @@ export class CurveEditor { const s = this.selected if (s < 0) return - if (this.size == null) throw new Error("CurveEditor.size was null or undefined.") + if (this.size == null) + throw new Error('CurveEditor.size was null or undefined.') const x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2) const y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2) const curvepos: Point = [ localpos[0] - this.margin, - localpos[1] - this.margin, + localpos[1] - this.margin ] const max_dist = 30 / graphcanvas.ds.scale this._nearest = this.getCloserPoint(curvepos, max_dist) @@ -173,7 +173,8 @@ export class CurveEditor { if (!points) return -1 max_dist = max_dist || 30 - if (this.size == null) throw new Error("CurveEditor.size was null or undefined.") + if (this.size == null) + throw new Error('CurveEditor.size was null or undefined.') const w = this.size[0] - this.margin * 2 const h = this.size[1] - this.margin * 2 const num = points.length diff --git a/src/lib/litegraph/src/DragAndScale.ts b/src/lib/litegraph/src/DragAndScale.ts index 8a56988604..c71fca2d25 100644 --- a/src/lib/litegraph/src/DragAndScale.ts +++ b/src/lib/litegraph/src/DragAndScale.ts @@ -1,6 +1,5 @@ -import type { Point, ReadOnlyRect, Rect } from "./interfaces" - -import { EaseFunction, Rectangle } from "./litegraph" +import type { Point, ReadOnlyRect, Rect } from './interfaces' +import { EaseFunction, Rectangle } from './litegraph' export interface DragAndScaleState { /** @@ -30,7 +29,7 @@ export class DragAndScale { state: DragAndScaleState lastState: DragAndScaleState = { offset: [0, 0], - scale: 0, + scale: 0 } /** Maximum scale (zoom in) */ @@ -67,7 +66,7 @@ export class DragAndScale { constructor(element: HTMLCanvasElement) { this.state = { offset: [0, 0], - scale: 1, + scale: 1 } this.max_scale = 10 this.min_scale = 0.1 @@ -86,9 +85,11 @@ export class DragAndScale { const current = this.state const previous = this.lastState - return current.scale !== previous.scale || + return ( + current.scale !== previous.scale || current.offset[0] !== previous.offset[0] || current.offset[1] !== previous.offset[1] + ) } computeVisibleArea(viewport: Rect | undefined): void { @@ -127,7 +128,7 @@ export class DragAndScale { convertOffsetToCanvas(pos: Point): Point { return [ (pos[0] + this.offset[0]) * this.scale, - (pos[1] + this.offset[1]) * this.scale, + (pos[1] + this.offset[1]) * this.scale ] } @@ -146,7 +147,11 @@ export class DragAndScale { this.onredraw?.(this) } - changeScale(value: number, zooming_center?: Point, roundToScaleOne = true): void { + changeScale( + value: number, + zooming_center?: Point, + roundToScaleOne = true + ): void { if (value < this.min_scale) { value = this.min_scale } else if (value > this.max_scale) { @@ -161,16 +166,13 @@ export class DragAndScale { const normalizedCenter: Point = [ zooming_center[0] - rect.x, - zooming_center[1] - rect.y, + zooming_center[1] - rect.y ] const center = this.convertCanvasToOffset(normalizedCenter) this.scale = value if (roundToScaleOne && Math.abs(this.scale - 1) < 0.01) this.scale = 1 const new_center = this.convertCanvasToOffset(normalizedCenter) - const delta_offset = [ - new_center[0] - center[0], - new_center[1] - center[1], - ] + const delta_offset = [new_center[0] - center[0], new_center[1] - center[1]] this.offset[0] += delta_offset[0] this.offset[1] += delta_offset[1] @@ -186,7 +188,10 @@ export class DragAndScale { * Fits the view to the specified bounds. * @param bounds The bounds to fit the view to, defined by a rectangle. */ - fitToBounds(bounds: ReadOnlyRect, { zoom = 0.75 }: { zoom?: number } = {}): void { + fitToBounds( + bounds: ReadOnlyRect, + { zoom = 0.75 }: { zoom?: number } = {} + ): void { const cw = this.element.width / window.devicePixelRatio const ch = this.element.height / window.devicePixelRatio let targetScale = this.scale @@ -204,8 +209,8 @@ export class DragAndScale { const scaledHeight = ch / targetScale // Calculate the target position to center the bounds in the viewport - const targetX = -bounds[0] - (bounds[2] * 0.5) + (scaledWidth * 0.5) - const targetY = -bounds[1] - (bounds[3] * 0.5) + (scaledHeight * 0.5) + const targetX = -bounds[0] - bounds[2] * 0.5 + scaledWidth * 0.5 + const targetY = -bounds[1] - bounds[3] * 0.5 + scaledHeight * 0.5 // Apply the changes immediately this.offset[0] = targetX @@ -223,16 +228,16 @@ export class DragAndScale { { duration = 350, zoom = 0.75, - easing = EaseFunction.EASE_IN_OUT_QUAD, - }: AnimationOptions = {}, + easing = EaseFunction.EASE_IN_OUT_QUAD + }: AnimationOptions = {} ) { - if (!(duration > 0)) throw new RangeError("Duration must be greater than 0") + if (!(duration > 0)) throw new RangeError('Duration must be greater than 0') const easeFunctions = { linear: (t: number) => t, easeInQuad: (t: number) => t * t, easeOutQuad: (t: number) => t * (2 - t), - easeInOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), + easeInOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t) } const easeFunction = easeFunctions[easing] ?? easeFunctions.linear @@ -241,8 +246,8 @@ export class DragAndScale { const ch = this.element.height / window.devicePixelRatio const startX = this.offset[0] const startY = this.offset[1] - const startX2 = startX - (cw / this.scale) - const startY2 = startY - (ch / this.scale) + const startX2 = startX - cw / this.scale + const startY2 = startY - ch / this.scale const startScale = this.scale let targetScale = startScale @@ -257,8 +262,8 @@ export class DragAndScale { const scaledWidth = cw / targetScale const scaledHeight = ch / targetScale - const targetX = -bounds[0] - (bounds[2] * 0.5) + (scaledWidth * 0.5) - const targetY = -bounds[1] - (bounds[3] * 0.5) + (scaledHeight * 0.5) + const targetX = -bounds[0] - bounds[2] * 0.5 + scaledWidth * 0.5 + const targetY = -bounds[1] - bounds[3] * 0.5 + scaledHeight * 0.5 const targetX2 = targetX - scaledWidth const targetY2 = targetY - scaledHeight @@ -267,14 +272,14 @@ export class DragAndScale { const progress = Math.min(elapsed / duration, 1) const easedProgress = easeFunction(progress) - const currentX = startX + ((targetX - startX) * easedProgress) - const currentY = startY + ((targetY - startY) * easedProgress) + const currentX = startX + (targetX - startX) * easedProgress + const currentY = startY + (targetY - startY) * easedProgress this.offset[0] = currentX this.offset[1] = currentY if (zoom > 0) { - const currentX2 = startX2 + ((targetX2 - startX2) * easedProgress) - const currentY2 = startY2 + ((targetY2 - startY2) * easedProgress) + const currentX2 = startX2 + (targetX2 - startX2) * easedProgress + const currentY2 = startY2 + (targetY2 - startY2) * easedProgress const currentWidth = Math.abs(currentX2 - currentX) const currentHeight = Math.abs(currentY2 - currentY) diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 206aa8cff1..0845ef2057 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -1,50 +1,64 @@ -import type { DragAndScaleState } from "./DragAndScale" -import type { LGraphEventMap } from "./infrastructure/LGraphEventMap" -import type { SubgraphEventMap } from "./infrastructure/SubgraphEventMap" +import { + SUBGRAPH_INPUT_ID, + SUBGRAPH_OUTPUT_ID +} from '@/lib/litegraph/src/constants' +import type { UUID } from '@/lib/litegraph/src/utils/uuid' +import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid' + +import type { DragAndScaleState } from './DragAndScale' +import { LGraphCanvas } from './LGraphCanvas' +import { LGraphGroup } from './LGraphGroup' +import { LGraphNode, type NodeId } from './LGraphNode' +import { LLink, type LinkId } from './LLink' +import { MapProxyHandler } from './MapProxyHandler' +import { Reroute, type RerouteId } from './Reroute' +import { CustomEventTarget } from './infrastructure/CustomEventTarget' +import type { LGraphEventMap } from './infrastructure/LGraphEventMap' +import type { SubgraphEventMap } from './infrastructure/SubgraphEventMap' import type { + DefaultConnectionColors, Dictionary, IContextMenuValue, + INodeInputSlot, + INodeOutputSlot, LinkNetwork, LinkSegment, MethodNames, OptionalProps, Point, - Positionable, - DefaultConnectionColors, - INodeInputSlot, - INodeOutputSlot, -} from "./interfaces" + Positionable +} from './interfaces' +import { LiteGraph, SubgraphNode } from './litegraph' +import { + alignOutsideContainer, + alignToContainer, + createBounds +} from './measure' +import { stringOrEmpty } from './strings' +import { SubgraphInput } from './subgraph/SubgraphInput' +import { SubgraphInputNode } from './subgraph/SubgraphInputNode' +import { SubgraphOutput } from './subgraph/SubgraphOutput' +import { SubgraphOutputNode } from './subgraph/SubgraphOutputNode' +import { + findUsedSubgraphIds, + getBoundaryLinks, + groupResolvedByOutput, + mapSubgraphInputsAndLinks, + mapSubgraphOutputsAndLinks, + multiClone, + splitPositionables +} from './subgraph/subgraphUtils' +import { Alignment, LGraphEventMode } from './types/globalEnums' import type { ExportedSubgraph, + ExposedWidget, ISerialisedGraph, ISerialisedNode, Serialisable, SerialisableGraph, - SerialisableReroute, - ExposedWidget, -} from "./types/serialisation" -import type { UUID } from "@/lib/litegraph/src/utils/uuid" - -import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from "@/lib/litegraph/src/constants" -import { createUuidv4, zeroUuid } from "@/lib/litegraph/src/utils/uuid" - -import { CustomEventTarget } from "./infrastructure/CustomEventTarget" -import { LGraphCanvas } from "./LGraphCanvas" -import { LGraphGroup } from "./LGraphGroup" -import { LGraphNode, type NodeId } from "./LGraphNode" -import { LiteGraph, SubgraphNode } from "./litegraph" -import { type LinkId, LLink } from "./LLink" -import { MapProxyHandler } from "./MapProxyHandler" -import { alignOutsideContainer, alignToContainer, createBounds } from "./measure" -import { Reroute, type RerouteId } from "./Reroute" -import { stringOrEmpty } from "./strings" -import { SubgraphInput } from "./subgraph/SubgraphInput" -import { SubgraphOutput } from "./subgraph/SubgraphOutput" -import { SubgraphInputNode } from "./subgraph/SubgraphInputNode" -import { SubgraphOutputNode } from "./subgraph/SubgraphOutputNode" -import { findUsedSubgraphIds, getBoundaryLinks, groupResolvedByOutput, mapSubgraphInputsAndLinks, mapSubgraphOutputsAndLinks, multiClone, splitPositionables } from "./subgraph/subgraphUtils" -import { Alignment, LGraphEventMode } from "./types/globalEnums" -import { getAllNestedItems } from "./utils/collections" + SerialisableReroute +} from './types/serialisation' +import { getAllNestedItems } from './utils/collections' export interface LGraphState { lastGroupId: number @@ -53,10 +67,12 @@ export interface LGraphState { lastRerouteId: number } -type ParamsArray, K extends MethodNames> = - Parameters[1] extends undefined - ? Parameters | Parameters[0] - : Parameters +type ParamsArray< + T extends Record, + K extends MethodNames +> = Parameters[1] extends undefined + ? Parameters | Parameters[0] + : Parameters /** Configuration used by {@link LGraph} `config`. */ export interface LGraphConfig { @@ -67,7 +83,7 @@ export interface LGraphConfig { export interface LGraphExtra extends Dictionary { reroutes?: SerialisableReroute[] - linkExtensions?: { id: number, parentId: number | undefined }[] + linkExtensions?: { id: number; parentId: number | undefined }[] ds?: DragAndScaleState } @@ -82,7 +98,9 @@ export interface BaseLGraph { * + onNodeAdded: when a new node is added to the graph * + onNodeRemoved: when a node inside this graph is removed */ -export class LGraph implements LinkNetwork, BaseLGraph, Serialisable { +export class LGraph + implements LinkNetwork, BaseLGraph, Serialisable +{ static serialisedSchemaVersion = 1 as const static STATUS_STOPPED = 1 @@ -90,21 +108,21 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable() @@ -238,7 +256,10 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable | null)[], node: LGraphNode): void + onGetNodeMenuOptions?( + options: (IContextMenuValue | null)[], + node: LGraphNode + ): void // @ts-ignore TODO: Fix after migration to frontend tsconfig rules private _input_nodes?: LGraphNode[] @@ -248,13 +269,14 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable() - this.links = new Proxy(links, handler) as Map & Record + this.links = new Proxy(links, handler) as Map & + Record this.list_of_graphcanvas = null this.clear() @@ -276,7 +298,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable c.clear()) + this.canvasAction((c) => c.clear()) } get subgraphs(): Map { @@ -354,7 +376,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable { @@ -446,7 +468,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable>( action: T, - params?: ParamsArray, + params?: ParamsArray ): void { const { list_of_graphcanvas } = this if (!list_of_graphcanvas) return @@ -796,7 +817,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable= LiteGraph.MAX_NUMBER_OF_NODES) { - throw "LiteGraph: max number of nodes in a graph reached" + throw 'LiteGraph: max number of nodes in a graph reached' } // give him an id if (LiteGraph.use_uuids) { - if (node.id == null || node.id == -1) - node.id = LiteGraph.uuidv4() + if (node.id == null || node.id == -1) node.id = LiteGraph.uuidv4() } else { if (node.id == null || node.id == -1) { node.id = ++state.lastNodeId - } else if (typeof node.id === "number" && state.lastNodeId < node.id) { + } else if (typeof node.id === 'number' && state.lastNodeId < node.id) { state.lastNodeId = node.id } } @@ -876,7 +894,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable c.deselect(node)) + this.canvasAction((c) => c.deselect(node)) const index = this._groups.indexOf(node) if (index != -1) { @@ -891,12 +909,12 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable c.checkPanels()) + this.canvasAction((c) => c.checkPanels()) this.setDirtyCanvas(true, true) // sure? - almost sure is wrong @@ -966,9 +984,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable): Reroute | undefined { + getRerouteOnPos( + x: number, + y: number, + reroutes?: Iterable + ): Reroute | undefined { for (const reroute of reroutes ?? this.reroutes.values()) { if (reroute.containsPoint([x, y])) return reroute } @@ -1143,7 +1159,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable c.onBeforeChange?.(this)) + this.canvasAction((c) => c.onBeforeChange?.(this)) } // used to resend actions, called after any change is made to the graph afterChange(info?: LGraphNode | null): void { this.onAfterChange?.(this, info) - this.canvasAction(c => c.onAfterChange?.(this)) + this.canvasAction((c) => c.onAfterChange?.(this)) } /** @@ -1206,14 +1222,14 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable c.setDirty(true, true)) + this.canvasAction((c) => c.setDirty(true, true)) this.on_change?.(this) } setDirtyCanvas(fg: boolean, bg?: boolean): void { - this.canvasAction(c => c.setDirty(fg, bg)) + this.canvasAction((c) => c.setDirty(fg, bg)) } addFloatingLink(link: LLink): LLink { @@ -1222,14 +1238,17 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable): Reroute { + setReroute({ + id, + parentId, + pos, + linkIds, + floating + }: OptionalProps): Reroute { id ??= ++this.state.lastRerouteId if (id > this.state.lastRerouteId) this.state.lastRerouteId = id @@ -1306,13 +1332,17 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable x.parentId === before.parentId)) { + for (const x of reroutes.filter((x) => x.parentId === before.parentId)) { x.parentId = rerouteId } } @@ -1331,7 +1361,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable x.parentId === before.parentId)) { + for (const x of reroutes.filter((x) => x.parentId === before.parentId)) { x.parentId = rerouteId } } @@ -1348,7 +1378,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable c.deselect(reroute)) + this.canvasAction((c) => c.deselect(reroute)) // Extract reroute from the reroute chain const { parentId, linkIds, floatingLinkIds } = reroute @@ -1364,7 +1394,9 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable): { subgraph: Subgraph, node: SubgraphNode } { - if (items.size === 0) throw new Error("Cannot convert to subgraph: nothing to convert") + convertToSubgraph(items: Set): { + subgraph: Subgraph + node: SubgraphNode + } { + if (items.size === 0) + throw new Error('Cannot convert to subgraph: nothing to convert') const { state, revision, config } = this - const { boundaryLinks, boundaryFloatingLinks, internalLinks, boundaryInputLinks, boundaryOutputLinks } = getBoundaryLinks(this, items) + const { + boundaryLinks, + boundaryFloatingLinks, + internalLinks, + boundaryInputLinks, + boundaryOutputLinks + } = getBoundaryLinks(this, items) const { nodes, reroutes, groups } = splitPositionables(items) const boundingRect = createBounds(items) - if (!boundingRect) throw new Error("Failed to create bounding rect for subgraph") + if (!boundingRect) + throw new Error('Failed to create bounding rect for subgraph') - const resolvedInputLinks = boundaryInputLinks.map(x => x.resolve(this)) - const resolvedOutputLinks = boundaryOutputLinks.map(x => x.resolve(this)) + const resolvedInputLinks = boundaryInputLinks.map((x) => x.resolve(this)) + const resolvedOutputLinks = boundaryOutputLinks.map((x) => x.resolve(this)) const clonedNodes = multiClone(nodes) // Inputs, outputs, and links - const links = internalLinks.map(x => x.asSerialisable()) + const links = internalLinks.map((x) => x.asSerialisable()) const inputs = mapSubgraphInputsAndLinks(resolvedInputLinks, links) const outputs = mapSubgraphOutputsAndLinks(resolvedOutputLinks, links) // Prepare subgraph data const data = { id: createUuidv4(), - name: "New Subgraph", + name: 'New Subgraph', inputNode: { id: SUBGRAPH_INPUT_ID, - bounding: [0, 0, 75, 100], + bounding: [0, 0, 75, 100] }, outputNode: { id: SUBGRAPH_OUTPUT_ID, - bounding: [0, 0, 75, 100], + bounding: [0, 0, 75, 100] }, inputs, outputs, @@ -1460,8 +1503,10 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable reroute.asSerialisable())), - groups: structuredClone([...groups].map(group => group.serialize())), + reroutes: structuredClone( + [...reroutes].map((reroute) => reroute.asSerialisable()) + ), + groups: structuredClone([...groups].map((group) => group.serialize())) } satisfies ExportedSubgraph const subgraph = this.createSubgraph(data) @@ -1476,14 +1521,22 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable x.serialize()) + const links = linkArray.map((x) => x.serialize()) if (reroutes?.length) { // Link parent IDs cannot go in 0.4 schema arrays extra.linkExtensions = linkArray - .filter(x => x.parentId !== undefined) - .map(x => ({ id: x.id, parentId: x.parentId })) + .filter((x) => x.parentId !== undefined) + .map((x) => ({ id: x.id, parentId: x.parentId })) } extra.reroutes = reroutes?.length ? reroutes : undefined @@ -1643,7 +1724,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable> { + asSerialisable(options?: { + sortNodes: boolean + }): SerialisableGraph & + Required> { const { id, revision, config, state } = this - const nodeList = !LiteGraph.use_uuids && options?.sortNodes - // @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers. - ? [...this._nodes].sort((a, b) => a.id - b.id) - : this._nodes + const nodeList = + !LiteGraph.use_uuids && options?.sortNodes + ? // @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers. + [...this._nodes].sort((a, b) => a.id - b.id) + : this._nodes - const nodes = nodeList.map(node => node.serialize()) - const groups = this._groups.map(x => x.serialize()) + const nodes = nodeList.map((node) => node.serialize()) + const groups = this._groups.map((x) => x.serialize()) - const links = this._links.size ? [...this._links.values()].map(x => x.asSerialisable()) : undefined - const floatingLinks = this.floatingLinks.size ? [...this.floatingLinks.values()].map(x => x.asSerialisable()) : undefined - const reroutes = this.reroutes.size ? [...this.reroutes.values()].map(x => x.asSerialisable()) : undefined + const links = this._links.size + ? [...this._links.values()].map((x) => x.asSerialisable()) + : undefined + const floatingLinks = this.floatingLinks.size + ? [...this.floatingLinks.values()].map((x) => x.asSerialisable()) + : undefined + const reroutes = this.reroutes.size + ? [...this.reroutes.values()].map((x) => x.asSerialisable()) + : undefined // Save scale and offset const extra = { ...this.extra } @@ -1692,14 +1783,14 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable usedSubgraphIds.has(subgraph.id)) - .map(x => x.asSerialisable()) + .filter((subgraph) => usedSubgraphIds.has(subgraph.id)) + .map((x) => x.asSerialisable()) if (usedSubgraphs.length > 0) { data.definitions = { subgraphs: usedSubgraphs } @@ -1735,13 +1826,13 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable this.#lastFloatingLinkId) this.#lastFloatingLinkId = floatingLink.id + if (floatingLink.id > this.#lastFloatingLinkId) + this.#lastFloatingLinkId = floatingLink.id } } @@ -1908,7 +2003,7 @@ export class LGraph implements LinkNetwork, BaseLGraph, Serialisable { - console.error("Error loading graph:", err) + req.addEventListener('error', (err) => { + console.error('Error loading graph:', err) }) } } @@ -1967,14 +2062,17 @@ export type GraphOrSubgraph = LGraph | Subgraph // ============================================================================ /** A subgraph definition. */ -export class Subgraph extends LGraph implements BaseLGraph, Serialisable { +export class Subgraph + extends LGraph + implements BaseLGraph, Serialisable +{ override readonly events = new CustomEventTarget() /** Limits the number of levels / depth that subgraphs may be nested. Prevents uncontrolled programmatic nesting. */ static MAX_NESTED_SUBGRAPHS = 1000 /** The display name of the subgraph. */ - name: string = "Unnamed Subgraph" + name: string = 'Unnamed Subgraph' readonly inputNode = new SubgraphInputNode(this) readonly outputNode = new SubgraphOutputNode(this) @@ -1991,11 +2089,8 @@ export class Subgraph extends LGraph implements BaseLGraph, Serialisable> { + override asSerialisable(): ExportedSubgraph & + Required> { return { id: this.id, version: LGraph.serialisedSchemaVersion, @@ -2185,13 +2321,13 @@ export class Subgraph extends LGraph implements BaseLGraph, Serialisable x.asSerialisable()), - outputs: this.outputs.map(x => x.asSerialisable()), + inputs: this.inputs.map((x) => x.asSerialisable()), + outputs: this.outputs.map((x) => x.asSerialisable()), widgets: [...this.widgets], - nodes: this.nodes.map(node => node.serialize()), - groups: this.groups.map(group => group.serialize()), - links: [...this.links.values()].map(x => x.asSerialisable()), - extra: this.extra, + nodes: this.nodes.map((node) => node.serialize()), + groups: this.groups.map((group) => group.serialize()), + links: [...this.links.values()].map((x) => x.asSerialisable()), + extra: this.extra } } } diff --git a/src/lib/litegraph/src/LGraphBadge.ts b/src/lib/litegraph/src/LGraphBadge.ts index 8e613c2af9..46bb78e6d0 100644 --- a/src/lib/litegraph/src/LGraphBadge.ts +++ b/src/lib/litegraph/src/LGraphBadge.ts @@ -1,8 +1,8 @@ -import { LGraphIcon, type LGraphIconOptions } from "./LGraphIcon" +import { LGraphIcon, type LGraphIconOptions } from './LGraphIcon' export enum BadgePosition { - TopLeft = "top-left", - TopRight = "top-right", + TopLeft = 'top-left', + TopRight = 'top-right' } export interface LGraphBadgeOptions { @@ -32,15 +32,15 @@ export class LGraphBadge { constructor({ text, - fgColor = "white", - bgColor = "#0F1F0F", + fgColor = 'white', + bgColor = '#0F1F0F', fontSize = 12, padding = 6, height = 20, cornerRadius = 5, iconOptions, xOffset = 0, - yOffset = 0, + yOffset = 0 }: LGraphBadgeOptions) { this.text = text this.fgColor = fgColor @@ -74,11 +74,7 @@ export class LGraphBadge { return iconWidth + textWidth + this.padding * 2 } - draw( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - ): void { + draw(ctx: CanvasRenderingContext2D, x: number, y: number): void { if (!this.visible) return x += this.xOffset @@ -113,8 +109,8 @@ export class LGraphBadge { // Draw badge text if (this.text) { ctx.fillStyle = this.fgColor - ctx.textBaseline = "middle" - ctx.textAlign = "left" + ctx.textBaseline = 'middle' + ctx.textAlign = 'left' ctx.fillText(this.text, drawX, centerY + 1) } diff --git a/src/lib/litegraph/src/LGraphButton.ts b/src/lib/litegraph/src/LGraphButton.ts index 1ee27c6f82..d2ebe9d302 100644 --- a/src/lib/litegraph/src/LGraphButton.ts +++ b/src/lib/litegraph/src/LGraphButton.ts @@ -1,5 +1,5 @@ -import { Rectangle } from "./infrastructure/Rectangle" -import { LGraphBadge, type LGraphBadgeOptions } from "./LGraphBadge" +import { LGraphBadge, type LGraphBadgeOptions } from './LGraphBadge' +import { Rectangle } from './infrastructure/Rectangle' export interface LGraphButtonOptions extends LGraphBadgeOptions { name?: string // To identify the button @@ -55,13 +55,13 @@ export class LGraphButton extends LGraphBadge { const { font, fillStyle, textBaseline, textAlign } = ctx // Use the same color as the title text (usually white) - const titleTextColor = ctx.fillStyle || "white" + const titleTextColor = ctx.fillStyle || 'white' // Draw as icon-only without background ctx.font = `${this.fontSize}px 'PrimeIcons'` ctx.fillStyle = titleTextColor - ctx.textBaseline = "middle" - ctx.textAlign = "center" + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' const centerX = adjustedX + width / 2 const centerY = adjustedY + this.height / 2 diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index c63fb8b88e..a3a4523123 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -1,7 +1,22 @@ // @ts-nocheck TODO: Fix after migration to frontend tsconfig rules -import type { ContextMenu } from "./ContextMenu" -import type { CustomEventDispatcher, ICustomEventTarget } from "./infrastructure/CustomEventTarget" -import type { LGraphCanvasEventMap } from "./infrastructure/LGraphCanvasEventMap" +import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector' + +import { CanvasPointer } from './CanvasPointer' +import type { ContextMenu } from './ContextMenu' +import { type AnimationOptions, DragAndScale } from './DragAndScale' +import type { LGraph } from './LGraph' +import { LGraphGroup } from './LGraphGroup' +import { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode' +import { LLink, type LinkId } from './LLink' +import { Reroute, type RerouteId } from './Reroute' +import { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots' +import { strokeShape } from './draw' +import type { + CustomEventDispatcher, + ICustomEventTarget +} from './infrastructure/CustomEventTarget' +import type { LGraphCanvasEventMap } from './infrastructure/LGraphCanvasEventMap' +import { NullGraphError } from './infrastructure/NullGraphError' import type { CanvasColour, ColorOption, @@ -26,30 +41,9 @@ import type { ReadOnlyPoint, ReadOnlyRect, Rect, - Size, -} from "./interfaces" -import type { LGraph } from "./LGraph" -import type { - CanvasPointerEvent, - CanvasPointerExtensions, -} from "./types/events" -import type { ClipboardItems, SubgraphIO } from "./types/serialisation" -import type { NeverNever } from "./types/utility" -import type { PickNevers } from "./types/utility" -import type { IBaseWidget } from "./types/widgets" -import type { UUID } from "./utils/uuid" - -import { LinkConnector } from "@/lib/litegraph/src/canvas/LinkConnector" - -import { isOverNodeInput, isOverNodeOutput } from "./canvas/measureSlots" -import { CanvasPointer } from "./CanvasPointer" -import { type AnimationOptions, DragAndScale } from "./DragAndScale" -import { strokeShape } from "./draw" -import { NullGraphError } from "./infrastructure/NullGraphError" -import { LGraphGroup } from "./LGraphGroup" -import { LGraphNode, type NodeId, type NodeProperty } from "./LGraphNode" -import { createUuidv4, LiteGraph, Rectangle, SubgraphNode } from "./litegraph" -import { type LinkId, LLink } from "./LLink" + Size +} from './interfaces' +import { LiteGraph, Rectangle, SubgraphNode, createUuidv4 } from './litegraph' import { containsRect, createBounds, @@ -59,15 +53,18 @@ import { isInRectangle, isPointInRect, overlapBounding, - snapPoint, -} from "./measure" -import { NodeInputSlot } from "./node/NodeInputSlot" -import { Reroute, type RerouteId } from "./Reroute" -import { stringOrEmpty } from "./strings" -import { Subgraph } from "./subgraph/Subgraph" -import { SubgraphInputNode } from "./subgraph/SubgraphInputNode" -import { SubgraphIONodeBase } from "./subgraph/SubgraphIONodeBase" -import { SubgraphOutputNode } from "./subgraph/SubgraphOutputNode" + snapPoint +} from './measure' +import { NodeInputSlot } from './node/NodeInputSlot' +import { stringOrEmpty } from './strings' +import { Subgraph } from './subgraph/Subgraph' +import { SubgraphIONodeBase } from './subgraph/SubgraphIONodeBase' +import { SubgraphInputNode } from './subgraph/SubgraphInputNode' +import { SubgraphOutputNode } from './subgraph/SubgraphOutputNode' +import type { + CanvasPointerEvent, + CanvasPointerExtensions +} from './types/events' import { CanvasItem, LGraphEventMode, @@ -75,12 +72,17 @@ import { LinkMarkerShape, LinkRenderType, RenderShape, - TitleMode, -} from "./types/globalEnums" -import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange" -import { findFirstNode, getAllNestedItems } from "./utils/collections" -import { BaseWidget } from "./widgets/BaseWidget" -import { toConcreteWidget } from "./widgets/widgetMap" + TitleMode +} from './types/globalEnums' +import type { ClipboardItems, SubgraphIO } from './types/serialisation' +import type { NeverNever } from './types/utility' +import type { PickNevers } from './types/utility' +import type { IBaseWidget } from './types/widgets' +import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange' +import { findFirstNode, getAllNestedItems } from './utils/collections' +import type { UUID } from './utils/uuid' +import { BaseWidget } from './widgets/BaseWidget' +import { toConcreteWidget } from './widgets/widgetMap' interface IShowSearchOptions { node_to?: LGraphNode | null @@ -132,7 +134,7 @@ interface HasShowSearchCallback { /** See {@link LGraphCanvas.showSearchBox} */ showSearchBox: ( event: MouseEvent, - options?: IShowSearchOptions, + options?: IShowSearchOptions ) => HTMLDivElement | void } @@ -146,7 +148,7 @@ interface IDialogExtensions extends ICloseable { } interface IDialog extends HTMLDivElement, IDialogExtensions {} -type PromptDialog = Omit +type PromptDialog = Omit interface IDialogOptions { position?: Point @@ -212,17 +214,19 @@ interface ICreatePanelOptions { } const cursors = { - NE: "nesw-resize", - SE: "nwse-resize", - SW: "nesw-resize", - NW: "nwse-resize", + NE: 'nesw-resize', + SE: 'nwse-resize', + SW: 'nesw-resize', + NW: 'nwse-resize' } as const /** * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked */ -export class LGraphCanvas implements CustomEventDispatcher { +export class LGraphCanvas + implements CustomEventDispatcher +{ // Optimised buffers used during rendering static #temp = new Float32Array(4) static #temp_vec2 = new Float32Array(2) @@ -233,34 +237,35 @@ export class LGraphCanvas implements CustomEventDispatcher static #lTempB: Point = new Float32Array(2) static #lTempC: Point = new Float32Array(2) - static DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=" + static DEFAULT_BACKGROUND_IMAGE = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=' - static DEFAULT_EVENT_LINK_COLOR = "#A86" + static DEFAULT_EVENT_LINK_COLOR = '#A86' /** Link type to colour dictionary. */ static link_type_colors: Dictionary = { - "-1": LGraphCanvas.DEFAULT_EVENT_LINK_COLOR, - "number": "#AAA", - "node": "#DCA", + '-1': LGraphCanvas.DEFAULT_EVENT_LINK_COLOR, + number: '#AAA', + node: '#DCA' } static gradients: Record = {} static search_limit = -1 static node_colors: Record = { - red: { color: "#322", bgcolor: "#533", groupcolor: "#A88" }, - brown: { color: "#332922", bgcolor: "#593930", groupcolor: "#b06634" }, - green: { color: "#232", bgcolor: "#353", groupcolor: "#8A8" }, - blue: { color: "#223", bgcolor: "#335", groupcolor: "#88A" }, + red: { color: '#322', bgcolor: '#533', groupcolor: '#A88' }, + brown: { color: '#332922', bgcolor: '#593930', groupcolor: '#b06634' }, + green: { color: '#232', bgcolor: '#353', groupcolor: '#8A8' }, + blue: { color: '#223', bgcolor: '#335', groupcolor: '#88A' }, pale_blue: { - color: "#2a363b", - bgcolor: "#3f5159", - groupcolor: "#3f789e", + color: '#2a363b', + bgcolor: '#3f5159', + groupcolor: '#3f789e' }, - cyan: { color: "#233", bgcolor: "#355", groupcolor: "#8AA" }, - purple: { color: "#323", bgcolor: "#535", groupcolor: "#a1309b" }, - yellow: { color: "#432", bgcolor: "#653", groupcolor: "#b58b2a" }, - black: { color: "#222", bgcolor: "#000", groupcolor: "#444" }, + cyan: { color: '#233', bgcolor: '#355', groupcolor: '#8AA' }, + purple: { color: '#323', bgcolor: '#535', groupcolor: '#a1309b' }, + yellow: { color: '#432', bgcolor: '#653', groupcolor: '#b58b2a' }, + black: { color: '#222', bgcolor: '#000', groupcolor: '#444' } } /** @@ -279,7 +284,7 @@ export class LGraphCanvas implements CustomEventDispatcher readOnly: false, hoveringOver: CanvasItem.Nothing, shouldSetCursor: true, - selectionChanged: false, + selectionChanged: false } #subgraph?: Subgraph @@ -290,19 +295,32 @@ export class LGraphCanvas implements CustomEventDispatcher set subgraph(value: Subgraph | undefined) { if (value !== this.#subgraph) { this.#subgraph = value - if (value) this.dispatch("litegraph:set-graph", { oldGraph: this.#subgraph, newGraph: value }) + if (value) + this.dispatch('litegraph:set-graph', { + oldGraph: this.#subgraph, + newGraph: value + }) } } /** Dispatches a custom event on the canvas. */ - dispatch>(type: T, detail: LGraphCanvasEventMap[T]): boolean + dispatch>( + type: T, + detail: LGraphCanvasEventMap[T] + ): boolean dispatch>(type: T): boolean - dispatch(type: T, detail?: LGraphCanvasEventMap[T]) { + dispatch( + type: T, + detail?: LGraphCanvasEventMap[T] + ) { const event = new CustomEvent(type as string, { detail, bubbles: true }) return this.canvas.dispatchEvent(event) } - dispatchEvent(type: TEvent, detail: LGraphCanvasEventMap[TEvent]) { + dispatchEvent( + type: TEvent, + detail: LGraphCanvasEventMap[TEvent] + ) { this.canvas.dispatchEvent(new CustomEvent(type, { detail })) } @@ -315,17 +333,17 @@ export class LGraphCanvas implements CustomEventDispatcher CanvasItem.SubgraphIoNode | CanvasItem.SubgraphIoSlot - let cursor = "default" + let cursor = 'default' if (this.state.draggingCanvas) { - cursor = "grabbing" + cursor = 'grabbing' } else if (this.state.readOnly) { - cursor = "grab" + cursor = 'grab' } else if (this.pointer.resizeDirection) { cursor = cursors[this.pointer.resizeDirection] ?? cursors.SE } else if (this.state.hoveringOver & crosshairItems) { - cursor = "crosshair" + cursor = 'crosshair' } else if (this.state.hoveringOver & CanvasItem.Reroute) { - cursor = "grab" + cursor = 'grab' } this.canvas.style.cursor = cursor @@ -398,7 +416,9 @@ export class LGraphCanvas implements CustomEventDispatcher #maximumFrameGap = 0 /** Maximum frames per second to render. 0: unlimited. Default: 0 */ public get maximumFps() { - return this.#maximumFrameGap > Number.EPSILON ? this.#maximumFrameGap / 1000 : 0 + return this.#maximumFrameGap > Number.EPSILON + ? this.#maximumFrameGap / 1000 + : 0 } public set maximumFps(value) { @@ -458,7 +478,7 @@ export class LGraphCanvas implements CustomEventDispatcher getDisconnectedColor: (type: string) => this.default_connection_color_byTypeOff[type] || this.default_connection_color_byType[type] || - this.default_connection_color.output_off, + this.default_connection_color.output_off } highquality_render: boolean @@ -526,7 +546,7 @@ export class LGraphCanvas implements CustomEventDispatcher visible_links: LLink[] = [] /** @deprecated This array is populated and cleared to support legacy extensions. The contents are ignored by Litegraph. */ connecting_links: ConnectingLink[] | null - linkConnector = new LinkConnector(links => this.connecting_links = links) + linkConnector = new LinkConnector((links) => (this.connecting_links = links)) /** The viewport of this canvas. Tightly coupled with {@link ds}. */ readonly viewport?: Rect autoresize: boolean @@ -620,12 +640,12 @@ export class LGraphCanvas implements CustomEventDispatcher /** If true, enable drag zoom. Ctrl+Shift+Drag Up/Down: zoom canvas. */ dragZoomEnabled: boolean = false /** The start position of the drag zoom. */ - #dragZoomStart: { pos: Point, scale: number } | null = null + #dragZoomStart: { pos: Point; scale: number } | null = null getMenuOptions?(): IContextMenuValue[] getExtraMenuOptions?( canvas: LGraphCanvas, - options: IContextMenuValue[], + options: IContextMenuValue[] ): IContextMenuValue[] static active_node: LGraphNode /** called before modifying the graph */ @@ -641,14 +661,14 @@ export class LGraphCanvas implements CustomEventDispatcher onDrawLinkTooltip?: ( ctx: CanvasRenderingContext2D, link: LLink | null, - canvas?: LGraphCanvas, + canvas?: LGraphCanvas ) => boolean /** to render foreground objects not affected by transform (for GUIs) */ onDrawOverlay?: (ctx: CanvasRenderingContext2D) => void onRenderBackground?: ( canvas: HTMLCanvasElement, - ctx: CanvasRenderingContext2D, + ctx: CanvasRenderingContext2D ) => boolean onNodeDblClicked?: (n: LGraphNode) => void @@ -666,7 +686,7 @@ export class LGraphCanvas implements CustomEventDispatcher constructor( canvas: HTMLCanvasElement, graph: LGraph, - options?: LGraphCanvas["options"], + options?: LGraphCanvas['options'] ) { options ||= {} this.options = options @@ -678,55 +698,74 @@ export class LGraphCanvas implements CustomEventDispatcher this.ds = new DragAndScale(canvas) this.pointer = new CanvasPointer(canvas) - this.linkConnector.events.addEventListener("link-created", () => this.#dirty()) + this.linkConnector.events.addEventListener('link-created', () => + this.#dirty() + ) // @deprecated Workaround: Keep until connecting_links is removed. - this.linkConnector.events.addEventListener("reset", () => { + this.linkConnector.events.addEventListener('reset', () => { this.connecting_links = null this.dirty_bgcanvas = true }) // Dropped a link on the canvas - this.linkConnector.events.addEventListener("dropped-on-canvas", (customEvent) => { - if (!this.connecting_links) return + this.linkConnector.events.addEventListener( + 'dropped-on-canvas', + (customEvent) => { + if (!this.connecting_links) return - const e = customEvent.detail - this.emitEvent({ - subType: "empty-release", - originalEvent: e, - linkReleaseContext: { links: this.connecting_links }, - }) + const e = customEvent.detail + this.emitEvent({ + subType: 'empty-release', + originalEvent: e, + linkReleaseContext: { links: this.connecting_links } + }) - const firstLink = this.linkConnector.renderLinks[0] + const firstLink = this.linkConnector.renderLinks[0] - // No longer in use - // add menu when releasing link in empty space - if (LiteGraph.release_link_on_empty_shows_menu) { - const linkReleaseContext = this.linkConnector.state.connectingTo === "input" - ? { - node_from: firstLink.node as LGraphNode, - slot_from: firstLink.fromSlot as INodeOutputSlot, - type_filter_in: firstLink.fromSlot.type, + // No longer in use + // add menu when releasing link in empty space + if (LiteGraph.release_link_on_empty_shows_menu) { + const linkReleaseContext = + this.linkConnector.state.connectingTo === 'input' + ? { + node_from: firstLink.node as LGraphNode, + slot_from: firstLink.fromSlot as INodeOutputSlot, + type_filter_in: firstLink.fromSlot.type + } + : { + node_to: firstLink.node as LGraphNode, + slot_to: firstLink.fromSlot as INodeInputSlot, + type_filter_out: firstLink.fromSlot.type + } + + const afterRerouteId = firstLink.fromReroute?.id + + if ('shiftKey' in e && e.shiftKey) { + if (this.allow_searchbox) { + this.showSearchBox( + e as unknown as MouseEvent, + linkReleaseContext as IShowSearchOptions + ) + } + } else if (this.linkConnector.state.connectingTo === 'input') { + this.showConnectionMenu({ + nodeFrom: firstLink.node as LGraphNode, + slotFrom: firstLink.fromSlot as INodeOutputSlot, + e, + afterRerouteId + }) + } else { + this.showConnectionMenu({ + nodeTo: firstLink.node as LGraphNode, + slotTo: firstLink.fromSlot as INodeInputSlot, + e, + afterRerouteId + }) } - : { - node_to: firstLink.node as LGraphNode, - slot_to: firstLink.fromSlot as INodeInputSlot, - type_filter_out: firstLink.fromSlot.type, - } - - const afterRerouteId = firstLink.fromReroute?.id - - if ("shiftKey" in e && e.shiftKey) { - if (this.allow_searchbox) { - this.showSearchBox(e as unknown as MouseEvent, linkReleaseContext as IShowSearchOptions) - } - } else if (this.linkConnector.state.connectingTo === "input") { - this.showConnectionMenu({ nodeFrom: firstLink.node as LGraphNode, slotFrom: firstLink.fromSlot as INodeOutputSlot, e, afterRerouteId }) - } else { - this.showConnectionMenu({ nodeTo: firstLink.node as LGraphNode, slotTo: firstLink.fromSlot as INodeInputSlot, e, afterRerouteId }) } } - }) + ) // otherwise it generates ugly patterns when scaling down too much this.zoom_modify_alpha = true @@ -736,10 +775,10 @@ export class LGraphCanvas implements CustomEventDispatcher this.node_title_color = LiteGraph.NODE_TITLE_COLOR this.default_link_color = LiteGraph.LINK_COLOR this.default_connection_color = { - input_off: "#778", - input_on: "#7F7", - output_off: "#778", - output_on: "#7F7", + input_off: '#778', + input_on: '#7F7', + output_off: '#778', + output_on: '#7F7' } this.default_connection_color_byType = { /* number: "#7F7", @@ -759,7 +798,7 @@ export class LGraphCanvas implements CustomEventDispatcher this.editor_alpha = 1 this.pause_rendering = false this.clear_background = true - this.clear_background_color = "#222" + this.clear_background_color = '#222' this.render_only_selected = true this.show_info = true @@ -825,7 +864,10 @@ export class LGraphCanvas implements CustomEventDispatcher this.setCanvas(canvas, options.skip_events) this.clear() - LGraphCanvas._measureText = (text: string, fontStyle = this.inner_text_font) => { + LGraphCanvas._measureText = ( + text: string, + fontStyle = this.inner_text_font + ) => { const { ctx } = this const { font } = ctx try { @@ -843,7 +885,11 @@ export class LGraphCanvas implements CustomEventDispatcher this.autoresize = options.autoresize } - static onGroupAdd(info: unknown, entry: unknown, mouse_event: MouseEvent): void { + static onGroupAdd( + info: unknown, + entry: unknown, + mouse_event: MouseEvent + ): void { const canvas = LGraphCanvas.active_canvas const group = new LiteGraph.LGraphGroup() @@ -859,7 +905,7 @@ export class LGraphCanvas implements CustomEventDispatcher * @returns */ static getBoundaryNodes( - nodes: LGraphNode[] | Dictionary, + nodes: LGraphNode[] | Dictionary ): NullableProperties { const _nodes = Array.isArray(nodes) ? nodes : Object.values(nodes) return ( @@ -867,7 +913,7 @@ export class LGraphCanvas implements CustomEventDispatcher top: null, right: null, bottom: null, - left: null, + left: null } ) } @@ -881,7 +927,7 @@ export class LGraphCanvas implements CustomEventDispatcher static alignNodes( nodes: Dictionary, direction: Direction, - align_to?: LGraphNode, + align_to?: LGraphNode ): void { alignNodes(Object.values(nodes), direction, align_to) LGraphCanvas.active_canvas.setDirty(true, true) @@ -892,19 +938,19 @@ export class LGraphCanvas implements CustomEventDispatcher options: IContextMenuOptions, event: MouseEvent, prev_menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): void { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { + new LiteGraph.ContextMenu(['Top', 'Bottom', 'Left', 'Right'], { event, callback: inner_clicked, - parentMenu: prev_menu, + parentMenu: prev_menu }) function inner_clicked(value: string) { alignNodes( Object.values(LGraphCanvas.active_canvas.selected_nodes), value.toLowerCase() as Direction, - node, + node ) LGraphCanvas.active_canvas.setDirty(true, true) } @@ -914,18 +960,18 @@ export class LGraphCanvas implements CustomEventDispatcher value: IContextMenuValue, options: IContextMenuOptions, event: MouseEvent, - prev_menu: ContextMenu, + prev_menu: ContextMenu ): void { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { + new LiteGraph.ContextMenu(['Top', 'Bottom', 'Left', 'Right'], { event, callback: inner_clicked, - parentMenu: prev_menu, + parentMenu: prev_menu }) function inner_clicked(value: string) { alignNodes( Object.values(LGraphCanvas.active_canvas.selected_nodes), - value.toLowerCase() as Direction, + value.toLowerCase() as Direction ) LGraphCanvas.active_canvas.setDirty(true, true) } @@ -935,17 +981,20 @@ export class LGraphCanvas implements CustomEventDispatcher value: IContextMenuValue, options: IContextMenuOptions, event: MouseEvent, - prev_menu: ContextMenu, + prev_menu: ContextMenu ): void { - new LiteGraph.ContextMenu(["Vertically", "Horizontally"], { + new LiteGraph.ContextMenu(['Vertically', 'Horizontally'], { event, callback: inner_clicked, - parentMenu: prev_menu, + parentMenu: prev_menu }) function inner_clicked(value: string) { const canvas = LGraphCanvas.active_canvas - distributeNodes(Object.values(canvas.selected_nodes), value === "Horizontally") + distributeNodes( + Object.values(canvas.selected_nodes), + value === 'Horizontally' + ) canvas.setDirty(true, true) } } @@ -955,17 +1004,17 @@ export class LGraphCanvas implements CustomEventDispatcher options: unknown, e: MouseEvent, prev_menu?: ContextMenu, - callback?: (node: LGraphNode | null) => void, + callback?: (node: LGraphNode | null) => void ): boolean | undefined { const canvas = LGraphCanvas.active_canvas const ref_window = canvas.getCanvasWindow() const { graph } = canvas if (!graph) return - inner_onMenuAdded("", prev_menu) + inner_onMenuAdded('', prev_menu) return false - type AddNodeMenu = Omit, "callback"> & { + type AddNodeMenu = Omit, 'callback'> & { callback: ( value: { value: string }, event: Event, @@ -974,12 +1023,15 @@ export class LGraphCanvas implements CustomEventDispatcher ) => void } - function inner_onMenuAdded(base_category: string, prev_menu?: ContextMenu): void { + function inner_onMenuAdded( + base_category: string, + prev_menu?: ContextMenu + ): void { if (!graph) return - const categories = LiteGraph - .getNodeTypesCategories(canvas.filter || graph.filter) - .filter(category => category.startsWith(base_category)) + const categories = LiteGraph.getNodeTypesCategories( + canvas.filter || graph.filter + ).filter((category) => category.startsWith(base_category)) const entries: AddNodeMenu[] = [] for (const category of categories) { @@ -987,18 +1039,20 @@ export class LGraphCanvas implements CustomEventDispatcher const base_category_regex = new RegExp(`^(${base_category})`) const category_name = category - .replace(base_category_regex, "") - .split("/", 1)[0] + .replace(base_category_regex, '') + .split('/', 1)[0] const category_path = - base_category === "" + base_category === '' ? `${category_name}/` : `${base_category}${category_name}/` let name = category_name // in case it has a namespace like "shader::math/rand" it hides the namespace - if (name.includes("::")) name = name.split("::", 2)[1] + if (name.includes('::')) name = name.split('::', 2)[1] - const index = entries.findIndex(entry => entry.value === category_path) + const index = entries.findIndex( + (entry) => entry.value === category_path + ) if (index === -1) { entries.push({ value: category_path, @@ -1006,14 +1060,14 @@ export class LGraphCanvas implements CustomEventDispatcher has_submenu: true, callback: function (value, event, mouseEvent, contextMenu) { inner_onMenuAdded(value.value, contextMenu) - }, + } }) } } const nodes = LiteGraph.getNodeTypesInCategory( base_category.slice(0, -1), - canvas.filter || graph.filter, + canvas.filter || graph.filter ) for (const node of nodes) { @@ -1030,23 +1084,30 @@ export class LGraphCanvas implements CustomEventDispatcher canvas.graph.beforeChange() const node = LiteGraph.createNode(value.value) if (node) { - if (!first_event) throw new TypeError("Context menu event was null. This should not occur in normal usage.") + if (!first_event) + throw new TypeError( + 'Context menu event was null. This should not occur in normal usage.' + ) node.pos = canvas.convertEventToCanvasOffset(first_event) canvas.graph.add(node) } else { - console.warn("Failed to create node of type:", value.value) + console.warn('Failed to create node of type:', value.value) } callback?.(node) canvas.graph.afterChange() - }, + } } entries.push(entry) } // @ts-expect-error Remove param ref_window - unused - new LiteGraph.ContextMenu(entries, { event: e, parentMenu: prev_menu }, ref_window) + new LiteGraph.ContextMenu( + entries, + { event: e, parentMenu: prev_menu }, + ref_window + ) } } @@ -1060,7 +1121,7 @@ export class LGraphCanvas implements CustomEventDispatcher _options: INodeOutputSlot[], e: MouseEvent, prev_menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): boolean | undefined { if (!node) return @@ -1068,8 +1129,15 @@ export class LGraphCanvas implements CustomEventDispatcher let entries: (IContextMenuValue | null)[] = [] - if (LiteGraph.do_add_triggers_slots && node.findOutputSlot("onExecuted") == -1) { - entries.push({ content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, { nameLocked: true }], className: "event" }) + if ( + LiteGraph.do_add_triggers_slots && + node.findOutputSlot('onExecuted') == -1 + ) { + entries.push({ + content: 'On Executed', + value: ['onExecuted', LiteGraph.EVENT, { nameLocked: true }], + className: 'event' + }) } // add callback for modifing the menu elements onMenuNodeOutputs const retEntries = node.onMenuNodeOutputs?.(entries) @@ -1077,17 +1145,19 @@ export class LGraphCanvas implements CustomEventDispatcher if (!entries.length) return - new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node, - }, - ) + new LiteGraph.ContextMenu(entries, { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + node + }) - function inner_clicked(this: ContextMenuDivElement, v: IContextMenuValue, e: any, prev: any) { + function inner_clicked( + this: ContextMenuDivElement, + v: IContextMenuValue, + e: any, + prev: any + ) { if (!node) return // TODO: This is a static method, so the below "that" appears broken. @@ -1097,8 +1167,7 @@ export class LGraphCanvas implements CustomEventDispatcher const value = v.value[1] - if (value && - (typeof value === "object" || Array.isArray(value))) { + if (value && (typeof value === 'object' || Array.isArray(value))) { // submenu why? const entries = [] for (const i in value) { @@ -1108,7 +1177,7 @@ export class LGraphCanvas implements CustomEventDispatcher event: e, callback: inner_clicked, parentMenu: prev_menu, - node, + node }) return false } @@ -1134,7 +1203,7 @@ export class LGraphCanvas implements CustomEventDispatcher options: unknown, e: MouseEvent, prev_menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): boolean | undefined { if (!node || !node.properties) return @@ -1143,20 +1212,19 @@ export class LGraphCanvas implements CustomEventDispatcher const entries: IContextMenuValue[] = [] for (const i in node.properties) { - value = node.properties[i] !== undefined ? node.properties[i] : " " - if (typeof value == "object") - value = JSON.stringify(value) + value = node.properties[i] !== undefined ? node.properties[i] : ' ' + if (typeof value == 'object') value = JSON.stringify(value) const info = node.getPropertyInfo(i) - if (info.type == "enum" || info.type == "combo") + if (info.type == 'enum' || info.type == 'combo') value = LGraphCanvas.getPropertyPrintableValue(value, info.values) // value could contain invalid html characters, clean that value = LGraphCanvas.decodeHTML(stringOrEmpty(value)) entries.push({ content: - `${info.label || i}` + - `${value}`, - value: i, + `${info.label || i}` + + `${value}`, + value: i }) } if (!entries.length) { @@ -1170,10 +1238,10 @@ export class LGraphCanvas implements CustomEventDispatcher callback: inner_clicked, parentMenu: prev_menu, allow_html: true, - node, + node }, // @ts-expect-error Unused - ref_window, + ref_window ) function inner_clicked(this: ContextMenuDivElement, v: { value: any }) { @@ -1181,7 +1249,7 @@ export class LGraphCanvas implements CustomEventDispatcher const rect = this.getBoundingClientRect() canvas.showEditPropertyValue(node, v.value, { - position: [rect.left, rect.top], + position: [rect.left, rect.top] }) } @@ -1190,7 +1258,7 @@ export class LGraphCanvas implements CustomEventDispatcher /** @deprecated */ static decodeHTML(str: string): string { - const e = document.createElement("div") + const e = document.createElement('div') e.textContent = str return e.innerHTML } @@ -1200,7 +1268,7 @@ export class LGraphCanvas implements CustomEventDispatcher options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): void { if (!node) return @@ -1209,7 +1277,10 @@ export class LGraphCanvas implements CustomEventDispatcher } const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + if ( + !canvas.selected_nodes || + Object.keys(canvas.selected_nodes).length <= 1 + ) { fApplyMultiNode(node) } else { for (const i in canvas.selected_nodes) { @@ -1222,46 +1293,50 @@ export class LGraphCanvas implements CustomEventDispatcher // TODO refactor :: this is used fot title but not for properties! static onShowPropertyEditor( - item: { property: keyof LGraphNode, type: string }, + item: { property: keyof LGraphNode; type: string }, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): void { - const property = item.property || "title" + const property = item.property || 'title' const value = node[property] - const title = document.createElement("span") - title.className = "name" + const title = document.createElement('span') + title.className = 'name' title.textContent = property - const input = document.createElement("input") - Object.assign(input, { type: "text", className: "value", autofocus: true }) + const input = document.createElement('input') + Object.assign(input, { type: 'text', className: 'value', autofocus: true }) - const button = document.createElement("button") - button.textContent = "OK" + const button = document.createElement('button') + button.textContent = 'OK' // TODO refactor :: use createDialog ? - const dialog = Object.assign(document.createElement("div"), { + const dialog = Object.assign(document.createElement('div'), { is_modified: false, - className: "graphdialog", - close: () => dialog.remove(), + className: 'graphdialog', + close: () => dialog.remove() }) dialog.append(title, input, button) input.value = String(value) - input.addEventListener("blur", function () { + input.addEventListener('blur', function () { this.focus() }) - input.addEventListener("keydown", (e: KeyboardEvent) => { + input.addEventListener('keydown', (e: KeyboardEvent) => { dialog.is_modified = true - if (e.key == "Escape") { + if (e.key == 'Escape') { // ESC dialog.close() - } else if (e.key == "Enter") { + } else if (e.key == 'Enter') { // save inner() - } else if (!e.target || !("localName" in e.target) || e.target.localName != "textarea") { + } else if ( + !e.target || + !('localName' in e.target) || + e.target.localName != 'textarea' + ) { return } e.preventDefault() @@ -1283,25 +1358,26 @@ export class LGraphCanvas implements CustomEventDispatcher dialog.style.top = `${canvasEl.height * 0.5 + offsety}px` } - button.addEventListener("click", inner) + button.addEventListener('click', inner) - if (canvasEl.parentNode == null) throw new TypeError("canvasEl.parentNode was null") + if (canvasEl.parentNode == null) + throw new TypeError('canvasEl.parentNode was null') canvasEl.parentNode.append(dialog) input.focus() let dialogCloseTimer: number - dialog.addEventListener("mouseleave", function () { + dialog.addEventListener('mouseleave', function () { if (LiteGraph.dialog_close_on_mouse_leave) { if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) { dialogCloseTimer = setTimeout( dialog.close, - LiteGraph.dialog_close_on_mouse_leave_delay, + LiteGraph.dialog_close_on_mouse_leave_delay ) } } }) - dialog.addEventListener("mouseenter", function () { + dialog.addEventListener('mouseenter', function () { if (LiteGraph.dialog_close_on_mouse_leave) { if (dialogCloseTimer) clearTimeout(dialogCloseTimer) } @@ -1312,9 +1388,9 @@ export class LGraphCanvas implements CustomEventDispatcher } function setValue(value: NodeProperty) { - if (item.type == "Number") { + if (item.type == 'Number') { value = Number(value) - } else if (item.type == "Boolean") { + } else if (item.type == 'Boolean') { value = Boolean(value) } // @ts-expect-error Requires refactor. @@ -1324,15 +1400,18 @@ export class LGraphCanvas implements CustomEventDispatcher } } - static getPropertyPrintableValue(value: unknown, values: unknown[] | object | undefined): string | undefined { + static getPropertyPrintableValue( + value: unknown, + values: unknown[] | object | undefined + ): string | undefined { if (!values) return String(value) if (Array.isArray(values)) { return String(value) } - if (typeof values === "object") { - let desc_value = "" + if (typeof values === 'object') { + let desc_value = '' for (const k in values) { // @ts-expect-error deprecated #578 if (values[k] != value) continue @@ -1349,7 +1428,7 @@ export class LGraphCanvas implements CustomEventDispatcher options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): void { if (!node.graph) throw new NullGraphError() @@ -1360,7 +1439,10 @@ export class LGraphCanvas implements CustomEventDispatcher } const graphcanvas = LGraphCanvas.active_canvas - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { fApplyMultiNode(node) } else { for (const i in graphcanvas.selected_nodes) { @@ -1376,7 +1458,7 @@ export class LGraphCanvas implements CustomEventDispatcher options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): void { if (!node.graph) throw new NullGraphError() @@ -1386,7 +1468,10 @@ export class LGraphCanvas implements CustomEventDispatcher } const graphcanvas = LGraphCanvas.active_canvas - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { fApplyMultiNode(node) } else { for (const i in graphcanvas.selected_nodes) { @@ -1401,12 +1486,14 @@ export class LGraphCanvas implements CustomEventDispatcher options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): boolean { - new LiteGraph.ContextMenu( - LiteGraph.NODE_MODES, - { event: e, callback: inner_clicked, parentMenu: menu, node }, - ) + new LiteGraph.ContextMenu(LiteGraph.NODE_MODES, { + event: e, + callback: inner_clicked, + parentMenu: menu, + node + }) function inner_clicked(v: string) { if (!node) return @@ -1422,7 +1509,10 @@ export class LGraphCanvas implements CustomEventDispatcher } const graphcanvas = LGraphCanvas.active_canvas - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { + if ( + !graphcanvas.selected_nodes || + Object.keys(graphcanvas.selected_nodes).length <= 1 + ) { fApplyMultiNode(node) } else { for (const i in graphcanvas.selected_nodes) { @@ -1440,22 +1530,28 @@ export class LGraphCanvas implements CustomEventDispatcher options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): boolean { - if (!node) throw "no node for color" + if (!node) throw 'no node for color' - const values: IContextMenuValue[] = [] + const values: IContextMenuValue< + string | null, + unknown, + { value: string | null } + >[] = [] values.push({ value: null, - content: "No color", + content: + "No color" }) for (const i in LGraphCanvas.node_colors) { const color = LGraphCanvas.node_colors[i] value = { value: i, - content: `${i}`, + content: + `${i}` } values.push(value) } @@ -1463,7 +1559,7 @@ export class LGraphCanvas implements CustomEventDispatcher event: e, callback: inner_clicked, parentMenu: menu, - node, + node }) function inner_clicked(v: IContextMenuValue) { @@ -1475,7 +1571,10 @@ export class LGraphCanvas implements CustomEventDispatcher } const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + if ( + !canvas.selected_nodes || + Object.keys(canvas.selected_nodes).length <= 1 + ) { fApplyColor(node) } else { for (const i in canvas.selected_nodes) { @@ -1489,22 +1588,25 @@ export class LGraphCanvas implements CustomEventDispatcher } static onMenuNodeShapes( - value: IContextMenuValue, - options: IContextMenuOptions, + value: IContextMenuValue<(typeof LiteGraph.VALID_SHAPES)[number]>, + options: IContextMenuOptions<(typeof LiteGraph.VALID_SHAPES)[number]>, e: MouseEvent, - menu?: ContextMenu, - node?: LGraphNode, + menu?: ContextMenu<(typeof LiteGraph.VALID_SHAPES)[number]>, + node?: LGraphNode ): boolean { - if (!node) throw "no node passed" + if (!node) throw 'no node passed' - new LiteGraph.ContextMenu(LiteGraph.VALID_SHAPES, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node, - }) + new LiteGraph.ContextMenu<(typeof LiteGraph.VALID_SHAPES)[number]>( + LiteGraph.VALID_SHAPES, + { + event: e, + callback: inner_clicked, + parentMenu: menu, + node + } + ) - function inner_clicked(v: typeof LiteGraph.VALID_SHAPES[number]) { + function inner_clicked(v: (typeof LiteGraph.VALID_SHAPES)[number]) { if (!node) return if (!node.graph) throw new NullGraphError() @@ -1515,7 +1617,10 @@ export class LGraphCanvas implements CustomEventDispatcher } const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + if ( + !canvas.selected_nodes || + Object.keys(canvas.selected_nodes).length <= 1 + ) { fApplyMultiNode(node) } else { for (const i in canvas.selected_nodes) { @@ -1539,7 +1644,7 @@ export class LGraphCanvas implements CustomEventDispatcher options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, - node: LGraphNode, + node: LGraphNode ): void { const { graph } = node if (!graph) throw new NullGraphError() @@ -1547,7 +1652,10 @@ export class LGraphCanvas implements CustomEventDispatcher const newSelected = new Set() - const fApplyMultiNode = function (node: LGraphNode, newNodes: Set): void { + const fApplyMultiNode = function ( + node: LGraphNode, + newNodes: Set + ): void { if (node.clonable === false) return const newnode = node.clone() @@ -1561,7 +1669,10 @@ export class LGraphCanvas implements CustomEventDispatcher } const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + if ( + !canvas.selected_nodes || + Object.keys(canvas.selected_nodes).length <= 1 + ) { fApplyMultiNode(node, newSelected) } else { for (const i in canvas.selected_nodes) { @@ -1630,7 +1741,7 @@ export class LGraphCanvas implements CustomEventDispatcher this.clear() newGraph.attachCanvas(this) - this.dispatch("litegraph:set-graph", { newGraph, oldGraph: graph }) + this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph }) this.#dirty() } @@ -1638,15 +1749,21 @@ export class LGraphCanvas implements CustomEventDispatcher const { graph } = this if (!graph) throw new NullGraphError() - const options = { bubbles: true, detail: { subgraph, closingGraph: graph }, cancelable: true } - const mayContinue = this.canvas.dispatchEvent(new CustomEvent("subgraph-opening", options)) + const options = { + bubbles: true, + detail: { subgraph, closingGraph: graph }, + cancelable: true + } + const mayContinue = this.canvas.dispatchEvent( + new CustomEvent('subgraph-opening', options) + ) if (!mayContinue) return this.clear() this.subgraph = subgraph this.setGraph(subgraph) - this.canvas.dispatchEvent(new CustomEvent("subgraph-opened", options)) + this.canvas.dispatchEvent(new CustomEvent('subgraph-opened', options)) } /** @@ -1663,11 +1780,12 @@ export class LGraphCanvas implements CustomEventDispatcher * @throws If {@link canvas} is an element ID that does not belong to a valid HTML canvas element */ #validateCanvas( - canvas: string | HTMLCanvasElement, + canvas: string | HTMLCanvasElement ): HTMLCanvasElement & { data?: LGraphCanvas } { - if (typeof canvas === "string") { + if (typeof canvas === 'string') { const el = document.getElementById(canvas) - if (!(el instanceof HTMLCanvasElement)) throw "Error validating LiteGraph canvas: Canvas element not found" + if (!(el instanceof HTMLCanvasElement)) + throw 'Error validating LiteGraph canvas: Canvas element not found' return el } return canvas @@ -1692,17 +1810,17 @@ export class LGraphCanvas implements CustomEventDispatcher if (!element) return // TODO: classList.add - element.className += " lgraphcanvas" + element.className += ' lgraphcanvas' element.data = this // Background canvas: To render objects behind nodes (background, links, groups) - this.bgcanvas = document.createElement("canvas") + this.bgcanvas = document.createElement('canvas') this.bgcanvas.width = this.canvas.width this.bgcanvas.height = this.canvas.height - const ctx = element.getContext?.("2d") + const ctx = element.getContext?.('2d') if (ctx == null) { - if (element.localName != "canvas") { + if (element.localName != 'canvas') { throw `Element supplied for LGraphCanvas must be a element, you passed a ${element.localName}` } throw "This browser doesn't support Canvas" @@ -1730,7 +1848,7 @@ export class LGraphCanvas implements CustomEventDispatcher */ bindEvents(): void { if (this._events_binded) { - console.warn("LGraphCanvas: events already bound") + console.warn('LGraphCanvas: events already bound') return } @@ -1745,26 +1863,26 @@ export class LGraphCanvas implements CustomEventDispatcher this._mouseout_callback = this.processMouseOut.bind(this) this._mousecancel_callback = this.processMouseCancel.bind(this) - canvas.addEventListener("pointerdown", this._mousedown_callback, true) - canvas.addEventListener("wheel", this._mousewheel_callback, false) + canvas.addEventListener('pointerdown', this._mousedown_callback, true) + canvas.addEventListener('wheel', this._mousewheel_callback, false) - canvas.addEventListener("pointerup", this._mouseup_callback, true) - canvas.addEventListener("pointermove", this._mousemove_callback) - canvas.addEventListener("pointerout", this._mouseout_callback) - canvas.addEventListener("pointercancel", this._mousecancel_callback, true) + canvas.addEventListener('pointerup', this._mouseup_callback, true) + canvas.addEventListener('pointermove', this._mousemove_callback) + canvas.addEventListener('pointerout', this._mouseout_callback) + canvas.addEventListener('pointercancel', this._mousecancel_callback, true) - canvas.addEventListener("contextmenu", this._doNothing) + canvas.addEventListener('contextmenu', this._doNothing) // Keyboard this._key_callback = this.processKey.bind(this) - canvas.addEventListener("keydown", this._key_callback, true) + canvas.addEventListener('keydown', this._key_callback, true) // keyup event must be bound on the document - document.addEventListener("keyup", this._key_callback, true) + document.addEventListener('keyup', this._key_callback, true) - canvas.addEventListener("dragover", this._doNothing, false) - canvas.addEventListener("dragend", this._doNothing, false) - canvas.addEventListener("dragenter", this._doReturnTrue, false) + canvas.addEventListener('dragover', this._doNothing, false) + canvas.addEventListener('dragend', this._doNothing, false) + canvas.addEventListener('dragenter', this._doReturnTrue, false) this._events_binded = true } @@ -1774,7 +1892,7 @@ export class LGraphCanvas implements CustomEventDispatcher */ unbindEvents(): void { if (!this._events_binded) { - console.warn("LGraphCanvas: no events bound") + console.warn('LGraphCanvas: no events bound') return } @@ -1783,16 +1901,16 @@ export class LGraphCanvas implements CustomEventDispatcher const { canvas } = this // Assertions: removing nullish is fine. - canvas.removeEventListener("pointercancel", this._mousecancel_callback!) - canvas.removeEventListener("pointerout", this._mouseout_callback!) - canvas.removeEventListener("pointermove", this._mousemove_callback!) - canvas.removeEventListener("pointerup", this._mouseup_callback!) - canvas.removeEventListener("pointerdown", this._mousedown_callback!) - canvas.removeEventListener("wheel", this._mousewheel_callback!) - canvas.removeEventListener("keydown", this._key_callback!) - document.removeEventListener("keyup", this._key_callback!) - canvas.removeEventListener("contextmenu", this._doNothing) - canvas.removeEventListener("dragenter", this._doReturnTrue) + canvas.removeEventListener('pointercancel', this._mousecancel_callback!) + canvas.removeEventListener('pointerout', this._mouseout_callback!) + canvas.removeEventListener('pointermove', this._mousemove_callback!) + canvas.removeEventListener('pointerup', this._mouseup_callback!) + canvas.removeEventListener('pointerdown', this._mousedown_callback!) + canvas.removeEventListener('wheel', this._mousewheel_callback!) + canvas.removeEventListener('keydown', this._key_callback!) + document.removeEventListener('keyup', this._key_callback!) + canvas.removeEventListener('contextmenu', this._doNothing) + canvas.removeEventListener('dragenter', this._doReturnTrue) this._mousedown_callback = undefined this._mousewheel_callback = undefined @@ -1823,7 +1941,7 @@ export class LGraphCanvas implements CustomEventDispatcher const { graph, linkConnector, pointer } = this if (!graph) throw new NullGraphError() - pointer.onDragEnd = upEvent => linkConnector.dropLinks(graph, upEvent) + pointer.onDragEnd = (upEvent) => linkConnector.dropLinks(graph, upEvent) pointer.finally = () => this.linkConnector.reset(true) } @@ -1860,7 +1978,8 @@ export class LGraphCanvas implements CustomEventDispatcher if (this.is_rendering) { if (this.#maximumFrameGap > 0) { // Manual FPS limit - const gap = this.#maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time) + const gap = + this.#maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time) setTimeout(renderFrame.bind(this), Math.max(1, gap)) } else { // FPS limited by refresh rate @@ -1941,7 +2060,13 @@ export class LGraphCanvas implements CustomEventDispatcher } processMouseDown(e: PointerEvent): void { - if (this.dragZoomEnabled && e.ctrlKey && e.shiftKey && !e.altKey && e.buttons) { + if ( + this.dragZoomEnabled && + e.ctrlKey && + e.shiftKey && + !e.altKey && + e.buttons + ) { this.#dragZoomStart = { pos: [e.x, e.y], scale: this.ds.scale } return } @@ -1964,7 +2089,8 @@ export class LGraphCanvas implements CustomEventDispatcher if (!is_inside) return - const node = graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes) ?? undefined + const node = + graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes) ?? undefined this.mouse[0] = x this.mouse[1] = y @@ -2007,13 +2133,17 @@ export class LGraphCanvas implements CustomEventDispatcher if (node) { this.processSelect(node, e, true) } else if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) { - // Reroutes - const reroute = graph.getRerouteOnPos(e.canvasX, e.canvasY, this.#visibleReroutes) + // Reroutes + const reroute = graph.getRerouteOnPos( + e.canvasX, + e.canvasY, + this.#visibleReroutes + ) if (reroute) { if (e.altKey) { pointer.onClick = (upEvent) => { if (upEvent.altKey) { - // Ensure deselected + // Ensure deselected if (reroute.selected) { this.deselect(reroute) this.onSelectionChange?.(this.selected_nodes) @@ -2041,8 +2171,8 @@ export class LGraphCanvas implements CustomEventDispatcher // this is to ensure to defocus(blur) if a text input element is on focus if ( !ref_window.document.activeElement || - (ref_window.document.activeElement.nodeName.toLowerCase() != "input" && - ref_window.document.activeElement.nodeName.toLowerCase() != "textarea") + (ref_window.document.activeElement.nodeName.toLowerCase() != 'input' && + ref_window.document.activeElement.nodeName.toLowerCase() != 'textarea') ) { e.preventDefault() } @@ -2084,26 +2214,34 @@ export class LGraphCanvas implements CustomEventDispatcher const ctrlOrMeta = e.ctrlKey || e.metaKey // Multi-select drag rectangle - if (ctrlOrMeta && !e.altKey && LiteGraph.canvasNavigationMode === "legacy") { + if ( + ctrlOrMeta && + !e.altKey && + LiteGraph.canvasNavigationMode === 'legacy' + ) { this.#setupNodeSelectionDrag(e, pointer, node) return } if (this.read_only) { - pointer.finally = () => this.dragging_canvas = false + pointer.finally = () => (this.dragging_canvas = false) this.dragging_canvas = true return } // clone node ALT dragging - if (LiteGraph.alt_drag_do_clone_nodes && e.altKey && !e.ctrlKey && node && this.allow_interaction) { + if ( + LiteGraph.alt_drag_do_clone_nodes && + e.altKey && + !e.ctrlKey && + node && + this.allow_interaction + ) { let newType = node.type if (node instanceof SubgraphNode) { - const cloned = node.subgraph - .clone() - .asSerialisable() + const cloned = node.subgraph.clone().asSerialisable() const subgraph = graph.createSubgraph(cloned) subgraph.configure(cloned) @@ -2123,9 +2261,9 @@ export class LGraphCanvas implements CustomEventDispatcher graph.add(cloned, false) this.#startDraggingItems(cloned, pointer) } - pointer.onDragEnd = e => this.#processDraggedItems(e) + pointer.onDragEnd = (e) => this.#processDraggedItems(e) } else { - // TODO: Check if before/after change are necessary here. + // TODO: Check if before/after change are necessary here. graph.beforeChange() graph.add(cloned, false) graph.afterChange() @@ -2147,13 +2285,17 @@ export class LGraphCanvas implements CustomEventDispatcher if (processSubgraphIONode(this, inputNode)) return if (processSubgraphIONode(this, outputNode)) return - function processSubgraphIONode(canvas: LGraphCanvas, ioNode: SubgraphInputNode | SubgraphOutputNode) { + function processSubgraphIONode( + canvas: LGraphCanvas, + ioNode: SubgraphInputNode | SubgraphOutputNode + ) { if (!ioNode.containsPoint([x, y])) return false ioNode.onPointerDown(e, pointer, linkConnector) pointer.onClick ??= () => canvas.processSelect(ioNode, e) - pointer.onDragStart ??= () => canvas.#startDraggingItems(ioNode, pointer, true) - pointer.onDragEnd ??= eUp => canvas.#processDraggedItems(eUp) + pointer.onDragStart ??= () => + canvas.#startDraggingItems(ioNode, pointer, true) + pointer.onDragEnd ??= (eUp) => canvas.#processDraggedItems(eUp) return true } } @@ -2167,8 +2309,9 @@ export class LGraphCanvas implements CustomEventDispatcher if (overReroute) { pointer.onClick = () => this.processSelect(reroute, e) if (!e.shiftKey) { - pointer.onDragStart = pointer => this.#startDraggingItems(reroute, pointer, true) - pointer.onDragEnd = e => this.#processDraggedItems(e) + pointer.onDragStart = (pointer) => + this.#startDraggingItems(reroute, pointer, true) + pointer.onDragEnd = (e) => this.#processDraggedItems(e) } } @@ -2213,16 +2356,17 @@ export class LGraphCanvas implements CustomEventDispatcher 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) + pointer.onDragStart = (pointer) => + this.#startDraggingItems(newReroute, pointer) + pointer.onDragEnd = (e) => this.#processDraggedItems(e) return } } else if (isInRectangle(x, y, centre[0] - 4, centre[1] - 4, 8, 8)) { this.ctx.lineWidth = lineWidth pointer.onClick = () => this.showLinkMenu(linkSegment, e) - pointer.onDragStart = () => this.dragging_canvas = true - pointer.finally = () => this.dragging_canvas = false + pointer.onDragStart = () => (this.dragging_canvas = true) + pointer.finally = () => (this.dragging_canvas = false) // clear tooltip this.over_link_center = undefined @@ -2243,14 +2387,14 @@ export class LGraphCanvas implements CustomEventDispatcher const offsetX = x - (b[0] + b[2]) const offsetY = y - (b[1] + b[3]) - pointer.onDragStart = () => this.resizingGroup = group + pointer.onDragStart = () => (this.resizingGroup = group) pointer.onDrag = (eMove) => { if (this.read_only) return // Resize only by the exact pointer movement const pos: Point = [ eMove.canvasX - group.pos[0] - offsetX, - eMove.canvasY - group.pos[1] - offsetY, + eMove.canvasY - group.pos[1] - offsetY ] // Unless snapping. if (this.#snapToGrid) snapPoint(pos, this.#snapToGrid) @@ -2258,7 +2402,7 @@ export class LGraphCanvas implements CustomEventDispatcher const resized = group.resize(pos[0], pos[1]) if (resized) this.dirty_bgcanvas = true } - pointer.finally = () => this.resizingGroup = null + pointer.finally = () => (this.resizingGroup = null) } else { const f = group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE const headerHeight = f * 1.4 @@ -2269,7 +2413,7 @@ export class LGraphCanvas implements CustomEventDispatcher group.pos[0], group.pos[1], group.size[0], - headerHeight, + headerHeight ) ) { // In title bar @@ -2278,15 +2422,15 @@ export class LGraphCanvas implements CustomEventDispatcher group.recomputeInsideNodes() this.#startDraggingItems(group, pointer, true) } - pointer.onDragEnd = e => this.#processDraggedItems(e) + pointer.onDragEnd = (e) => this.#processDraggedItems(e) } } pointer.onDoubleClick = () => { this.emitEvent({ - subType: "group-double-click", + subType: 'group-double-click', originalEvent: e, - group, + group }) } } else { @@ -2297,8 +2441,8 @@ export class LGraphCanvas implements CustomEventDispatcher e.preventDefault() } this.emitEvent({ - subType: "empty-double-click", - originalEvent: e, + subType: 'empty-double-click', + originalEvent: e }) } } @@ -2311,9 +2455,9 @@ export class LGraphCanvas implements CustomEventDispatcher this.allow_dragcanvas ) { // allow dragging canvas if canvas is not in standard, or read-only (pan mode in standard) - if (LiteGraph.canvasNavigationMode !== "standard" || this.read_only) { + if (LiteGraph.canvasNavigationMode !== 'standard' || this.read_only) { pointer.onClick = () => this.processSelect(null, e) - pointer.finally = () => this.dragging_canvas = false + pointer.finally = () => (this.dragging_canvas = false) this.dragging_canvas = true } else { this.#setupNodeSelectionDrag(e, pointer) @@ -2321,7 +2465,11 @@ export class LGraphCanvas implements CustomEventDispatcher } } - #setupNodeSelectionDrag(e: CanvasPointerEvent, pointer: CanvasPointer, node?: LGraphNode | undefined): void { + #setupNodeSelectionDrag( + e: CanvasPointerEvent, + pointer: CanvasPointer, + node?: LGraphNode | undefined + ): void { const dragRect = new Float32Array(4) dragRect[0] = e.canvasX @@ -2331,12 +2479,13 @@ export class LGraphCanvas implements CustomEventDispatcher pointer.onClick = (eUp) => { // Click, not drag - const clickedItem = node ?? this.#getPositionableOnPos(eUp.canvasX, eUp.canvasY) + const clickedItem = + node ?? this.#getPositionableOnPos(eUp.canvasX, eUp.canvasY) this.processSelect(clickedItem, eUp) } - pointer.onDragStart = () => this.dragging_rectangle = dragRect - pointer.onDragEnd = upEvent => this.#handleMultiSelect(upEvent, dragRect) - pointer.finally = () => this.dragging_rectangle = null + pointer.onDragStart = () => (this.dragging_rectangle = dragRect) + pointer.onDragEnd = (upEvent) => this.#handleMultiSelect(upEvent, dragRect) + pointer.finally = () => (this.dragging_rectangle = null) } /** @@ -2348,7 +2497,7 @@ export class LGraphCanvas implements CustomEventDispatcher #processNodeClick( e: CanvasPointerEvent, ctrlOrMeta: boolean, - node: LGraphNode, + node: LGraphNode ): void { const { pointer, graph, linkConnector } = this if (!graph) throw new NullGraphError() @@ -2379,7 +2528,10 @@ export class LGraphCanvas implements CustomEventDispatcher const link_pos = node.getOutputPos(i) if (isInRectangle(x, y, link_pos[0] - 15, link_pos[1] - 10, 30, 20)) { // Drag multiple output links - if (e.shiftKey && (output.links?.length || output._floatingLinks?.size)) { + if ( + e.shiftKey && + (output.links?.length || output._floatingLinks?.size) + ) { linkConnector.moveOutputLink(graph, output) this.#linkConnectorDrop() return @@ -2412,15 +2564,17 @@ export class LGraphCanvas implements CustomEventDispatcher if (inputs) { for (const [i, input] of inputs.entries()) { const link_pos = node.getInputPos(i) - const isInSlot = input instanceof NodeInputSlot - ? isInRect(x, y, input.boundingRect) - : isInRectangle(x, y, link_pos[0] - 15, link_pos[1] - 10, 30, 20) + const isInSlot = + input instanceof NodeInputSlot + ? isInRect(x, y, input.boundingRect) + : isInRectangle(x, y, link_pos[0] - 15, link_pos[1] - 10, 30, 20) if (isInSlot) { pointer.onDoubleClick = () => node.onInputDblClick?.(i, e) pointer.onClick = () => node.onInputClick?.(i, e) - const shouldBreakLink = LiteGraph.ctrl_alt_click_do_break_link && + const shouldBreakLink = + LiteGraph.ctrl_alt_click_do_break_link && ctrlOrMeta && e.altKey && !e.shiftKey @@ -2470,9 +2624,9 @@ export class LGraphCanvas implements CustomEventDispatcher node.onDblClick?.(e, pos, this) this.emitEvent({ - subType: "node-double-click", + subType: 'node-double-click', originalEvent: e, - node, + node }) this.processNodeDblClicked(node) } @@ -2492,7 +2646,12 @@ export class LGraphCanvas implements CustomEventDispatcher const resizeDirection = node.findResizeDirection(x, y) if (resizeDirection) { pointer.resizeDirection = resizeDirection - const startBounds = new Rectangle(node.pos[0], node.pos[1], node.size[0], node.size[1]) + const startBounds = new Rectangle( + node.pos[0], + node.pos[1], + node.size[0], + node.size[1] + ) pointer.onDragStart = () => { graph.beforeChange() @@ -2505,45 +2664,53 @@ export class LGraphCanvas implements CustomEventDispatcher const deltaX = eMove.canvasX - x const deltaY = eMove.canvasY - y - const newBounds = new Rectangle(startBounds.x, startBounds.y, startBounds.width, startBounds.height) + const newBounds = new Rectangle( + startBounds.x, + startBounds.y, + startBounds.width, + startBounds.height + ) // Handle resize based on the direction switch (resizeDirection) { - case "NE": // North-East (top-right) - newBounds.y = startBounds.y + deltaY - newBounds.width = startBounds.width + deltaX - newBounds.height = startBounds.height - deltaY - break - case "SE": // South-East (bottom-right) - newBounds.width = startBounds.width + deltaX - newBounds.height = startBounds.height + deltaY - break - case "SW": // South-West (bottom-left) - newBounds.x = startBounds.x + deltaX - newBounds.width = startBounds.width - deltaX - newBounds.height = startBounds.height + deltaY - break - case "NW": // North-West (top-left) - newBounds.x = startBounds.x + deltaX - newBounds.y = startBounds.y + deltaY - newBounds.width = startBounds.width - deltaX - newBounds.height = startBounds.height - deltaY - break + case 'NE': // North-East (top-right) + newBounds.y = startBounds.y + deltaY + newBounds.width = startBounds.width + deltaX + newBounds.height = startBounds.height - deltaY + break + case 'SE': // South-East (bottom-right) + newBounds.width = startBounds.width + deltaX + newBounds.height = startBounds.height + deltaY + break + case 'SW': // South-West (bottom-left) + newBounds.x = startBounds.x + deltaX + newBounds.width = startBounds.width - deltaX + newBounds.height = startBounds.height + deltaY + break + case 'NW': // North-West (top-left) + newBounds.x = startBounds.x + deltaX + newBounds.y = startBounds.y + deltaY + newBounds.width = startBounds.width - deltaX + newBounds.height = startBounds.height - deltaY + break } // Apply snapping to position changes if (this.#snapToGrid) { - if (resizeDirection.includes("N") || resizeDirection.includes("W")) { + if ( + resizeDirection.includes('N') || + resizeDirection.includes('W') + ) { const originalX = newBounds.x const originalY = newBounds.y snapPoint(newBounds.pos, this.#snapToGrid) // Adjust size to compensate for snapped position - if (resizeDirection.includes("N")) { + if (resizeDirection.includes('N')) { newBounds.height += originalY - newBounds.y } - if (resizeDirection.includes("W")) { + if (resizeDirection.includes('W')) { newBounds.width += originalX - newBounds.x } } @@ -2557,14 +2724,14 @@ export class LGraphCanvas implements CustomEventDispatcher const min = node.computeSize() if (newBounds.width < min[0]) { // If resizing from left, adjust position to maintain right edge - if (resizeDirection.includes("W")) { + if (resizeDirection.includes('W')) { newBounds.x = startBounds.x + startBounds.width - min[0] } newBounds.width = min[0] } if (newBounds.height < min[1]) { // If resizing from top, adjust position to maintain bottom edge - if (resizeDirection.includes("N")) { + if (resizeDirection.includes('N')) { newBounds.y = startBounds.y + startBounds.height - min[1] } newBounds.height = min[1] @@ -2592,18 +2759,23 @@ export class LGraphCanvas implements CustomEventDispatcher } // Drag node - pointer.onDragStart = pointer => this.#startDraggingItems(node, pointer, true) - pointer.onDragEnd = e => this.#processDraggedItems(e) + pointer.onDragStart = (pointer) => + this.#startDraggingItems(node, pointer, true) + pointer.onDragEnd = (e) => this.#processDraggedItems(e) } this.dirty_canvas = true } - #processWidgetClick(e: CanvasPointerEvent, node: LGraphNode, widget: IBaseWidget) { + #processWidgetClick( + e: CanvasPointerEvent, + node: LGraphNode, + widget: IBaseWidget + ) { const { pointer } = this // Custom widget - CanvasPointer - if (typeof widget.onPointerDown === "function") { + if (typeof widget.onPointerDown === 'function') { const handled = widget.onPointerDown(pointer, node, this) if (handled) return } @@ -2616,16 +2788,18 @@ export class LGraphCanvas implements CustomEventDispatcher const widgetInstance = toConcreteWidget(widget, node, false) if (widgetInstance) { - pointer.onClick = () => widgetInstance.onClick({ - e, - node, - canvas: this, - }) - pointer.onDrag = eMove => widgetInstance.onDrag?.({ - e: eMove, - node, - canvas: this, - }) + pointer.onClick = () => + widgetInstance.onClick({ + e, + node, + canvas: this + }) + pointer.onDrag = (eMove) => + widgetInstance.onDrag?.({ + e: eMove, + node, + canvas: this + }) } else if (widget.mouse) { const result = widget.mouse(e, [x, y], node) if (result != null) this.dirty_canvas = result @@ -2678,7 +2852,16 @@ export class LGraphCanvas implements CustomEventDispatcher if (outputs) { for (const [i, output] of outputs.entries()) { const link_pos = node.getOutputPos(i) - if (isInRectangle(e.canvasX, e.canvasY, link_pos[0] - 15, link_pos[1] - 10, 30, 20)) { + if ( + isInRectangle( + e.canvasX, + e.canvasY, + link_pos[0] - 15, + link_pos[1] - 10, + 30, + 20 + ) + ) { mClikSlot = output mClikSlot_index = i mClikSlot_isOut = true @@ -2691,7 +2874,16 @@ export class LGraphCanvas implements CustomEventDispatcher if (inputs) { for (const [i, input] of inputs.entries()) { const link_pos = node.getInputPos(i) - if (isInRectangle(e.canvasX, e.canvasY, link_pos[0] - 15, link_pos[1] - 10, 30, 20)) { + if ( + isInRectangle( + e.canvasX, + e.canvasY, + link_pos[0] - 15, + link_pos[1] - 10, + 30, + 20 + ) + ) { mClikSlot = input mClikSlot_index = i mClikSlot_isOut = false @@ -2704,7 +2896,7 @@ export class LGraphCanvas implements CustomEventDispatcher const alphaPosY = 0.5 - (mClikSlot_index + 1) / - (mClikSlot_isOut ? outputs.length : inputs.length) + (mClikSlot_isOut ? outputs.length : inputs.length) const node_bounding = node.getBounding() // estimate a position: this is a bad semi-bad-working mess .. REFACTOR with // a correct autoplacement that knows about the others slots and nodes @@ -2712,26 +2904,27 @@ export class LGraphCanvas implements CustomEventDispatcher !mClikSlot_isOut ? node_bounding[0] : node_bounding[0] + node_bounding[2], - e.canvasY - 80, + e.canvasY - 80 ] - pointer.onClick = () => this.createDefaultNodeForSlot({ - nodeFrom: !mClikSlot_isOut ? null : node, - slotFrom: !mClikSlot_isOut ? null : mClikSlot_index, - nodeTo: !mClikSlot_isOut ? node : null, - slotTo: !mClikSlot_isOut ? mClikSlot_index : null, - position: posRef, - nodeType: "AUTO", - posAdd: [!mClikSlot_isOut ? -30 : 30, -alphaPosY * 130], - posSizeFix: [!mClikSlot_isOut ? -1 : 0, 0], - }) + pointer.onClick = () => + this.createDefaultNodeForSlot({ + nodeFrom: !mClikSlot_isOut ? null : node, + slotFrom: !mClikSlot_isOut ? null : mClikSlot_index, + nodeTo: !mClikSlot_isOut ? node : null, + slotTo: !mClikSlot_isOut ? mClikSlot_index : null, + position: posRef, + nodeType: 'AUTO', + posAdd: [!mClikSlot_isOut ? -30 : 30, -alphaPosY * 130], + posSizeFix: [!mClikSlot_isOut ? -1 : 0, 0] + }) } } // Drag canvas using middle mouse button if (this.allow_dragcanvas) { - pointer.onDragStart = () => this.dragging_canvas = true - pointer.finally = () => this.dragging_canvas = false + pointer.onDragStart = () => (this.dragging_canvas = true) + pointer.finally = () => (this.dragging_canvas = false) } } @@ -2743,7 +2936,7 @@ export class LGraphCanvas implements CustomEventDispatcher } const start = this.#dragZoomStart - if (!start) throw new TypeError("Drag-zoom state object was null") + if (!start) throw new TypeError('Drag-zoom state object was null') if (!this.graph) throw new NullGraphError() // calculate delta @@ -2760,7 +2953,12 @@ export class LGraphCanvas implements CustomEventDispatcher * Called when a mouse move event has to be processed */ processMouseMove(e: PointerEvent): void { - if (this.dragZoomEnabled && e.ctrlKey && e.shiftKey && this.#dragZoomStart) { + if ( + this.dragZoomEnabled && + e.ctrlKey && + e.shiftKey && + this.#dragZoomStart + ) { this.#processDragZoom(e) return } @@ -2777,10 +2975,7 @@ export class LGraphCanvas implements CustomEventDispatcher const mouse: ReadOnlyPoint = [e.clientX, e.clientY] this.mouse[0] = mouse[0] this.mouse[1] = mouse[1] - const delta = [ - mouse[0] - this.last_mouse[0], - mouse[1] - this.last_mouse[1], - ] + const delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]] this.last_mouse = mouse const { canvasX: x, canvasY: y } = e this.graph_mouse[0] = x @@ -2815,11 +3010,7 @@ export class LGraphCanvas implements CustomEventDispatcher } // get node over - const node = graph.getNodeOnPos( - x, - y, - this.visible_nodes, - ) + const node = graph.getNodeOnPos(x, y, this.visible_nodes) const dragRect = this.dragging_rectangle if (dragRect) { @@ -2829,7 +3020,7 @@ export class LGraphCanvas implements CustomEventDispatcher } else if (resizingGroup) { // Resizing a group underPointer |= CanvasItem.Group - pointer.resizeDirection = "SE" + pointer.resizeDirection = 'SE' } else if (this.dragging_canvas) { this.ds.offset[0] += delta[0] / this.ds.scale this.ds.offset[1] += delta[1] / this.ds.scale @@ -2896,7 +3087,7 @@ export class LGraphCanvas implements CustomEventDispatcher if (!firstLink || !linkConnector.isNodeValidDrop(node)) { // No link, or none of the dragged links may be dropped here - } else if (linkConnector.state.connectingTo === "input") { + } else if (linkConnector.state.connectingTo === 'input') { if (overWidget) { // Check widgets first - inputId is only valid if over the input socket const slot = node.getSlotFromWidget(overWidget) @@ -2911,7 +3102,7 @@ export class LGraphCanvas implements CustomEventDispatcher // Not over a valid widget - treat drop on invalid widget same as node background if (!linkConnector.overWidget) { if (inputId === -1 && outputId === -1) { - // Node background / title under the pointer + // Node background / title under the pointer const result = node.findInputByType(firstLink.fromSlot.type) if (result) { highlightInput = result.slot @@ -2920,7 +3111,10 @@ export class LGraphCanvas implements CustomEventDispatcher } else if ( inputId != -1 && node.inputs[inputId] && - LiteGraph.isValidConnection(firstLink.fromSlot.type, node.inputs[inputId].type) + LiteGraph.isValidConnection( + firstLink.fromSlot.type, + node.inputs[inputId].type + ) ) { highlightPos = pos // XXX CHECK THIS @@ -2932,7 +3126,7 @@ export class LGraphCanvas implements CustomEventDispatcher if (widget) linkConnector.overWidget = widget } } - } else if (linkConnector.state.connectingTo === "output") { + } else if (linkConnector.state.connectingTo === 'output') { // Connecting from an input to an output if (inputId === -1 && outputId === -1) { const result = node.findOutputByType(firstLink.fromSlot.type) @@ -2944,7 +3138,10 @@ export class LGraphCanvas implements CustomEventDispatcher if ( outputId != -1 && node.outputs[outputId] && - LiteGraph.isValidConnection(firstLink.fromSlot.type, node.outputs[outputId].type) + LiteGraph.isValidConnection( + firstLink.fromSlot.type, + node.outputs[outputId].type + ) ) { highlightPos = pos } @@ -2986,7 +3183,7 @@ export class LGraphCanvas implements CustomEventDispatcher !this.read_only && group.isInResize(x, y) ) { - pointer.resizeDirection = "SE" + pointer.resizeDirection = 'SE' } else { pointer.resizeDirection &&= undefined } @@ -2999,9 +3196,9 @@ export class LGraphCanvas implements CustomEventDispatcher e, [ x - this.node_capturing_input.pos[0], - y - this.node_capturing_input.pos[1], + y - this.node_capturing_input.pos[1] ], - this, + this ) } @@ -3052,7 +3249,7 @@ export class LGraphCanvas implements CustomEventDispatcher this._highlight_pos = reroute.pos } - return underPointer |= CanvasItem.RerouteSlot + return (underPointer |= CanvasItem.RerouteSlot) } } } @@ -3070,7 +3267,11 @@ export class LGraphCanvas implements CustomEventDispatcher * @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(item: Positionable, pointer: CanvasPointer, sticky = false): void { + #startDraggingItems( + item: Positionable, + pointer: CanvasPointer, + sticky = false + ): void { this.emitBeforeChange() this.graph?.beforeChange() // Ensure that dragging is properly cleaned up, on success or failure. @@ -3153,10 +3354,14 @@ export class LGraphCanvas implements CustomEventDispatcher this.dirty_canvas = true // @ts-expect-error Unused param - this.node_over?.onMouseUp?.(e, [x - this.node_over.pos[0], y - this.node_over.pos[1]], this) + this.node_over?.onMouseUp?.( + e, + [x - this.node_over.pos[0], y - this.node_over.pos[1]], + this + ) this.node_capturing_input?.onMouseUp?.(e, [ x - this.node_capturing_input.pos[0], - y - this.node_capturing_input.pos[1], + y - this.node_capturing_input.pos[1] ]) } } else if (e.button === 1) { @@ -3189,7 +3394,7 @@ export class LGraphCanvas implements CustomEventDispatcher } processMouseCancel(): void { - console.warn("Pointer cancel!") + console.warn('Pointer cancel!') this.pointer.reset() } @@ -3210,16 +3415,19 @@ export class LGraphCanvas implements CustomEventDispatcher let { scale } = this.ds - if (LiteGraph.canvasNavigationMode === "legacy" || (LiteGraph.canvasNavigationMode === "standard" && e.ctrlKey)) { + if ( + LiteGraph.canvasNavigationMode === 'legacy' || + (LiteGraph.canvasNavigationMode === 'standard' && e.ctrlKey) + ) { if (delta > 0) { scale *= this.zoom_speed } else if (delta < 0) { - scale *= 1 / (this.zoom_speed) + scale *= 1 / this.zoom_speed } this.ds.changeScale(scale, [e.clientX, e.clientY]) } else if ( LiteGraph.macTrackpadGestures && - (!LiteGraph.macGesturesRequireMac || navigator.userAgent.includes("Mac")) + (!LiteGraph.macGesturesRequireMac || navigator.userAgent.includes('Mac')) ) { if (e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) { if (e.deltaY > 0) { @@ -3246,7 +3454,9 @@ export class LGraphCanvas implements CustomEventDispatcher } #noItemsSelected(): void { - const event = new CustomEvent("litegraph:no-items-selected", { bubbles: true }) + const event = new CustomEvent('litegraph:no-items-selected', { + bubbles: true + }) this.canvas.dispatchEvent(event) } @@ -3261,11 +3471,11 @@ export class LGraphCanvas implements CustomEventDispatcher let block_default = false // @ts-expect-error - if (e.target.localName == "input") return + if (e.target.localName == 'input') return - if (e.type == "keydown") { + if (e.type == 'keydown') { // TODO: Switch - if (e.key === " ") { + if (e.key === ' ') { // space this.read_only = true if (this._previously_dragging_canvas === null) { @@ -3273,7 +3483,7 @@ export class LGraphCanvas implements CustomEventDispatcher } this.dragging_canvas = this.pointer.isDown block_default = true - } else if (e.key === "Escape") { + } else if (e.key === 'Escape') { // esc if (this.linkConnector.isConnecting) { this.linkConnector.reset() @@ -3296,10 +3506,10 @@ export class LGraphCanvas implements CustomEventDispatcher } else if (e.keyCode === 86 && (e.metaKey || e.ctrlKey)) { // paste this.pasteFromClipboard({ connectInputs: e.shiftKey }) - } else if (e.key === "Delete" || e.key === "Backspace") { + } else if (e.key === 'Delete' || e.key === 'Backspace') { // delete or backspace // @ts-expect-error - if (e.target.localName != "input" && e.target.localName != "textarea") { + if (e.target.localName != 'input' && e.target.localName != 'textarea') { if (this.selectedItems.size === 0) { this.#noItemsSelected() return @@ -3314,11 +3524,12 @@ export class LGraphCanvas implements CustomEventDispatcher for (const node of Object.values(this.selected_nodes)) { node.onKeyDown?.(e) } - } else if (e.type == "keyup") { - if (e.key === " ") { + } else if (e.type == 'keyup') { + if (e.key === ' ') { // space this.read_only = false - this.dragging_canvas = (this._previously_dragging_canvas ?? false) && this.pointer.isDown + this.dragging_canvas = + (this._previously_dragging_canvas ?? false) && this.pointer.isDown this._previously_dragging_canvas = null } @@ -3347,7 +3558,7 @@ export class LGraphCanvas implements CustomEventDispatcher groups: [], reroutes: [], links: [], - subgraphs: [], + subgraphs: [] } const subgraphs = new Set() @@ -3390,54 +3601,55 @@ export class LGraphCanvas implements CustomEventDispatcher // Add unique subgraph entries // TODO: Must find all nested subgraphs for (const subgraph of subgraphs) { - const cloned = subgraph - .clone(true) - .asSerialisable() + const cloned = subgraph.clone(true).asSerialisable() serialisable.subgraphs.push(cloned) } localStorage.setItem( - "litegrapheditor_clipboard", - JSON.stringify(serialisable), + 'litegrapheditor_clipboard', + JSON.stringify(serialisable) ) } - emitEvent(detail: LGraphCanvasEventMap["litegraph:canvas"]): void { + emitEvent(detail: LGraphCanvasEventMap['litegraph:canvas']): void { this.canvas.dispatchEvent( - new CustomEvent("litegraph:canvas", { + new CustomEvent('litegraph:canvas', { bubbles: true, - detail, - }), + detail + }) ) } /** @todo Refactor to where it belongs - e.g. Deleting / creating nodes is not actually canvas event. */ emitBeforeChange(): void { this.emitEvent({ - subType: "before-change", + subType: 'before-change' }) } /** @todo See {@link emitBeforeChange} */ emitAfterChange(): void { this.emitEvent({ - subType: "after-change", + subType: 'after-change' }) } /** * Pastes the items from the canvas "clipbaord" - a local storage variable. */ - _pasteFromClipboard(options: IPasteFromClipboardOptions = {}): ClipboardPasteResult | undefined { - const { - connectInputs = false, - position = this.graph_mouse, - } = options + _pasteFromClipboard( + options: IPasteFromClipboardOptions = {} + ): ClipboardPasteResult | undefined { + const { connectInputs = false, position = this.graph_mouse } = options // if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior - if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && connectInputs) return + if ( + !LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && + connectInputs + ) + return - const data = localStorage.getItem("litegrapheditor_clipboard") + const data = localStorage.getItem('litegrapheditor_clipboard') if (!data) return const { graph } = this @@ -3456,7 +3668,10 @@ export class LGraphCanvas implements CustomEventDispatcher let offsetX = Infinity let offsetY = Infinity for (const item of [...parsed.nodes, ...parsed.reroutes]) { - if (item.pos == null) throw new TypeError("Invalid node encounterd on paste. `pos` was null.") + if (item.pos == null) + throw new TypeError( + 'Invalid node encounterd on paste. `pos` was null.' + ) if (item.pos[0] < offsetX) offsetX = item.pos[0] if (item.pos[1] < offsetY) offsetY = item.pos[1] @@ -3475,7 +3690,7 @@ export class LGraphCanvas implements CustomEventDispatcher nodes: new Map(), links: new Map(), reroutes: new Map(), - subgraphs: new Map(), + subgraphs: new Map() } const { created, nodes, links, reroutes } = results @@ -3545,10 +3760,14 @@ export class LGraphCanvas implements CustomEventDispatcher // Find the copied node / reroute ID let outNode: LGraphNode | null | undefined = nodes.get(info.origin_id) let afterRerouteId: number | undefined - if (info.parentId != null) afterRerouteId = reroutes.get(info.parentId)?.id + if (info.parentId != null) + afterRerouteId = reroutes.get(info.parentId)?.id // If it wasn't copied, use the original graph value - if (connectInputs && LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs) { + if ( + connectInputs && + LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs + ) { outNode ??= graph.getNodeById(info.origin_id) afterRerouteId ??= info.parentId } @@ -3559,7 +3778,7 @@ export class LGraphCanvas implements CustomEventDispatcher info.origin_slot, inNode, info.target_slot, - afterRerouteId, + afterRerouteId ) if (link) links.set(info.id, link) } @@ -3567,7 +3786,7 @@ export class LGraphCanvas implements CustomEventDispatcher // Remap linkIds for (const reroute of reroutes.values()) { - const ids = [...reroute.linkIds].map(x => links.get(x)?.id ?? x) + const ids = [...reroute.linkIds].map((x) => links.get(x)?.id ?? x) reroute.update(reroute.parentId, undefined, ids, reroute.floating) // Remove any invalid items @@ -3690,7 +3909,7 @@ export class LGraphCanvas implements CustomEventDispatcher processSelect( item: TPositionable | null | undefined, e: CanvasPointerEvent | undefined, - sticky: boolean = false, + sticky: boolean = false ): void { const addModifier = e?.shiftKey const subtractModifier = e != null && (e.metaKey || e.ctrlKey) @@ -3717,7 +3936,9 @@ export class LGraphCanvas implements CustomEventDispatcher * Selects a {@link Positionable} item. * @param item The canvas item to add to the selection. */ - select(item: TPositionable): void { + select( + item: TPositionable + ): void { if (item.selected && this.selectedItems.has(item)) return item.selected = true @@ -3739,7 +3960,7 @@ export class LGraphCanvas implements CustomEventDispatcher } } if (item.outputs) { - for (const id of item.outputs.flatMap(x => x.links)) { + for (const id of item.outputs.flatMap((x) => x.links)) { if (id == null) continue this.highlighted_links[id] = true } @@ -3750,7 +3971,9 @@ export class LGraphCanvas implements CustomEventDispatcher * Deselects a {@link Positionable} item. * @param item The canvas item to remove from the selection. */ - deselect(item: TPositionable): void { + deselect( + item: TPositionable + ): void { if (!item.selected && !this.selectedItems.has(item)) return item.selected = false @@ -3780,7 +4003,7 @@ export class LGraphCanvas implements CustomEventDispatcher } } if (item.outputs) { - for (const id of item.outputs.flatMap(x => x.links)) { + for (const id of item.outputs.flatMap((x) => x.links)) { if (id == null) continue const node = LLink.getTargetNode(graph, id) @@ -3796,7 +4019,7 @@ export class LGraphCanvas implements CustomEventDispatcher this.processSelect( item, e, - e && (e.shiftKey || e.metaKey || e.ctrlKey || this.multi_select), + e && (e.shiftKey || e.metaKey || e.ctrlKey || this.multi_select) ) } @@ -3824,7 +4047,10 @@ export class LGraphCanvas implements CustomEventDispatcher * @param items Items to select - if falsy, all items on the canvas will be selected * @param add_to_current_selection If set, the items will be added to the current selection instead of replacing it */ - selectItems(items?: Positionable[], add_to_current_selection?: boolean): void { + selectItems( + items?: Positionable[], + add_to_current_selection?: boolean + ): void { const itemsToSelect = items ?? this.positionableItems if (!add_to_current_selection) this.deselectAll() for (const item of itemsToSelect) this.select(item) @@ -3870,7 +4096,8 @@ export class LGraphCanvas implements CustomEventDispatcher this.setDirty(true) // Legacy code - const oldNode = keepSelected?.id == null ? null : this.selected_nodes[keepSelected.id] + const oldNode = + keepSelected?.id == null ? null : this.selected_nodes[keepSelected.id] this.selected_nodes = {} this.current_node = null this.highlighted_links = {} @@ -3887,7 +4114,7 @@ export class LGraphCanvas implements CustomEventDispatcher } } if (keepSelected.outputs) { - for (const id of keepSelected.outputs.flatMap(x => x.links)) { + for (const id of keepSelected.outputs.flatMap((x) => x.links)) { if (id == null) continue this.highlighted_links[id] = true } @@ -3968,7 +4195,7 @@ export class LGraphCanvas implements CustomEventDispatcher * adds some useful properties to a mouse event, like the position in graph coordinates */ adjustMouseEvent( - e: T & Partial, + e: T & Partial ): asserts e is T & CanvasPointerEvent { let clientX_rel = e.clientX let clientY_rel = e.clientY @@ -4028,7 +4255,7 @@ export class LGraphCanvas implements CustomEventDispatcher // TODO: -> this.ds.convertCanvasToOffset return this.convertCanvasToOffset([ e.clientX - rect.left, - e.clientY - rect.top, + e.clientY - rect.top ]) } @@ -4095,7 +4322,8 @@ export class LGraphCanvas implements CustomEventDispatcher * renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) */ draw(force_canvas?: boolean, force_bgcanvas?: boolean): void { - if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) return + if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) + return // fps counting const now = LiteGraph.getTime() @@ -4108,7 +4336,9 @@ export class LGraphCanvas implements CustomEventDispatcher if (this.dirty_canvas || force_canvas) { this.computeVisibleNodes(undefined, this.visible_nodes) // Update visible node IDs - this.#visible_node_ids = new Set(this.visible_nodes.map(node => node.id)) + this.#visible_node_ids = new Set( + this.visible_nodes.map((node) => node.id) + ) // Arrange subgraph IO nodes const { subgraph } = this @@ -4160,9 +4390,10 @@ export class LGraphCanvas implements CustomEventDispatcher } // TODO: Set snapping value when changed instead of once per frame - this.#snapToGrid = this.#shiftDown || LiteGraph.alwaysSnapToGrid - ? this.graph?.getSnapToGridSize() - : undefined + this.#snapToGrid = + this.#shiftDown || LiteGraph.alwaysSnapToGrid + ? this.graph?.getSnapToGridSize() + : undefined // clear // canvas.width = canvas.width; @@ -4181,7 +4412,7 @@ export class LGraphCanvas implements CustomEventDispatcher 0, 0, this.bgcanvas.width / scale, - this.bgcanvas.height / scale, + this.bgcanvas.height / scale ) } @@ -4219,7 +4450,12 @@ export class LGraphCanvas implements CustomEventDispatcher } // Draw subgraph IO nodes - this.subgraph?.draw(ctx, this.colourGetter, this.linkConnector.renderLinks[0]?.fromSlot, this.editor_alpha) + this.subgraph?.draw( + ctx, + this.colourGetter, + this.linkConnector.renderLinks[0]?.fromSlot, + this.editor_alpha + ) // on top (debug) if (this.render_execution_order) { @@ -4238,13 +4474,19 @@ export class LGraphCanvas implements CustomEventDispatcher ctx.lineWidth = this.connections_width for (const renderLink of renderLinks) { - const { fromSlot, fromPos: pos, fromDirection, dragDirection } = renderLink + const { + fromSlot, + fromPos: pos, + fromDirection, + dragDirection + } = renderLink const connShape = fromSlot.shape const connType = fromSlot.type - const colour = connType === LiteGraph.EVENT - ? LiteGraph.EVENT_LINK_COLOR - : LiteGraph.CONNECTING_LINK_COLOR + const colour = + connType === LiteGraph.EVENT + ? LiteGraph.EVENT_LINK_COLOR + : LiteGraph.CONNECTING_LINK_COLOR // the connection being dragged by the mouse this.renderLink( @@ -4256,7 +4498,7 @@ export class LGraphCanvas implements CustomEventDispatcher null, colour, fromDirection, - dragDirection, + dragDirection ) ctx.beginPath() @@ -4266,7 +4508,7 @@ export class LGraphCanvas implements CustomEventDispatcher highlightPos[0] - 6 + 0.5, highlightPos[1] - 5 + 0.5, 14, - 10, + 10 ) } else if (connShape === RenderShape.ARROW) { ctx.moveTo(pos[0] + 8, pos[1] + 0.5) @@ -4287,7 +4529,7 @@ export class LGraphCanvas implements CustomEventDispatcher // Area-selection rectangle if (this.dragging_rectangle) { const { eDown, eMove } = this.pointer - ctx.strokeStyle = "#FFF" + ctx.strokeStyle = '#FFF' if (eDown && eMove) { // Do not scale the selection box @@ -4308,7 +4550,11 @@ export class LGraphCanvas implements CustomEventDispatcher } // on top of link center - if (!this.isDragging && this.over_link_center && this.render_link_tooltip) { + if ( + !this.isDragging && + this.over_link_center && + this.render_link_tooltip + ) { this.drawLinkTooltip(ctx, this.over_link_center) } else { this.onDrawLinkTooltip?.(ctx, null) @@ -4331,7 +4577,9 @@ export class LGraphCanvas implements CustomEventDispatcher const centre = linkSegment._pos if (!centre) continue - if (isInRectangle(e.canvasX, e.canvasY, centre[0] - 4, centre[1] - 4, 8, 8)) { + if ( + isInRectangle(e.canvasX, e.canvasY, centre[0] - 4, centre[1] - 4, 8, 8) + ) { return linkSegment } } @@ -4340,7 +4588,9 @@ export class LGraphCanvas implements CustomEventDispatcher /** Get the target snap / highlight point in graph space */ #getHighlightPosition(): ReadOnlyPoint { return LiteGraph.snaps_for_comfy - ? this.linkConnector.state.snapLinksPos ?? this._highlight_pos ?? this.graph_mouse + ? this.linkConnector.state.snapLinksPos ?? + this._highlight_pos ?? + this.graph_mouse : this.graph_mouse } @@ -4351,12 +4601,12 @@ export class LGraphCanvas implements CustomEventDispatcher */ #renderSnapHighlight( ctx: CanvasRenderingContext2D, - highlightPos: ReadOnlyPoint, + highlightPos: ReadOnlyPoint ): void { const linkConnectorSnap = !!this.linkConnector.state.snapLinksPos if (!this._highlight_pos && !linkConnectorSnap) return - ctx.fillStyle = "#ffcc00" + ctx.fillStyle = '#ffcc00' ctx.beginPath() const shape = this._highlight_input?.shape @@ -4372,10 +4622,15 @@ export class LGraphCanvas implements CustomEventDispatcher const { linkConnector } = this const { overReroute, overWidget } = linkConnector - if (!LiteGraph.snap_highlights_node || !linkConnector.isConnecting || linkConnectorSnap) return + if ( + !LiteGraph.snap_highlights_node || + !linkConnector.isConnecting || + linkConnectorSnap + ) + return // Reroute highlight - overReroute?.drawHighlight(ctx, "#ffcc00aa") + overReroute?.drawHighlight(ctx, '#ffcc00aa') // Ensure we're mousing over a node and connecting a link const node = this.node_over @@ -4396,25 +4651,24 @@ export class LGraphCanvas implements CustomEventDispatcher ctx.roundRect(x, y, width, height, radius) // TODO: Currently works on LTR slots only. Add support for other directions. - const start = linkConnector.state.connectingTo === "output" ? 0 : 1 + const start = linkConnector.state.connectingTo === 'output' ? 0 : 1 const inverter = start ? -1 : 1 // Radial highlight centred on highlight pos const hx = highlightPos[0] const hy = highlightPos[1] - const gRadius = width < height - ? width - : width * Math.max(height / width, 0.5) + const gRadius = + width < height ? width : width * Math.max(height / width, 0.5) const gradient = ctx.createRadialGradient(hx, hy, 0, hx, hy, gRadius) - gradient.addColorStop(1, "#00000000") - gradient.addColorStop(0, "#ffcc00aa") + gradient.addColorStop(1, '#00000000') + gradient.addColorStop(0, '#ffcc00aa') // Linear gradient over half the node. const linearGradient = ctx.createLinearGradient(x, y, x + width, y) - linearGradient.addColorStop(0.5, "#00000000") - linearGradient.addColorStop(start + 0.67 * inverter, "#ddeeff33") - linearGradient.addColorStop(start + inverter, "#ffcc0055") + linearGradient.addColorStop(0.5, '#00000000') + linearGradient.addColorStop(start + 0.67 * inverter, '#ddeeff33') + linearGradient.addColorStop(start + inverter, '#ffcc0055') /** * Workaround for a canvas render issue. @@ -4431,10 +4685,12 @@ export class LGraphCanvas implements CustomEventDispatcher const { computedHeight } = overWidget ctx.beginPath() - const { pos: [nodeX, nodeY] } = node + const { + pos: [nodeX, nodeY] + } = node const height = LiteGraph.NODE_WIDGET_HEIGHT if ( - overWidget.type.startsWith("custom") && + overWidget.type.startsWith('custom') && computedHeight != null && computedHeight > height * 2 ) { @@ -4443,7 +4699,7 @@ export class LGraphCanvas implements CustomEventDispatcher nodeX + 9, nodeY + overWidget.y + 9, (overWidget.width ?? area[2]) - 18, - computedHeight - 18, + computedHeight - 18 ) } else { // Regular widget, probably @@ -4452,7 +4708,7 @@ export class LGraphCanvas implements CustomEventDispatcher nodeY + overWidget.y, overWidget.width ?? area[2], height, - height * 0.5, + height * 0.5 ) } ctx.stroke() @@ -4477,16 +4733,20 @@ export class LGraphCanvas implements CustomEventDispatcher ctx.translate(x, y) ctx.font = `10px ${LiteGraph.DEFAULT_FONT}` - ctx.fillStyle = "#888" - ctx.textAlign = "left" + ctx.fillStyle = '#888' + ctx.textAlign = 'left' if (this.graph) { ctx.fillText(`T: ${this.graph.globaltime.toFixed(2)}s`, 5, 13 * 1) ctx.fillText(`I: ${this.graph.iteration}`, 5, 13 * 2) - ctx.fillText(`N: ${this.graph._nodes.length} [${this.visible_nodes.length}]`, 5, 13 * 3) + ctx.fillText( + `N: ${this.graph._nodes.length} [${this.visible_nodes.length}]`, + 5, + 13 * 3 + ) ctx.fillText(`V: ${this.graph._version}`, 5, 13 * 4) ctx.fillText(`FPS:${this.fps.toFixed(2)}`, 5, 13 * 5) } else { - ctx.fillText("No graph selected", 5, 13 * 1) + ctx.fillText('No graph selected', 5, 13 * 1) } ctx.restore() } @@ -4505,12 +4765,17 @@ export class LGraphCanvas implements CustomEventDispatcher } if (!this.bgctx) { - this.bgctx = this.bgcanvas.getContext("2d") + this.bgctx = this.bgcanvas.getContext('2d') } const ctx = this.bgctx - if (!ctx) throw new TypeError("Background canvas context was null.") + if (!ctx) throw new TypeError('Background canvas context was null.') - const viewport = this.viewport || [0, 0, ctx.canvas.width, ctx.canvas.height] + const viewport = this.viewport || [ + 0, + 0, + ctx.canvas.width, + ctx.canvas.height + ] // clear if (this.clear_background) { @@ -4544,7 +4809,7 @@ export class LGraphCanvas implements CustomEventDispatcher this.visible_area[0], this.visible_area[1], this.visible_area[2], - this.visible_area[3], + this.visible_area[3] ) } @@ -4560,14 +4825,14 @@ export class LGraphCanvas implements CustomEventDispatcher this._bg_img.name = this.background_image this._bg_img.src = this.background_image const that = this - this._bg_img.addEventListener("load", function () { + this._bg_img.addEventListener('load', function () { that.draw(true, true) }) } let pattern = this._pattern if (pattern == null && this._bg_img.width > 0) { - pattern = ctx.createPattern(this._bg_img, "repeat") ?? undefined + pattern = ctx.createPattern(this._bg_img, 'repeat') ?? undefined this._pattern_img = this._bg_img this._pattern = pattern } @@ -4580,9 +4845,9 @@ export class LGraphCanvas implements CustomEventDispatcher this.visible_area[0], this.visible_area[1], this.visible_area[2], - this.visible_area[3], + this.visible_area[3] ) - ctx.fillStyle = "transparent" + ctx.fillStyle = 'transparent' } ctx.globalAlpha = 1.0 @@ -4601,23 +4866,23 @@ export class LGraphCanvas implements CustomEventDispatcher // ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20); // bg if (this.render_canvas_border) { - ctx.strokeStyle = "#235" + ctx.strokeStyle = '#235' ctx.strokeRect(0, 0, canvas.width, canvas.height) } if (this.render_connections_shadows) { - ctx.shadowColor = "#000" + ctx.shadowColor = '#000' ctx.shadowOffsetX = 0 ctx.shadowOffsetY = 0 ctx.shadowBlur = 6 } else { - ctx.shadowColor = "rgba(0,0,0,0)" + ctx.shadowColor = 'rgba(0,0,0,0)' } // draw connections this.drawConnections(ctx) - ctx.shadowColor = "rgba(0,0,0,0)" + ctx.shadowColor = 'rgba(0,0,0,0)' // restore state ctx.restore() @@ -4646,7 +4911,7 @@ export class LGraphCanvas implements CustomEventDispatcher ctx.shadowOffsetY = 2 * this.ds.scale ctx.shadowBlur = 3 * this.ds.scale } else { - ctx.shadowColor = "transparent" + ctx.shadowColor = 'transparent' } // custom draw collapsed method (draw after shadows because they are affected) @@ -4677,14 +4942,7 @@ export class LGraphCanvas implements CustomEventDispatcher } // draw shape - this.drawNodeShape( - node, - ctx, - size, - color, - bgcolor, - !!node.selected, - ) + this.drawNodeShape(node, ctx, size, color, bgcolor, !!node.selected) // Render title buttons (if not collapsed) if (node.title_buttons && !node.flags.collapsed) { @@ -4712,7 +4970,7 @@ export class LGraphCanvas implements CustomEventDispatcher node.drawBadges(ctx) } - ctx.shadowColor = "transparent" + ctx.shadowColor = 'transparent' // TODO: Legacy behaviour: onDrawForeground received ctx in this state ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR @@ -4728,13 +4986,15 @@ export class LGraphCanvas implements CustomEventDispatcher if (!node.collapsed) { node.arrange() node.drawSlots(ctx, { - fromSlot: this.linkConnector.renderLinks[0]?.fromSlot as INodeOutputSlot | INodeInputSlot, + fromSlot: this.linkConnector.renderLinks[0]?.fromSlot as + | INodeOutputSlot + | INodeInputSlot, colorContext: this.colourGetter, editorAlpha: this.editor_alpha, - lowQuality: this.low_quality, + lowQuality: this.low_quality }) - ctx.textAlign = "left" + ctx.textAlign = 'left' ctx.globalAlpha = 1 this.drawNodeWidgets(node, null, ctx) @@ -4759,13 +5019,14 @@ export class LGraphCanvas implements CustomEventDispatcher */ drawLinkTooltip(ctx: CanvasRenderingContext2D, link: LinkSegment): void { const pos = link._pos - ctx.fillStyle = "black" + ctx.fillStyle = 'black' ctx.beginPath() if (this.linkMarkerShape === LinkMarkerShape.Arrow) { const transform = ctx.getTransform() ctx.translate(pos[0], pos[1]) // Assertion: Number.isFinite guarantees this is a number. - if (Number.isFinite(link._centreAngle)) ctx.rotate(link._centreAngle as number) + if (Number.isFinite(link._centreAngle)) + ctx.rotate(link._centreAngle as number) ctx.moveTo(-2, -3) ctx.lineTo(+4, 0) ctx.lineTo(-2, +3) @@ -4787,40 +5048,35 @@ export class LGraphCanvas implements CustomEventDispatcher let text: string | null = null - if (typeof data === "number") - text = data.toFixed(2) - else if (typeof data === "string") - text = `"${data}"` - else if (typeof data === "boolean") - text = String(data) - else if (data.toToolTip) - text = data.toToolTip() - else - text = `[${data.constructor.name}]` + if (typeof data === 'number') text = data.toFixed(2) + else if (typeof data === 'string') text = `"${data}"` + else if (typeof data === 'boolean') text = String(data) + else if (data.toToolTip) text = data.toToolTip() + else text = `[${data.constructor.name}]` if (text == null) return // Hard-coded tooltip limit text = text.substring(0, 30) - ctx.font = "14px Courier New" + ctx.font = '14px Courier New' const info = ctx.measureText(text) const w = info.width + 20 const h = 24 - ctx.shadowColor = "black" + ctx.shadowColor = 'black' ctx.shadowOffsetX = 2 ctx.shadowOffsetY = 2 ctx.shadowBlur = 3 - ctx.fillStyle = "#454" + ctx.fillStyle = '#454' ctx.beginPath() ctx.roundRect(pos[0] - w * 0.5, pos[1] - 15 - h, w, h, [3]) ctx.moveTo(pos[0] - 10, pos[1] - 15) ctx.lineTo(pos[0] + 10, pos[1] - 15) ctx.lineTo(pos[0], pos[1] - 5) ctx.fill() - ctx.shadowColor = "transparent" - ctx.textAlign = "center" - ctx.fillStyle = "#CEC" + ctx.shadowColor = 'transparent' + ctx.textAlign = 'center' + ctx.fillStyle = '#CEC' ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3) } @@ -4839,7 +5095,7 @@ export class LGraphCanvas implements CustomEventDispatcher size: Size, fgcolor: CanvasColour, bgcolor: CanvasColour, - _selected: boolean, + _selected: boolean ): void { // Rendering options ctx.strokeStyle = fgcolor @@ -4852,9 +5108,11 @@ export class LGraphCanvas implements CustomEventDispatcher const shape = node.renderingShape const { title_mode } = node - const render_title = title_mode == TitleMode.TRANSPARENT_TITLE || title_mode == TitleMode.NO_TITLE - ? false - : true + const render_title = + title_mode == TitleMode.TRANSPARENT_TITLE || + title_mode == TitleMode.NO_TITLE + ? false + : true // Normalised node dimensions const area = LGraphCanvas.#tmp_area @@ -4876,7 +5134,7 @@ export class LGraphCanvas implements CustomEventDispatcher area[3], shape == RenderShape.CARD ? [LiteGraph.ROUND_RADIUS, LiteGraph.ROUND_RADIUS, 0, 0] - : [LiteGraph.ROUND_RADIUS], + : [LiteGraph.ROUND_RADIUS] ) } else if (shape == RenderShape.CIRCLE) { ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI * 2) @@ -4885,11 +5143,11 @@ export class LGraphCanvas implements CustomEventDispatcher // Separator - title bar <-> body if (!collapsed && render_title) { - ctx.shadowColor = "transparent" - ctx.fillStyle = "rgba(0,0,0,0.2)" + ctx.shadowColor = 'transparent' + ctx.fillStyle = 'rgba(0,0,0,0.2)' ctx.fillRect(0, -1, area[2], 2) } - ctx.shadowColor = "transparent" + ctx.shadowColor = 'transparent' node.onDrawBackground?.(ctx) @@ -4897,14 +5155,14 @@ export class LGraphCanvas implements CustomEventDispatcher if (render_title || title_mode == TitleMode.TRANSPARENT_TITLE) { node.drawTitleBarBackground(ctx, { scale: this.ds.scale, - low_quality, + low_quality }) // title box node.drawTitleBox(ctx, { scale: this.ds.scale, low_quality, - box_size: 10, + box_size: 10 }) ctx.globalAlpha = old_alpha @@ -4913,7 +5171,7 @@ export class LGraphCanvas implements CustomEventDispatcher node.drawTitleText(ctx, { scale: this.ds.scale, default_title_color: this.node_title_color, - low_quality, + low_quality }) // custom title render @@ -4929,7 +5187,7 @@ export class LGraphCanvas implements CustomEventDispatcher title_height, title_mode, collapsed, - ...strokeStyle, + ...strokeStyle }) } } @@ -4937,8 +5195,10 @@ export class LGraphCanvas implements CustomEventDispatcher node.drawProgressBar(ctx) // these counter helps in conditioning drawing based on if the node has been executed or an action occurred - if (node.execute_triggered != null && node.execute_triggered > 0) node.execute_triggered-- - if (node.action_triggered != null && node.action_triggered > 0) node.action_triggered-- + if (node.execute_triggered != null && node.execute_triggered > 0) + node.execute_triggered-- + if (node.action_triggered != null && node.action_triggered > 0) + node.action_triggered-- } /** @@ -4955,7 +5215,7 @@ export class LGraphCanvas implements CustomEventDispatcher drawSnapGuide( ctx: CanvasRenderingContext2D, item: Positionable, - shape = RenderShape.ROUND, + shape = RenderShape.ROUND ) { const snapGuide = LGraphCanvas.#temp snapGuide.set(item.boundingRect) @@ -4977,8 +5237,8 @@ export class LGraphCanvas implements CustomEventDispatcher ctx.beginPath() const [x, y, w, h] = snapGuide if (shape === RenderShape.CIRCLE) { - const midX = x + (w * 0.5) - const midY = y + (h * 0.5) + const midX = x + w * 0.5 + const midY = y + h * 0.5 const radius = Math.min(w * 0.5, h * 0.5) ctx.arc(midX, midY, radius, 0, Math.PI * 2) } else { @@ -4986,8 +5246,8 @@ export class LGraphCanvas implements CustomEventDispatcher } ctx.lineWidth = 0.5 - ctx.strokeStyle = "#FFFFFF66" - ctx.fillStyle = "#FFFFFF22" + ctx.strokeStyle = '#FFFFFF66' + ctx.fillStyle = '#FFFFFF22' ctx.fill() ctx.stroke() ctx.globalAlpha = globalAlpha @@ -5012,8 +5272,8 @@ export class LGraphCanvas implements CustomEventDispatcher // draw connections ctx.lineWidth = this.connections_width - ctx.fillStyle = "#AAA" - ctx.strokeStyle = "#AAA" + ctx.fillStyle = '#AAA' + ctx.strokeStyle = '#AAA' ctx.globalAlpha = this.editor_alpha // for every node const nodes = graph._nodes @@ -5036,14 +5296,24 @@ export class LGraphCanvas implements CustomEventDispatcher if (start_node == null) continue const outputId = link.origin_slot - const startPos: Point = outputId === -1 - ? [start_node.pos[0] + 10, start_node.pos[1] + 10] - : start_node.getOutputPos(outputId) + const startPos: Point = + outputId === -1 + ? [start_node.pos[0] + 10, start_node.pos[1] + 10] + : start_node.getOutputPos(outputId) const output = start_node.outputs[outputId] if (!output) continue - this.#renderAllLinkSegments(ctx, link, startPos, endPos, visibleReroutes, now, output.dir, input.dir) + this.#renderAllLinkSegments( + ctx, + link, + startPos, + endPos, + visibleReroutes, + now, + output.dir, + input.dir + ) } } @@ -5061,7 +5331,16 @@ export class LGraphCanvas implements CustomEventDispatcher const endPos = inputNode.getInputPos(link.target_slot) - this.#renderAllLinkSegments(ctx, link, output.pos, endPos, visibleReroutes, now, input.dir, input.dir) + this.#renderAllLinkSegments( + ctx, + link, + output.pos, + endPos, + visibleReroutes, + now, + input.dir, + input.dir + ) } } @@ -5077,7 +5356,16 @@ export class LGraphCanvas implements CustomEventDispatcher const startPos = outputNode.getOutputPos(link.origin_slot) - this.#renderAllLinkSegments(ctx, link, startPos, input.pos, visibleReroutes, now, output.dir, input.dir) + this.#renderAllLinkSegments( + ctx, + link, + startPos, + input.pos, + visibleReroutes, + now, + output.dir, + input.dir + ) } } @@ -5108,7 +5396,12 @@ export class LGraphCanvas implements CustomEventDispatcher ctx.globalAlpha = 1 } - #renderFloatingLinks(ctx: CanvasRenderingContext2D, graph: LGraph, visibleReroutes: Reroute[], now: number) { + #renderFloatingLinks( + ctx: CanvasRenderingContext2D, + graph: LGraph, + visibleReroutes: Reroute[], + now: number + ) { // Render floating links with 3/4 current alpha const { globalAlpha } = ctx ctx.globalAlpha = globalAlpha * 0.33 @@ -5121,7 +5414,7 @@ export class LGraphCanvas implements CustomEventDispatcher if (!firstReroute || !reroute?.floating) continue // Input not connected - if (reroute.floating.slotType === "input") { + if (reroute.floating.slotType === 'input') { const node = graph.getNodeById(link.target_id) if (!node) continue @@ -5130,7 +5423,17 @@ export class LGraphCanvas implements CustomEventDispatcher const endDirection = node.inputs[link.target_slot]?.dir firstReroute._dragging = true - this.#renderAllLinkSegments(ctx, link, startPos, endPos, visibleReroutes, now, LinkDirection.CENTER, endDirection, true) + this.#renderAllLinkSegments( + ctx, + link, + startPos, + endPos, + visibleReroutes, + now, + LinkDirection.CENTER, + endDirection, + true + ) } else { const node = graph.getNodeById(link.origin_id) if (!node) continue @@ -5140,7 +5443,17 @@ export class LGraphCanvas implements CustomEventDispatcher const startDirection = node.outputs[link.origin_slot]?.dir link._dragging = true - this.#renderAllLinkSegments(ctx, link, startPos, endPos, visibleReroutes, now, startDirection, LinkDirection.CENTER, true) + this.#renderAllLinkSegments( + ctx, + link, + startPos, + endPos, + visibleReroutes, + now, + startDirection, + LinkDirection.CENTER, + true + ) } } ctx.globalAlpha = globalAlpha @@ -5155,7 +5468,7 @@ export class LGraphCanvas implements CustomEventDispatcher now: number, startDirection?: LinkDirection, endDirection?: LinkDirection, - disabled: boolean = false, + disabled: boolean = false ) { const { graph, renderedPaths } = this if (!graph) return @@ -5164,20 +5477,24 @@ export class LGraphCanvas implements CustomEventDispatcher const reroutes = LLink.getReroutes(graph, link) const points: [Point, ...Point[], Point] = [ startPos, - ...reroutes.map(x => x.pos), - endPos, + ...reroutes.map((x) => x.pos), + endPos ] // Bounding box of all points (bezier overshoot on long links will be cut) - const pointsX = points.map(x => x[0]) - const pointsY = points.map(x => x[1]) + const pointsX = points.map((x) => x[0]) + const pointsY = points.map((x) => x[1]) LGraphCanvas.#link_bounding[0] = Math.min(...pointsX) LGraphCanvas.#link_bounding[1] = Math.min(...pointsY) - LGraphCanvas.#link_bounding[2] = Math.max(...pointsX) - LGraphCanvas.#link_bounding[0] - LGraphCanvas.#link_bounding[3] = Math.max(...pointsY) - LGraphCanvas.#link_bounding[1] + LGraphCanvas.#link_bounding[2] = + Math.max(...pointsX) - LGraphCanvas.#link_bounding[0] + LGraphCanvas.#link_bounding[3] = + Math.max(...pointsY) - LGraphCanvas.#link_bounding[1] // skip links outside of the visible area of the canvas - if (!overlapBounding(LGraphCanvas.#link_bounding, LGraphCanvas.#margin_area)) + if ( + !overlapBounding(LGraphCanvas.#link_bounding, LGraphCanvas.#margin_area) + ) return const start_dir = startDirection || LinkDirection.RIGHT @@ -5195,7 +5512,8 @@ export class LGraphCanvas implements CustomEventDispatcher if (!renderedPaths.has(reroute)) { renderedPaths.add(reroute) visibleReroutes.push(reroute) - reroute._colour = link.color || + reroute._colour = + link.color || LGraphCanvas.link_type_colors[link.type] || this.default_link_color @@ -5219,19 +5537,22 @@ export class LGraphCanvas implements CustomEventDispatcher startControl, endControl: reroute.controlPoint, reroute, - disabled, - }, + disabled + } ) } } - if (!startControl && reroutes.at(-1)?.floating?.slotType === "input") { + if (!startControl && reroutes.at(-1)?.floating?.slotType === 'input') { // Floating link connected to an input startControl = [0, 0] } else { // Calculate start control for the next iter control point const nextPos = reroutes[j + 1]?.pos ?? endPos - const dist = Math.min(Reroute.maxSplineOffset, distance(reroute.pos, nextPos) * 0.25) + const dist = Math.min( + Reroute.maxSplineOffset, + distance(reroute.pos, nextPos) * 0.25 + ) startControl = [dist * reroute.cos, dist * reroute.sin] } } @@ -5253,7 +5574,7 @@ export class LGraphCanvas implements CustomEventDispatcher null, LinkDirection.CENTER, end_dir, - { startControl, disabled }, + { startControl, disabled } ) // Skip normal render when link is being dragged } else if (!link._dragging) { @@ -5266,7 +5587,7 @@ export class LGraphCanvas implements CustomEventDispatcher 0, null, start_dir, - end_dir, + end_dir ) } renderedPaths.add(link) @@ -5283,9 +5604,9 @@ export class LGraphCanvas implements CustomEventDispatcher link, true, f, - "white", + 'white', start_dir, - end_dir, + end_dir ) ctx.globalAlpha = tmp } @@ -5318,7 +5639,7 @@ export class LGraphCanvas implements CustomEventDispatcher endControl, reroute, num_sublines = 1, - disabled = false, + disabled = false }: { /** When defined, render data will be saved to this reroute instead of the {@link link}. */ reroute?: Reroute @@ -5330,11 +5651,11 @@ export class LGraphCanvas implements CustomEventDispatcher num_sublines?: number /** Whether this is a floating link segment */ disabled?: boolean - } = {}, + } = {} ): void { const linkColour = link != null && this.highlighted_links[link.id] - ? "#FFF" + ? '#FFF' : color || link?.color || (link?.type != null && LGraphCanvas.link_type_colors[link.type]) || @@ -5342,15 +5663,17 @@ export class LGraphCanvas implements CustomEventDispatcher const startDir = start_dir || LinkDirection.RIGHT const endDir = end_dir || LinkDirection.LEFT - const dist = this.links_render_mode == LinkRenderType.SPLINE_LINK && (!endControl || !startControl) - ? distance(a, b) - : 0 + const dist = + this.links_render_mode == LinkRenderType.SPLINE_LINK && + (!endControl || !startControl) + ? distance(a, b) + : 0 // TODO: Subline code below was inserted in the wrong place - should be before this statement if (this.render_connections_border && !this.low_quality) { ctx.lineWidth = this.connections_width + 4 } - ctx.lineJoin = "round" + ctx.lineJoin = 'round' num_sublines ||= 1 if (num_sublines > 1) ctx.lineWidth = 0.5 @@ -5394,7 +5717,7 @@ export class LGraphCanvas implements CustomEventDispatcher innerB[0], innerB[1] + offsety, b[0], - b[1] + offsety, + b[1] + offsety ) // Calculate centre point @@ -5406,38 +5729,38 @@ export class LGraphCanvas implements CustomEventDispatcher linkSegment._centreAngle = Math.atan2( justPastCentre[1] - pos[1], - justPastCentre[0] - pos[0], + justPastCentre[0] - pos[0] ) } } else { const l = this.links_render_mode == LinkRenderType.LINEAR_LINK ? 15 : 10 switch (startDir) { - case LinkDirection.LEFT: - innerA[0] += -l - break - case LinkDirection.RIGHT: - innerA[0] += l - break - case LinkDirection.UP: - innerA[1] += -l - break - case LinkDirection.DOWN: - innerA[1] += l - break + case LinkDirection.LEFT: + innerA[0] += -l + break + case LinkDirection.RIGHT: + innerA[0] += l + break + case LinkDirection.UP: + innerA[1] += -l + break + case LinkDirection.DOWN: + innerA[1] += l + break } switch (endDir) { - case LinkDirection.LEFT: - innerB[0] += -l - break - case LinkDirection.RIGHT: - innerB[0] += l - break - case LinkDirection.UP: - innerB[1] += -l - break - case LinkDirection.DOWN: - innerB[1] += l - break + case LinkDirection.LEFT: + innerB[0] += -l + break + case LinkDirection.RIGHT: + innerB[0] += l + break + case LinkDirection.UP: + innerB[1] += -l + break + case LinkDirection.DOWN: + innerB[1] += l + break } if (this.links_render_mode == LinkRenderType.LINEAR_LINK) { path.moveTo(a[0], a[1] + offsety) @@ -5452,7 +5775,7 @@ export class LGraphCanvas implements CustomEventDispatcher if (linkSegment && this.linkMarkerShape === LinkMarkerShape.Arrow) { linkSegment._centreAngle = Math.atan2( innerB[1] - innerA[1], - innerB[0] - innerA[0], + innerB[0] - innerA[0] ) } } else if (this.links_render_mode == LinkRenderType.STRAIGHT_LINK) { @@ -5483,7 +5806,7 @@ export class LGraphCanvas implements CustomEventDispatcher // rendering the outline of the connection can be a little bit slow if (this.render_connections_border && !this.low_quality && !skip_border) { - ctx.strokeStyle = "rgba(0,0,0,0.5)" + ctx.strokeStyle = 'rgba(0,0,0,0.5)' ctx.stroke(path) } @@ -5492,11 +5815,7 @@ export class LGraphCanvas implements CustomEventDispatcher ctx.stroke(path) // render arrow in the middle - if ( - this.ds.scale >= 0.6 && - this.highquality_render && - linkSegment - ) { + if (this.ds.scale >= 0.6 && this.highquality_render && linkSegment) { // render arrow if (this.render_connection_arrows) { // compute two points in the connection @@ -5555,7 +5874,7 @@ export class LGraphCanvas implements CustomEventDispatcher } if (disabled) { const { fillStyle, globalAlpha } = ctx - ctx.fillStyle = this._pattern ?? "#797979" + ctx.fillStyle = this._pattern ?? '#797979' ctx.globalAlpha = 0.75 ctx.fill() ctx.globalAlpha = globalAlpha @@ -5567,9 +5886,9 @@ export class LGraphCanvas implements CustomEventDispatcher const { fillStyle, font, globalAlpha, lineWidth, strokeStyle } = ctx ctx.globalAlpha = 1 ctx.lineWidth = 4 - ctx.fillStyle = "white" - ctx.strokeStyle = "black" - ctx.font = "16px Arial" + ctx.fillStyle = 'white' + ctx.strokeStyle = 'black' + ctx.font = '16px Arial' const text = String(linkSegment.id) const { width, actualBoundingBoxAscent } = ctx.measureText(text) @@ -5613,7 +5932,7 @@ export class LGraphCanvas implements CustomEventDispatcher b: ReadOnlyPoint, t: number, start_dir: LinkDirection, - end_dir: LinkDirection, + end_dir: LinkDirection ): Point { start_dir ||= LinkDirection.RIGHT end_dir ||= LinkDirection.LEFT @@ -5646,54 +5965,54 @@ export class LGraphCanvas implements CustomEventDispatcher point: Point, direction: LinkDirection, dist: number, - factor = 0.25, + factor = 0.25 ): void { switch (direction) { - case LinkDirection.LEFT: - point[0] += dist * -factor - break - case LinkDirection.RIGHT: - point[0] += dist * factor - break - case LinkDirection.UP: - point[1] += dist * -factor - break - case LinkDirection.DOWN: - point[1] += dist * factor - break + case LinkDirection.LEFT: + point[0] += dist * -factor + break + case LinkDirection.RIGHT: + point[0] += dist * factor + break + case LinkDirection.UP: + point[1] += dist * -factor + break + case LinkDirection.DOWN: + point[1] += dist * factor + break } } drawExecutionOrder(ctx: CanvasRenderingContext2D): void { - ctx.shadowColor = "transparent" + ctx.shadowColor = 'transparent' ctx.globalAlpha = 0.25 - ctx.textAlign = "center" - ctx.strokeStyle = "white" + ctx.textAlign = 'center' + ctx.strokeStyle = 'white' ctx.globalAlpha = 0.75 const { visible_nodes } = this for (const node of visible_nodes) { - ctx.fillStyle = "black" + ctx.fillStyle = 'black' ctx.fillRect( node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT, node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT, + LiteGraph.NODE_TITLE_HEIGHT ) if (node.order == 0) { ctx.strokeRect( node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT, + LiteGraph.NODE_TITLE_HEIGHT ) } - ctx.fillStyle = "#FFF" + ctx.fillStyle = '#FFF' ctx.fillText( stringOrEmpty(node.order), node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5, - node.pos[1] - 6, + node.pos[1] - 6 ) } ctx.globalAlpha = 1 @@ -5707,11 +6026,11 @@ export class LGraphCanvas implements CustomEventDispatcher drawNodeWidgets( node: LGraphNode, _posY: null, - ctx: CanvasRenderingContext2D, + ctx: CanvasRenderingContext2D ): void { node.drawWidgets(ctx, { lowQuality: this.low_quality, - editorAlpha: this.editor_alpha, + editorAlpha: this.editor_alpha }) } @@ -5750,7 +6069,10 @@ export class LGraphCanvas implements CustomEventDispatcher resize(width?: number, height?: number): void { if (!width && !height) { const parent = this.canvas.parentElement - if (!parent) throw new TypeError("Attempted to resize canvas, but parent element was null.") + if (!parent) + throw new TypeError( + 'Attempted to resize canvas, but parent element was null.' + ) width = parent.offsetWidth height = parent.offsetHeight } @@ -5777,15 +6099,16 @@ export class LGraphCanvas implements CustomEventDispatcher const { graph } = this if (!graph) throw new NullGraphError() - const title = "data" in segment && segment.data != null - ? segment.data.constructor.name - : undefined + const title = + 'data' in segment && segment.data != null + ? segment.data.constructor.name + : undefined const { origin_id, origin_slot } = segment if (origin_id == null || origin_slot == null) { - new LiteGraph.ContextMenu(["Link has no origin"], { + new LiteGraph.ContextMenu(['Link has no origin'], { event: e, - title, + title }) return false } @@ -5793,50 +6116,67 @@ export class LGraphCanvas implements CustomEventDispatcher const node_left = graph.getNodeById(origin_id) const fromType = node_left?.outputs?.[origin_slot]?.type - const options = ["Add Node", "Add Reroute", null, "Delete", null] + const options = ['Add Node', 'Add Reroute', null, 'Delete', null] const menu = new LiteGraph.ContextMenu(options, { event: e, title, - callback: inner_clicked.bind(this), + callback: inner_clicked.bind(this) }) return false - function inner_clicked(this: LGraphCanvas, v: string, options: unknown, e: MouseEvent) { + function inner_clicked( + this: LGraphCanvas, + v: string, + options: unknown, + e: MouseEvent + ) { if (!graph) throw new NullGraphError() switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, (node) => { - if (!node?.inputs?.length || !node?.outputs?.length || origin_slot == null) return + case 'Add Node': + LGraphCanvas.onMenuAdd(null, null, e, menu, (node) => { + if ( + !node?.inputs?.length || + !node?.outputs?.length || + origin_slot == null + ) + return - // leave the connection type checking inside connectByType - const options = { afterRerouteId: segment.parentId } - if (node_left?.connectByType(origin_slot, node, fromType ?? "*", options)) { - node.pos[0] -= node.size[0] * 0.5 + // leave the connection type checking inside connectByType + const options = { afterRerouteId: segment.parentId } + if ( + node_left?.connectByType( + origin_slot, + node, + fromType ?? '*', + options + ) + ) { + node.pos[0] -= node.size[0] * 0.5 + } + }) + break + + case 'Add Reroute': { + try { + this.emitBeforeChange() + this.adjustMouseEvent(e) + graph.createReroute(segment._pos, segment) + this.setDirty(false, true) + } catch (error) { + console.error(error) + } finally { + this.emitAfterChange() } - }) - break - - case "Add Reroute": { - try { - this.emitBeforeChange() - this.adjustMouseEvent(e) - graph.createReroute(segment._pos, segment) - this.setDirty(false, true) - } catch (error) { - console.error(error) - } finally { - this.emitAfterChange() + break } - break - } - case "Delete": - graph.removeLink(segment.id) - break - default: + case 'Delete': + graph.removeLink(segment.id) + break + default: } } } @@ -5847,87 +6187,101 @@ export class LGraphCanvas implements CustomEventDispatcher posSizeFix: Point } - const opts = Object.assign({ - nodeFrom: null, - slotFrom: null, - nodeTo: null, - slotTo: null, - position: [0, 0], - nodeType: undefined, - posAdd: [0, 0], - posSizeFix: [0, 0], - }, optPass) + const opts = Object.assign( + { + nodeFrom: null, + slotFrom: null, + nodeTo: null, + slotTo: null, + position: [0, 0], + nodeType: undefined, + posAdd: [0, 0], + posSizeFix: [0, 0] + }, + optPass + ) const { afterRerouteId } = opts const isFrom = opts.nodeFrom && opts.slotFrom !== null const isTo = !isFrom && opts.nodeTo && opts.slotTo !== null if (!isFrom && !isTo) { - console.warn(`No data passed to createDefaultNodeForSlot`, opts.nodeFrom, opts.slotFrom, opts.nodeTo, opts.slotTo) + console.warn( + `No data passed to createDefaultNodeForSlot`, + opts.nodeFrom, + opts.slotFrom, + opts.nodeTo, + opts.slotTo + ) return false } if (!opts.nodeType) { - console.warn("No type to createDefaultNodeForSlot") + console.warn('No type to createDefaultNodeForSlot') return false } const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo - if (!nodeX) throw new TypeError("nodeX was null when creating default node for slot.") + if (!nodeX) + throw new TypeError('nodeX was null when creating default node for slot.') let slotX = isFrom ? opts.slotFrom : opts.slotTo let iSlotConn: number | false = false if (nodeX instanceof SubgraphIONodeBase) { - if (typeof slotX !== "object" || !slotX) { - console.warn("Cant get slot information", slotX) + if (typeof slotX !== 'object' || !slotX) { + console.warn('Cant get slot information', slotX) return false } const { name } = slotX - iSlotConn = nodeX.slots.findIndex(s => s.name === name) + iSlotConn = nodeX.slots.findIndex((s) => s.name === name) slotX = nodeX.slots[iSlotConn] if (!slotX) { - console.warn("Cant get slot information", slotX) + console.warn('Cant get slot information', slotX) return false } } else { switch (typeof slotX) { - case "string": - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX, false) : nodeX.findInputSlot(slotX, false) - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX] - break - case "object": - if (slotX === null) { - console.warn("Cant get slot information", slotX) - return false - } + case 'string': + iSlotConn = isFrom + ? nodeX.findOutputSlot(slotX, false) + : nodeX.findInputSlot(slotX, false) + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX] + break + case 'object': + if (slotX === null) { + console.warn('Cant get slot information', slotX) + return false + } - // ok slotX - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name) - break - case "number": - iSlotConn = slotX - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX] - break - case "undefined": - default: - console.warn("Cant get slot information", slotX) - return false + // ok slotX + iSlotConn = isFrom + ? nodeX.findOutputSlot(slotX.name) + : nodeX.findInputSlot(slotX.name) + break + case 'number': + iSlotConn = slotX + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX] + break + case 'undefined': + default: + console.warn('Cant get slot information', slotX) + return false } } // check for defaults nodes for this slottype - const fromSlotType = slotX.type == LiteGraph.EVENT ? "_event_" : slotX.type + const fromSlotType = slotX.type == LiteGraph.EVENT ? '_event_' : slotX.type const slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in if (slotTypesDefault?.[fromSlotType]) { // TODO: Remove "any" kludge let nodeNewType: any = false - if (typeof slotTypesDefault[fromSlotType] == "object") { + if (typeof slotTypesDefault[fromSlotType] == 'object') { for (const typeX in slotTypesDefault[fromSlotType]) { if ( opts.nodeType == slotTypesDefault[fromSlotType][typeX] || - opts.nodeType == "AUTO" + opts.nodeType == 'AUTO' ) { nodeNewType = slotTypesDefault[fromSlotType][typeX] break @@ -5935,14 +6289,14 @@ export class LGraphCanvas implements CustomEventDispatcher } } else if ( opts.nodeType == slotTypesDefault[fromSlotType] || - opts.nodeType == "AUTO" + opts.nodeType == 'AUTO' ) { nodeNewType = slotTypesDefault[fromSlotType] } if (nodeNewType) { // TODO: Remove "any" kludge let nodeNewOpts: any = false - if (typeof nodeNewType == "object" && nodeNewType.node) { + if (typeof nodeNewType == 'object' && nodeNewType.node) { nodeNewOpts = nodeNewType nodeNewType = nodeNewType.node } @@ -5962,7 +6316,7 @@ export class LGraphCanvas implements CustomEventDispatcher for (const i in nodeNewOpts.inputs) { newNode.addOutput( nodeNewOpts.inputs[i][0], - nodeNewOpts.inputs[i][1], + nodeNewOpts.inputs[i][1] ) } } @@ -5971,7 +6325,7 @@ export class LGraphCanvas implements CustomEventDispatcher for (const i in nodeNewOpts.outputs) { newNode.addOutput( nodeNewOpts.outputs[i][0], - nodeNewOpts.outputs[i][1], + nodeNewOpts.outputs[i][1] ) } } @@ -5988,23 +6342,40 @@ export class LGraphCanvas implements CustomEventDispatcher this.graph.add(newNode) newNode.pos = [ - opts.position[0] + opts.posAdd[0] + (opts.posSizeFix[0] ? opts.posSizeFix[0] * newNode.size[0] : 0), - opts.position[1] + opts.posAdd[1] + (opts.posSizeFix[1] ? opts.posSizeFix[1] * newNode.size[1] : 0), + opts.position[0] + + opts.posAdd[0] + + (opts.posSizeFix[0] ? opts.posSizeFix[0] * newNode.size[0] : 0), + opts.position[1] + + opts.posAdd[1] + + (opts.posSizeFix[1] ? opts.posSizeFix[1] * newNode.size[1] : 0) ] // Interim API - allow the link connection to be canceled. // TODO: https://github.com/Comfy-Org/litegraph.js/issues/946 const detail = { node: newNode, opts } - const mayConnectLinks = this.canvas.dispatchEvent(new CustomEvent("connect-new-default-node", { detail, cancelable: true })) + const mayConnectLinks = this.canvas.dispatchEvent( + new CustomEvent('connect-new-default-node', { + detail, + cancelable: true + }) + ) if (!mayConnectLinks) return true // connect the two! if (isFrom) { - if (!opts.nodeFrom) throw new TypeError("createDefaultNodeForSlot - nodeFrom was null") - opts.nodeFrom.connectByType(iSlotConn, newNode, fromSlotType, { afterRerouteId }) + if (!opts.nodeFrom) + throw new TypeError( + 'createDefaultNodeForSlot - nodeFrom was null' + ) + opts.nodeFrom.connectByType(iSlotConn, newNode, fromSlotType, { + afterRerouteId + }) } else { - if (!opts.nodeTo) throw new TypeError("createDefaultNodeForSlot - nodeTo was null") - opts.nodeTo.connectByTypeOutput(iSlotConn, newNode, fromSlotType, { afterRerouteId }) + if (!opts.nodeTo) + throw new TypeError('createDefaultNodeForSlot - nodeTo was null') + opts.nodeTo.connectByTypeOutput(iSlotConn, newNode, fromSlotType, { + afterRerouteId + }) } // if connecting in between @@ -6020,16 +6391,24 @@ export class LGraphCanvas implements CustomEventDispatcher return false } - showConnectionMenu(optPass: Partial): ContextMenu | undefined { - const opts = Object.assign({ - nodeFrom: null, - slotFrom: null, - nodeTo: null, - slotTo: null, - e: undefined, - allow_searchbox: this.allow_searchbox, - showSearchBox: this.showSearchBox, - }, optPass || {}) + showConnectionMenu( + optPass: Partial + ): ContextMenu | undefined { + const opts = Object.assign< + ICreateNodeOptions & HasShowSearchCallback, + ICreateNodeOptions + >( + { + nodeFrom: null, + slotFrom: null, + nodeTo: null, + slotTo: null, + e: undefined, + allow_searchbox: this.allow_searchbox, + showSearchBox: this.showSearchBox + }, + optPass || {} + ) const dirty = () => this.#dirty() const that = this const { graph } = this @@ -6039,73 +6418,74 @@ export class LGraphCanvas implements CustomEventDispatcher const isTo = !isFrom && opts.nodeTo && opts.slotTo if (!isFrom && !isTo) { - console.warn("No data passed to showConnectionMenu") + console.warn('No data passed to showConnectionMenu') return } const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo - if (!nodeX) throw new TypeError("nodeX was null when creating default node for slot.") + if (!nodeX) + throw new TypeError('nodeX was null when creating default node for slot.') let slotX = isFrom ? opts.slotFrom : opts.slotTo let iSlotConn: number if (nodeX instanceof SubgraphIONodeBase) { - if (typeof slotX !== "object" || !slotX) { - console.warn("Cant get slot information", slotX) + if (typeof slotX !== 'object' || !slotX) { + console.warn('Cant get slot information', slotX) return } const { name } = slotX - iSlotConn = nodeX.slots.findIndex(s => s.name === name) + iSlotConn = nodeX.slots.findIndex((s) => s.name === name) // If it's not found in the main slots, it might be the empty slot from a Subgraph node. // In that case, the original `slotX` object is the correct one, so don't overwrite it. if (iSlotConn !== -1) { slotX = nodeX.slots[iSlotConn] } if (!slotX) { - console.warn("Cant get slot information", slotX) + console.warn('Cant get slot information', slotX) return } } else { switch (typeof slotX) { - case "string": - iSlotConn = isFrom - ? nodeX.findOutputSlot(slotX, false) - : nodeX.findInputSlot(slotX, false) - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX] - break - case "object": - if (slotX === null) { - console.warn("Cant get slot information", slotX) - return - } + case 'string': + iSlotConn = isFrom + ? nodeX.findOutputSlot(slotX, false) + : nodeX.findInputSlot(slotX, false) + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX] + break + case 'object': + if (slotX === null) { + console.warn('Cant get slot information', slotX) + return + } - // ok slotX - iSlotConn = isFrom - ? nodeX.findOutputSlot(slotX.name) - : nodeX.findInputSlot(slotX.name) - break - case "number": - iSlotConn = slotX - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX] - break - default: - console.warn("Cant get slot information", slotX) - return + // ok slotX + iSlotConn = isFrom + ? nodeX.findOutputSlot(slotX.name) + : nodeX.findInputSlot(slotX.name) + break + case 'number': + iSlotConn = slotX + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX] + break + default: + console.warn('Cant get slot information', slotX) + return } } - const options = ["Add Node", "Add Reroute", null] + const options = ['Add Node', 'Add Reroute', null] if (opts.allow_searchbox) { - options.push("Search", null) + options.push('Search', null) } // get defaults nodes for this slottype - const fromSlotType = slotX.type == LiteGraph.EVENT ? "_event_" : slotX.type + const fromSlotType = slotX.type == LiteGraph.EVENT ? '_event_' : slotX.type const slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in if (slotTypesDefault?.[fromSlotType]) { - if (typeof slotTypesDefault[fromSlotType] == "object") { + if (typeof slotTypesDefault[fromSlotType] == 'object') { for (const typeX in slotTypesDefault[fromSlotType]) { options.push(slotTypesDefault[fromSlotType][typeX]) } @@ -6119,71 +6499,104 @@ export class LGraphCanvas implements CustomEventDispatcher event: opts.e, extra: slotX, title: - (slotX && slotX.name != "" - ? slotX.name + (fromSlotType ? " | " : "") - : "") + (slotX && fromSlotType ? fromSlotType : ""), - callback: inner_clicked, + (slotX && slotX.name != '' + ? slotX.name + (fromSlotType ? ' | ' : '') + : '') + (slotX && fromSlotType ? fromSlotType : ''), + callback: inner_clicked }) return menu // callback - function inner_clicked(v: string | undefined, options: IContextMenuOptions, e: MouseEvent) { + function inner_clicked( + v: string | undefined, + options: IContextMenuOptions, + e: MouseEvent + ) { switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) { - if (!node) return + case 'Add Node': + LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) { + if (!node) return - if (isFrom) { - if (!opts.nodeFrom) throw new TypeError("Cannot add node to SubgraphInputNode: nodeFrom was null") - const slot = opts.nodeFrom.connectByType(iSlotConn, node, fromSlotType, { afterRerouteId }) - if (!slot) console.warn("Failed to make new connection.") - // } - } else { - if (!opts.nodeTo) throw new TypeError("Cannot add node to SubgraphInputNode: nodeTo was null") - opts.nodeTo.connectByTypeOutput(iSlotConn, node, fromSlotType, { afterRerouteId }) - } - }) - break - case "Add Reroute":{ - const node = isFrom ? opts.nodeFrom : opts.nodeTo - const slot = options.extra - - if (!graph) throw new NullGraphError() - if (!node) throw new TypeError("Cannot add reroute: node was null") - if (!slot) throw new TypeError("Cannot add reroute: slot was null") - if (!opts.e) throw new TypeError("Cannot add reroute: CanvasPointerEvent was null") - - if (node instanceof SubgraphIONodeBase) { - throw new TypeError("Cannot add floating reroute to Subgraph IO Nodes") - } else { - const reroute = node.connectFloatingReroute([opts.e.canvasX, opts.e.canvasY], slot, afterRerouteId) - if (!reroute) throw new Error("Failed to create reroute") - } - - dirty() - break - } - case "Search": - if (isFrom) { - // @ts-expect-error Subgraph - opts.showSearchBox(e, { node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType }) - } else { - // @ts-expect-error Subgraph - opts.showSearchBox(e, { node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType }) - } - break - default: { - const customProps = { - position: [opts.e?.canvasX ?? 0, opts.e?.canvasY ?? 0], - nodeType: v, - afterRerouteId, - } satisfies Partial - - const options = Object.assign(opts, customProps) - if (!that.createDefaultNodeForSlot(options)) + if (isFrom) { + if (!opts.nodeFrom) + throw new TypeError( + 'Cannot add node to SubgraphInputNode: nodeFrom was null' + ) + const slot = opts.nodeFrom.connectByType( + iSlotConn, + node, + fromSlotType, + { afterRerouteId } + ) + if (!slot) console.warn('Failed to make new connection.') + // } + } else { + if (!opts.nodeTo) + throw new TypeError( + 'Cannot add node to SubgraphInputNode: nodeTo was null' + ) + opts.nodeTo.connectByTypeOutput(iSlotConn, node, fromSlotType, { + afterRerouteId + }) + } + }) break - } + case 'Add Reroute': { + const node = isFrom ? opts.nodeFrom : opts.nodeTo + const slot = options.extra + + if (!graph) throw new NullGraphError() + if (!node) throw new TypeError('Cannot add reroute: node was null') + if (!slot) throw new TypeError('Cannot add reroute: slot was null') + if (!opts.e) + throw new TypeError( + 'Cannot add reroute: CanvasPointerEvent was null' + ) + + if (node instanceof SubgraphIONodeBase) { + throw new TypeError( + 'Cannot add floating reroute to Subgraph IO Nodes' + ) + } else { + const reroute = node.connectFloatingReroute( + [opts.e.canvasX, opts.e.canvasY], + slot, + afterRerouteId + ) + if (!reroute) throw new Error('Failed to create reroute') + } + + dirty() + break + } + case 'Search': + if (isFrom) { + // @ts-expect-error Subgraph + opts.showSearchBox(e, { + node_from: opts.nodeFrom, + slot_from: slotX, + type_filter_in: fromSlotType + }) + } else { + // @ts-expect-error Subgraph + opts.showSearchBox(e, { + node_to: opts.nodeTo, + slot_from: slotX, + type_filter_out: fromSlotType + }) + } + break + default: { + const customProps = { + position: [opts.e?.canvasX ?? 0, opts.e?.canvasY ?? 0], + nodeType: v, + afterRerouteId + } satisfies Partial + + const options = Object.assign(opts, customProps) + if (!that.createDefaultNodeForSlot(options)) break + } } } } @@ -6194,14 +6607,14 @@ export class LGraphCanvas implements CustomEventDispatcher value: any, callback: (arg0: any) => void, event: CanvasPointerEvent, - multiline?: boolean, + multiline?: boolean ): HTMLDivElement { const that = this - title = title || "" + title = title || '' const customProperties = { is_modified: false, - className: "graphdialog rounded", + className: 'graphdialog rounded', innerHTML: multiline ? " " : " ", @@ -6210,47 +6623,50 @@ export class LGraphCanvas implements CustomEventDispatcher if (dialog.parentNode) { dialog.remove() } - }, + } } satisfies Partial - const div = document.createElement("div") + const div = document.createElement('div') const dialog: PromptDialog = Object.assign(div, customProperties) const graphcanvas = LGraphCanvas.active_canvas const { canvas } = graphcanvas - if (!canvas.parentNode) throw new TypeError("canvas element parentNode was null when opening a prompt.") + if (!canvas.parentNode) + throw new TypeError( + 'canvas element parentNode was null when opening a prompt.' + ) canvas.parentNode.append(dialog) if (this.ds.scale > 1) dialog.style.transform = `scale(${this.ds.scale})` let dialogCloseTimer: number let prevent_timeout = 0 - LiteGraph.pointerListenerAdd(dialog, "leave", function () { + LiteGraph.pointerListenerAdd(dialog, 'leave', function () { if (prevent_timeout) return if (LiteGraph.dialog_close_on_mouse_leave) { if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) { dialogCloseTimer = setTimeout( dialog.close, - LiteGraph.dialog_close_on_mouse_leave_delay, + LiteGraph.dialog_close_on_mouse_leave_delay ) } } }) - LiteGraph.pointerListenerAdd(dialog, "enter", function () { + LiteGraph.pointerListenerAdd(dialog, 'enter', function () { if (LiteGraph.dialog_close_on_mouse_leave && dialogCloseTimer) clearTimeout(dialogCloseTimer) }) - const selInDia = dialog.querySelectorAll("select") + const selInDia = dialog.querySelectorAll('select') if (selInDia) { // if filtering, check focus changed to comboboxes and prevent closing for (const selIn of selInDia) { - selIn.addEventListener("click", function () { + selIn.addEventListener('click', function () { prevent_timeout++ }) - selIn.addEventListener("blur", function () { + selIn.addEventListener('blur', function () { prevent_timeout = 0 }) - selIn.addEventListener("change", function () { + selIn.addEventListener('change', function () { prevent_timeout = -1 }) } @@ -6258,25 +6674,26 @@ export class LGraphCanvas implements CustomEventDispatcher this.prompt_box?.close() this.prompt_box = dialog - const name_element: HTMLSpanElement | null = dialog.querySelector(".name") - if (!name_element) throw new TypeError("name_element was null") + const name_element: HTMLSpanElement | null = dialog.querySelector('.name') + if (!name_element) throw new TypeError('name_element was null') name_element.textContent = title - const value_element: HTMLInputElement | null = dialog.querySelector(".value") - if (!value_element) throw new TypeError("value_element was null") + const value_element: HTMLInputElement | null = + dialog.querySelector('.value') + if (!value_element) throw new TypeError('value_element was null') value_element.value = value value_element.select() const input = value_element - input.addEventListener("keydown", function (e: KeyboardEvent) { + input.addEventListener('keydown', function (e: KeyboardEvent) { dialog.is_modified = true - if (e.key == "Escape") { + if (e.key == 'Escape') { // ESC dialog.close() } else if ( - e.key == "Enter" && - (e.target as Element).localName != "textarea" + e.key == 'Enter' && + (e.target as Element).localName != 'textarea' ) { if (callback) { callback(this.value) @@ -6289,10 +6706,10 @@ export class LGraphCanvas implements CustomEventDispatcher e.stopPropagation() }) - const button = dialog.querySelector("button") - if (!button) throw new TypeError("button was null when opening prompt") + const button = dialog.querySelector('button') + if (!button) throw new TypeError('button was null when opening prompt') - button.addEventListener("click", function () { + button.addEventListener('click', function () { callback?.(input.value) that.setDirty(true) dialog.close() @@ -6320,12 +6737,15 @@ export class LGraphCanvas implements CustomEventDispatcher function handleOutsideClick(e: Event) { if (e.target === canvas && Date.now() - clickTime > 256) { dialog.close() - canvas.parentElement?.removeEventListener("click", handleOutsideClick) - canvas.parentElement?.removeEventListener("touchend", handleOutsideClick) + canvas.parentElement?.removeEventListener('click', handleOutsideClick) + canvas.parentElement?.removeEventListener( + 'touchend', + handleOutsideClick + ) } } - canvas.parentElement?.addEventListener("click", handleOutsideClick) - canvas.parentElement?.addEventListener("touchend", handleOutsideClick) + canvas.parentElement?.addEventListener('click', handleOutsideClick) + canvas.parentElement?.addEventListener('touchend', handleOutsideClick) }, 10) return dialog @@ -6333,7 +6753,7 @@ export class LGraphCanvas implements CustomEventDispatcher showSearchBox( event: MouseEvent, - searchOptions?: IShowSearchOptions, + searchOptions?: IShowSearchOptions ): HTMLDivElement { // proposed defaults const options: IShowSearchOptions = { @@ -6353,7 +6773,7 @@ export class LGraphCanvas implements CustomEventDispatcher show_general_after_typefiltered: true, hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave, show_all_if_empty: true, - show_all_on_open: LiteGraph.search_show_all_on_open, + show_all_on_open: LiteGraph.search_show_all_on_open } Object.assign(options, searchOptions) @@ -6363,42 +6783,45 @@ export class LGraphCanvas implements CustomEventDispatcher const { canvas } = graphcanvas const root_document = canvas.ownerDocument || document - const div = document.createElement("div") + const div = document.createElement('div') const dialog = Object.assign(div, { close(this: typeof div) { that.search_box = undefined this.blur() canvas.focus() - root_document.body.style.overflow = "" + root_document.body.style.overflow = '' // important, if canvas loses focus keys wont be captured setTimeout(() => canvas.focus(), 20) dialog.remove() - }, + } } satisfies Partial & ICloseable) - dialog.className = "litegraph litesearchbox graphdialog rounded" - dialog.innerHTML = "Search " + dialog.className = 'litegraph litesearchbox graphdialog rounded' + dialog.innerHTML = + "Search " if (options.do_type_filter) { - dialog.innerHTML += "" - dialog.innerHTML += "" + dialog.innerHTML += + "" + dialog.innerHTML += + "" } - const helper = document.createElement("div") - helper.className = "helper" + const helper = document.createElement('div') + helper.className = 'helper' dialog.append(helper) if (root_document.fullscreenElement) { root_document.fullscreenElement.append(dialog) } else { root_document.body.append(dialog) - root_document.body.style.overflow = "hidden" + root_document.body.style.overflow = 'hidden' } // dialog element has been appended let selIn let selOut if (options.do_type_filter) { - selIn = dialog.querySelector(".slot_in_type_filter") - selOut = dialog.querySelector(".slot_out_type_filter") + selIn = dialog.querySelector('.slot_in_type_filter') + selOut = dialog.querySelector('.slot_out_type_filter') } if (this.ds.scale > 1) { @@ -6410,40 +6833,42 @@ export class LGraphCanvas implements CustomEventDispatcher // FIXME: Remove "any" kludge let prevent_timeout: any = false let timeout_close: number | null = null - LiteGraph.pointerListenerAdd(dialog, "enter", function () { + LiteGraph.pointerListenerAdd(dialog, 'enter', function () { if (timeout_close) { clearTimeout(timeout_close) timeout_close = null } }) - dialog.addEventListener("pointerleave", function () { + dialog.addEventListener('pointerleave', function () { if (prevent_timeout) return const hideDelay = options.hide_on_mouse_leave - const delay = typeof hideDelay === "number" ? hideDelay : 500 + const delay = typeof hideDelay === 'number' ? hideDelay : 500 timeout_close = setTimeout(dialog.close, delay) }) // if filtering, check focus changed to comboboxes and prevent closing if (options.do_type_filter) { - if (!selIn) throw new TypeError("selIn was null when showing search box") - if (!selOut) throw new TypeError("selOut was null when showing search box") + if (!selIn) + throw new TypeError('selIn was null when showing search box') + if (!selOut) + throw new TypeError('selOut was null when showing search box') - selIn.addEventListener("click", function () { + selIn.addEventListener('click', function () { prevent_timeout++ }) - selIn.addEventListener("blur", function () { + selIn.addEventListener('blur', function () { prevent_timeout = 0 }) - selIn.addEventListener("change", function () { + selIn.addEventListener('change', function () { prevent_timeout = -1 }) - selOut.addEventListener("click", function () { + selOut.addEventListener('click', function () { prevent_timeout++ }) - selOut.addEventListener("blur", function () { + selOut.addEventListener('blur', function () { prevent_timeout = 0 }) - selOut.addEventListener("change", function () { + selOut.addEventListener('change', function () { prevent_timeout = -1 }) } @@ -6457,28 +6882,28 @@ export class LGraphCanvas implements CustomEventDispatcher let timeout: number | null = null let selected: ChildNode | null = null - const maybeInput = dialog.querySelector("input") - if (!maybeInput) throw new TypeError("Could not create search input box.") + const maybeInput = dialog.querySelector('input') + if (!maybeInput) throw new TypeError('Could not create search input box.') const input = maybeInput if (input) { - input.addEventListener("blur", function () { + input.addEventListener('blur', function () { this.focus() }) - input.addEventListener("keydown", function (e) { - if (e.key == "ArrowUp") { + input.addEventListener('keydown', function (e) { + if (e.key == 'ArrowUp') { // UP changeSelection(false) - } else if (e.key == "ArrowDown") { + } else if (e.key == 'ArrowDown') { // DOWN changeSelection(true) - } else if (e.key == "Escape") { + } else if (e.key == 'Escape') { // ESC dialog.close() - } else if (e.key == "Enter") { + } else if (e.key == 'Enter') { if (selected instanceof HTMLElement) { - select(unescape(String(selected.dataset["type"]))) + select(unescape(String(selected.dataset['type']))) } else if (first) { select(first) } else { @@ -6508,10 +6933,10 @@ export class LGraphCanvas implements CustomEventDispatcher options.type_filter_in == LiteGraph.EVENT || options.type_filter_in == LiteGraph.ACTION ) { - options.type_filter_in = "_event_" + options.type_filter_in = '_event_' } for (let iK = 0; iK < nSlots; iK++) { - const opt = document.createElement("option") + const opt = document.createElement('option') opt.value = aSlots[iK] opt.innerHTML = aSlots[iK] selIn.append(opt) @@ -6519,12 +6944,12 @@ export class LGraphCanvas implements CustomEventDispatcher // @ts-expect-error options.type_filter_in !== false && String(options.type_filter_in).toLowerCase() == - String(aSlots[iK]).toLowerCase() + String(aSlots[iK]).toLowerCase() ) { opt.selected = true } } - selIn.addEventListener("change", function () { + selIn.addEventListener('change', function () { refreshHelper() }) } @@ -6535,22 +6960,22 @@ export class LGraphCanvas implements CustomEventDispatcher options.type_filter_out == LiteGraph.EVENT || options.type_filter_out == LiteGraph.ACTION ) { - options.type_filter_out = "_event_" + options.type_filter_out = '_event_' } for (const aSlot of aSlots) { - const opt = document.createElement("option") + const opt = document.createElement('option') opt.value = aSlot opt.innerHTML = aSlot selOut.append(opt) if ( options.type_filter_out !== false && String(options.type_filter_out).toLowerCase() == - String(aSlot).toLowerCase() + String(aSlot).toLowerCase() ) { opt.selected = true } } - selOut.addEventListener("change", function () { + selOut.addEventListener('change', function () { refreshHelper() }) } @@ -6592,30 +7017,41 @@ export class LGraphCanvas implements CustomEventDispatcher // FIXME: any let iS: any = false switch (typeof options.slot_from) { - case "string": - iS = options.node_from.findOutputSlot(options.slot_from) - break - case "object": - if (options.slot_from == null) throw new TypeError("options.slot_from was null when showing search box") + case 'string': + iS = options.node_from.findOutputSlot(options.slot_from) + break + case 'object': + if (options.slot_from == null) + throw new TypeError( + 'options.slot_from was null when showing search box' + ) - iS = options.slot_from.name - ? options.node_from.findOutputSlot(options.slot_from.name) - : -1 - // @ts-expect-error change interface check - if (iS == -1 && options.slot_from.slot_index !== undefined) iS = options.slot_from.slot_index - break - case "number": - iS = options.slot_from - break - default: - // try with first if no name set - iS = 0 + iS = options.slot_from.name + ? options.node_from.findOutputSlot(options.slot_from.name) + : -1 + // @ts-expect-error change interface check + if (iS == -1 && options.slot_from.slot_index !== undefined) + iS = options.slot_from.slot_index + break + case 'number': + iS = options.slot_from + break + default: + // try with first if no name set + iS = 0 } if (options.node_from.outputs[iS] !== undefined) { if (iS !== false && iS > -1) { - if (node == null) throw new TypeError("options.slot_from was null when showing search box") + if (node == null) + throw new TypeError( + 'options.slot_from was null when showing search box' + ) - options.node_from.connectByType(iS, node, options.node_from.outputs[iS].type) + options.node_from.connectByType( + iS, + node, + options.node_from.outputs[iS].type + ) } } else { // console.warn("cant find slot " + options.slot_from); @@ -6625,30 +7061,41 @@ export class LGraphCanvas implements CustomEventDispatcher // FIXME: any let iS: any = false switch (typeof options.slot_from) { - case "string": - iS = options.node_to.findInputSlot(options.slot_from) - break - case "object": - if (options.slot_from == null) throw new TypeError("options.slot_from was null when showing search box") + case 'string': + iS = options.node_to.findInputSlot(options.slot_from) + break + case 'object': + if (options.slot_from == null) + throw new TypeError( + 'options.slot_from was null when showing search box' + ) - iS = options.slot_from.name - ? options.node_to.findInputSlot(options.slot_from.name) - : -1 - // @ts-expect-error change interface check - if (iS == -1 && options.slot_from.slot_index !== undefined) iS = options.slot_from.slot_index - break - case "number": - iS = options.slot_from - break - default: - // try with first if no name set - iS = 0 + iS = options.slot_from.name + ? options.node_to.findInputSlot(options.slot_from.name) + : -1 + // @ts-expect-error change interface check + if (iS == -1 && options.slot_from.slot_index !== undefined) + iS = options.slot_from.slot_index + break + case 'number': + iS = options.slot_from + break + default: + // try with first if no name set + iS = 0 } if (options.node_to.inputs[iS] !== undefined) { if (iS !== false && iS > -1) { - if (node == null) throw new TypeError("options.slot_from was null when showing search box") + if (node == null) + throw new TypeError( + 'options.slot_from was null when showing search box' + ) // try connection - options.node_to.connectByTypeOutput(iS, node, options.node_to.inputs[iS].type) + options.node_to.connectByTypeOutput( + iS, + node, + options.node_to.inputs[iS].type + ) } } else { // console.warn("cant find slot_nodeTO " + options.slot_from); @@ -6669,16 +7116,14 @@ export class LGraphCanvas implements CustomEventDispatcher ? helper.childNodes[0] : helper.childNodes[helper.childNodes.length] } else if (selected instanceof Element) { - selected.classList.remove("selected") - selected = forward - ? selected.nextSibling - : selected.previousSibling + selected.classList.remove('selected') + selected = forward ? selected.nextSibling : selected.previousSibling selected ||= prev } if (selected instanceof Element) { - selected.classList.add("selected") - selected.scrollIntoView({ block: "end", behavior: "smooth" }) + selected.classList.add('selected') + selected.scrollIntoView({ block: 'end', behavior: 'smooth' }) } } @@ -6686,7 +7131,7 @@ export class LGraphCanvas implements CustomEventDispatcher timeout = null let str = input.value first = null - helper.innerHTML = "" + helper.innerHTML = '' if (!str && !options.show_all_if_empty) return if (that.onSearchBox) { @@ -6708,16 +7153,19 @@ export class LGraphCanvas implements CustomEventDispatcher let sIn: any = false let sOut: any = false if (options.do_type_filter && that.search_box) { - sIn = that.search_box.querySelector(".slot_in_type_filter") - sOut = that.search_box.querySelector(".slot_out_type_filter") + sIn = that.search_box.querySelector('.slot_in_type_filter') + sOut = that.search_box.querySelector('.slot_out_type_filter') } const keys = Object.keys(LiteGraph.registered_node_types) - const filtered = keys.filter(x => inner_test_filter(x)) + const filtered = keys.filter((x) => inner_test_filter(x)) for (const item of filtered) { addResult(item) - if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) + if ( + LGraphCanvas.search_limit !== -1 && + c++ > LGraphCanvas.search_limit + ) break } @@ -6732,8 +7180,8 @@ export class LGraphCanvas implements CustomEventDispatcher for (const i in LiteGraph.registered_node_types) { if ( inner_test_filter(i, { - inTypeOverride: sIn && sIn.value ? "*" : false, - outTypeOverride: sOut && sOut.value ? "*" : false, + inTypeOverride: sIn && sIn.value ? '*' : false, + outTypeOverride: sOut && sOut.value ? '*' : false }) ) { // @ts-expect-error @@ -6742,8 +7190,11 @@ export class LGraphCanvas implements CustomEventDispatcher } // @ts-expect-error for (const extraItem of filtered_extra) { - addResult(extraItem, "generic_type") - if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) + addResult(extraItem, 'generic_type') + if ( + LGraphCanvas.search_limit !== -1 && + c++ > LGraphCanvas.search_limit + ) break } } @@ -6763,8 +7214,11 @@ export class LGraphCanvas implements CustomEventDispatcher } // @ts-expect-error for (const extraItem of filtered_extra) { - addResult(extraItem, "not_in_filter") - if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) + addResult(extraItem, 'not_in_filter') + if ( + LGraphCanvas.search_limit !== -1 && + c++ > LGraphCanvas.search_limit + ) break } } @@ -6775,13 +7229,13 @@ export class LGraphCanvas implements CustomEventDispatcher inTypeOverride?: string | boolean outTypeOverride?: string | boolean skipFilter?: boolean - }, + } ): boolean { optsIn = optsIn || {} const optsDef = { skipFilter: false, inTypeOverride: false, - outTypeOverride: false, + outTypeOverride: false } const opts = Object.assign(optsDef, optsIn) const ctor = LiteGraph.registered_node_types[type] @@ -6798,12 +7252,12 @@ export class LGraphCanvas implements CustomEventDispatcher if (options.do_type_filter && !opts.skipFilter) { const sType = type - let sV = opts.inTypeOverride !== false - ? opts.inTypeOverride - : sIn.value + let sV = + opts.inTypeOverride !== false ? opts.inTypeOverride : sIn.value // type is stored if (sIn && sV && LiteGraph.registered_slot_in_types[sV]?.nodes) { - const doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType) + const doesInc = + LiteGraph.registered_slot_in_types[sV].nodes.includes(sType) if (doesInc === false) return false } @@ -6811,7 +7265,8 @@ export class LGraphCanvas implements CustomEventDispatcher if (opts.outTypeOverride !== false) sV = opts.outTypeOverride // type is stored if (sOut && sV && LiteGraph.registered_slot_out_types[sV]?.nodes) { - const doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType) + const doesInc = + LiteGraph.registered_slot_out_types[sV].nodes.includes(sType) if (doesInc === false) return false } } @@ -6820,27 +7275,27 @@ export class LGraphCanvas implements CustomEventDispatcher } function addResult(type: string, className?: string): void { - const help = document.createElement("div") + const help = document.createElement('div') first ||= type const nodeType = LiteGraph.registered_node_types[type] if (nodeType?.title) { help.textContent = nodeType?.title - const typeEl = document.createElement("span") - typeEl.className = "litegraph lite-search-item-type" + const typeEl = document.createElement('span') + typeEl.className = 'litegraph lite-search-item-type' typeEl.textContent = type help.append(typeEl) } else { help.textContent = type } - help.dataset["type"] = escape(type) - help.className = "litegraph lite-search-item" + help.dataset['type'] = escape(type) + help.className = 'litegraph lite-search-item' if (className) { help.className += ` ${className}` } - help.addEventListener("click", function () { - select(unescape(String(this.dataset["type"]))) + help.addEventListener('click', function () { + select(unescape(String(this.dataset['type']))) }) helper.append(help) } @@ -6852,7 +7307,7 @@ export class LGraphCanvas implements CustomEventDispatcher showEditPropertyValue( node: LGraphNode, property: string, - options: IDialogOptions, + options: IDialogOptions ): IDialog | undefined { if (!node || node.properties[property] === undefined) return @@ -6861,26 +7316,26 @@ export class LGraphCanvas implements CustomEventDispatcher const info = node.getPropertyInfo(property) const { type } = info - let input_html = "" + let input_html = '' if ( - type == "string" || - type == "number" || - type == "array" || - type == "object" + type == 'string' || + type == 'number' || + type == 'array' || + type == 'object' ) { input_html = "" - } else if ((type == "enum" || type == "combo") && info.values) { + } else if ((type == 'enum' || type == 'combo') && info.values) { input_html = "" - } else if (type == "boolean" || type == "toggle") { - const checked = node.properties[property] ? "checked" : "" + input_html += '' + } else if (type == 'boolean' || type == 'toggle') { + const checked = node.properties[property] ? 'checked' : '' input_html = `` } else { console.warn(`unknown type: ${type}`) @@ -6889,44 +7344,45 @@ export class LGraphCanvas implements CustomEventDispatcher const dialog = this.createDialog( `${info.label || property}${input_html}`, - options, + options ) let input: HTMLInputElement | HTMLSelectElement | null - if ((type == "enum" || type == "combo") && info.values) { - input = dialog.querySelector("select") - input?.addEventListener("change", function (e) { + if ((type == 'enum' || type == 'combo') && info.values) { + input = dialog.querySelector('select') + input?.addEventListener('change', function (e) { dialog.modified() setValue((e.target as HTMLSelectElement)?.value) }) - } else if (type == "boolean" || type == "toggle") { - input = dialog.querySelector("input") - input?.addEventListener("click", function () { + } else if (type == 'boolean' || type == 'toggle') { + input = dialog.querySelector('input') + input?.addEventListener('click', function () { dialog.modified() // @ts-expect-error setValue(!!input.checked) }) } else { - input = dialog.querySelector("input") + input = dialog.querySelector('input') if (input) { - input.addEventListener("blur", function () { + input.addEventListener('blur', function () { this.focus() }) - let v = node.properties[property] !== undefined - ? node.properties[property] - : "" - if (type !== "string") { + let v = + node.properties[property] !== undefined + ? node.properties[property] + : '' + if (type !== 'string') { v = JSON.stringify(v) } // @ts-expect-error input.value = v - input.addEventListener("keydown", function (e) { - if (e.key == "Escape") { + input.addEventListener('keydown', function (e) { + if (e.key == 'Escape') { // ESC dialog.close() - } else if (e.key == "Enter") { + } else if (e.key == 'Enter') { // ENTER // save inner() @@ -6941,9 +7397,10 @@ export class LGraphCanvas implements CustomEventDispatcher } input?.focus() - const button = dialog.querySelector("button") - if (!button) throw new TypeError("Show edit property value button was null.") - button.addEventListener("click", inner) + const button = dialog.querySelector('button') + if (!button) + throw new TypeError('Show edit property value button was null.') + button.addEventListener('click', inner) function inner() { setValue(input?.value) @@ -6953,16 +7410,16 @@ export class LGraphCanvas implements CustomEventDispatcher function setValue(value: string | number | undefined) { if ( info?.values && - typeof info.values === "object" && + typeof info.values === 'object' && info.values[value] != undefined ) { value = info.values[value] } - if (typeof node.properties[property] == "number") { + if (typeof node.properties[property] == 'number') { value = Number(value) } - if (type == "array" || type == "object") { + if (type == 'array' || type == 'object') { // @ts-expect-error JSON.parse doesn't care. value = JSON.parse(value) } @@ -6984,12 +7441,12 @@ export class LGraphCanvas implements CustomEventDispatcher const def_options = { checkForInput: false, closeOnLeave: true, - closeOnLeave_checkModified: true, + closeOnLeave_checkModified: true } options = Object.assign(def_options, options || {}) const customProperties = { - className: "graphdialog", + className: 'graphdialog', innerHTML: html, is_modified: false, modified() { @@ -6997,10 +7454,10 @@ export class LGraphCanvas implements CustomEventDispatcher }, close(this: IDialog) { this.remove() - }, + } } satisfies Partial - const div = document.createElement("div") + const div = document.createElement('div') const dialog: IDialog = Object.assign(div, customProperties) const rect = this.canvas.getBoundingClientRect() @@ -7026,19 +7483,20 @@ export class LGraphCanvas implements CustomEventDispatcher dialog.style.left = `${offsetx}px` dialog.style.top = `${offsety}px` - if (!this.canvas.parentNode) throw new TypeError("Canvas parent element was null.") + if (!this.canvas.parentNode) + throw new TypeError('Canvas parent element was null.') this.canvas.parentNode.append(dialog) // acheck for input and use default behaviour: save on enter, close on esc if (options.checkForInput) { - const aI = dialog.querySelectorAll("input") + const aI = dialog.querySelectorAll('input') if (aI) { for (const iX of aI) { - iX.addEventListener("keydown", function (e) { + iX.addEventListener('keydown', function (e) { dialog.modified() - if (e.key == "Escape") { + if (e.key == 'Escape') { dialog.close() - } else if (e.key != "Enter") { + } else if (e.key != 'Enter') { return } e.preventDefault() @@ -7051,32 +7509,32 @@ export class LGraphCanvas implements CustomEventDispatcher let dialogCloseTimer: number let prevent_timeout = 0 - dialog.addEventListener("mouseleave", function () { + dialog.addEventListener('mouseleave', function () { if (prevent_timeout) return if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) { dialogCloseTimer = setTimeout( dialog.close, - LiteGraph.dialog_close_on_mouse_leave_delay, + LiteGraph.dialog_close_on_mouse_leave_delay ) } }) - dialog.addEventListener("mouseenter", function () { + dialog.addEventListener('mouseenter', function () { if (options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) { if (dialogCloseTimer) clearTimeout(dialogCloseTimer) } }) - const selInDia = dialog.querySelectorAll("select") + const selInDia = dialog.querySelectorAll('select') // if filtering, check focus changed to comboboxes and prevent closing if (selInDia) { for (const selIn of selInDia) { - selIn.addEventListener("click", function () { + selIn.addEventListener('click', function () { prevent_timeout++ }) - selIn.addEventListener("blur", function () { + selIn.addEventListener('blur', function () { prevent_timeout = 0 }) - selIn.addEventListener("change", function () { + selIn.addEventListener('change', function () { prevent_timeout = -1 }) } @@ -7090,32 +7548,35 @@ export class LGraphCanvas implements CustomEventDispatcher const ref_window = options.window || window // TODO: any kludge - const root: any = document.createElement("div") - root.className = "litegraph dialog" - root.innerHTML = "
" - root.header = root.querySelector(".dialog-header") + const root: any = document.createElement('div') + root.className = 'litegraph dialog' + root.innerHTML = + "
" + root.header = root.querySelector('.dialog-header') if (options.width) - root.style.width = options.width + (typeof options.width === "number" ? "px" : "") + root.style.width = + options.width + (typeof options.width === 'number' ? 'px' : '') if (options.height) - root.style.height = options.height + (typeof options.height === "number" ? "px" : "") + root.style.height = + options.height + (typeof options.height === 'number' ? 'px' : '') if (options.closable) { - const close = document.createElement("span") - close.innerHTML = "✕" - close.classList.add("close") - close.addEventListener("click", function () { + const close = document.createElement('span') + close.innerHTML = '✕' + close.classList.add('close') + close.addEventListener('click', function () { root.close() }) root.header.append(close) } - root.title_element = root.querySelector(".dialog-title") + root.title_element = root.querySelector('.dialog-title') root.title_element.textContent = title - root.content = root.querySelector(".dialog-content") - root.alt_content = root.querySelector(".dialog-alt-content") - root.footer = root.querySelector(".dialog-footer") + root.content = root.querySelector('.dialog-content') + root.alt_content = root.querySelector('.dialog-alt-content') + root.footer = root.querySelector('.dialog-footer') root.close = function () { - if (typeof root.onClose == "function") root.onClose() + if (typeof root.onClose == 'function') root.onClose() root.remove() this.remove() } @@ -7125,11 +7586,11 @@ export class LGraphCanvas implements CustomEventDispatcher let vTo: string let vAlt: string if (force !== undefined) { - vTo = force ? "block" : "none" - vAlt = force ? "none" : "block" + vTo = force ? 'block' : 'none' + vAlt = force ? 'none' : 'block' } else { - vTo = root.alt_content.style.display != "block" ? "block" : "none" - vAlt = root.alt_content.style.display != "block" ? "none" : "block" + vTo = root.alt_content.style.display != 'block' ? 'block' : 'none' + vAlt = root.alt_content.style.display != 'block' ? 'none' : 'block' } root.alt_content.style.display = vTo root.content.style.display = vAlt @@ -7138,19 +7599,19 @@ export class LGraphCanvas implements CustomEventDispatcher root.toggleFooterVisibility = function (force: unknown) { let vTo: string if (force !== undefined) { - vTo = force ? "block" : "none" + vTo = force ? 'block' : 'none' } else { - vTo = root.footer.style.display != "block" ? "block" : "none" + vTo = root.footer.style.display != 'block' ? 'block' : 'none' } root.footer.style.display = vTo } root.clear = function () { - this.content.innerHTML = "" + this.content.innerHTML = '' } root.addHTML = function (code: string, classname: string, on_footer: any) { - const elem = document.createElement("div") + const elem = document.createElement('div') if (classname) elem.className = classname elem.innerHTML = code if (on_footer) root.footer.append(elem) @@ -7160,87 +7621,101 @@ export class LGraphCanvas implements CustomEventDispatcher root.addButton = function (name: any, callback: any, options: any) { // TODO: any kludge - const elem: any = document.createElement("button") + const elem: any = document.createElement('button') elem.textContent = name elem.options = options - elem.classList.add("btn") - elem.addEventListener("click", callback) + elem.classList.add('btn') + elem.addEventListener('click', callback) root.footer.append(elem) return elem } root.addSeparator = function () { - const elem = document.createElement("div") - elem.className = "separator" + const elem = document.createElement('div') + elem.className = 'separator' root.content.append(elem) } - root.addWidget = function (type: string, name: any, value: unknown, options: { label?: any, type?: any, values?: any, callback?: any }, callback: (arg0: any, arg1: any, arg2: any) => void) { + root.addWidget = function ( + type: string, + name: any, + value: unknown, + options: { label?: any; type?: any; values?: any; callback?: any }, + callback: (arg0: any, arg1: any, arg2: any) => void + ) { options = options || {} let str_value = String(value) type = type.toLowerCase() - if (type == "number" && typeof value === "number") str_value = value.toFixed(3) + if (type == 'number' && typeof value === 'number') + str_value = value.toFixed(3) // FIXME: any kludge - const elem: HTMLDivElement & { options?: unknown, value?: unknown } = document.createElement("div") - elem.className = "property" - elem.innerHTML = "" - const nameSpan = elem.querySelector(".property_name") - if (!nameSpan) throw new TypeError("Property name element was null.") + const elem: HTMLDivElement & { options?: unknown; value?: unknown } = + document.createElement('div') + elem.className = 'property' + elem.innerHTML = + "" + const nameSpan = elem.querySelector('.property_name') + if (!nameSpan) throw new TypeError('Property name element was null.') nameSpan.textContent = options.label || name // TODO: any kludge - const value_element: HTMLSpanElement | null = elem.querySelector(".property_value") - if (!value_element) throw new TypeError("Property name element was null.") + const value_element: HTMLSpanElement | null = + elem.querySelector('.property_value') + if (!value_element) throw new TypeError('Property name element was null.') value_element.textContent = str_value - elem.dataset["property"] = name - elem.dataset["type"] = options.type || type + elem.dataset['property'] = name + elem.dataset['type'] = options.type || type elem.options = options elem.value = value - if (type == "code") { - elem.addEventListener("click", function () { - root.inner_showCodePad(this.dataset["property"]) + if (type == 'code') { + elem.addEventListener('click', function () { + root.inner_showCodePad(this.dataset['property']) }) - } else if (type == "boolean") { - elem.classList.add("boolean") - if (value) elem.classList.add("bool-on") - elem.addEventListener("click", () => { - const propname = elem.dataset["property"] + } else if (type == 'boolean') { + elem.classList.add('boolean') + if (value) elem.classList.add('bool-on') + elem.addEventListener('click', () => { + const propname = elem.dataset['property'] elem.value = !elem.value - elem.classList.toggle("bool-on") - if (!value_element) throw new TypeError("Property name element was null.") + elem.classList.toggle('bool-on') + if (!value_element) + throw new TypeError('Property name element was null.') - value_element.textContent = elem.value - ? "true" - : "false" + value_element.textContent = elem.value ? 'true' : 'false' innerChange(propname, elem.value) }) - } else if (type == "string" || type == "number") { - if (!value_element) throw new TypeError("Property name element was null.") - value_element.setAttribute("contenteditable", "true") - value_element.addEventListener("keydown", function (e) { + } else if (type == 'string' || type == 'number') { + if (!value_element) + throw new TypeError('Property name element was null.') + value_element.setAttribute('contenteditable', 'true') + value_element.addEventListener('keydown', function (e) { // allow for multiline - if (e.code == "Enter" && (type != "string" || !e.shiftKey)) { + if (e.code == 'Enter' && (type != 'string' || !e.shiftKey)) { e.preventDefault() this.blur() } }) - value_element.addEventListener("blur", function () { + value_element.addEventListener('blur', function () { let v: string | number | null = this.textContent - const propname = this.parentElement?.dataset["property"] - const proptype = this.parentElement?.dataset["type"] - if (proptype == "number") v = Number(v) + const propname = this.parentElement?.dataset['property'] + const proptype = this.parentElement?.dataset['type'] + if (proptype == 'number') v = Number(v) innerChange(propname, v) }) - } else if (type == "enum" || type == "combo") { - const str_value = LGraphCanvas.getPropertyPrintableValue(value, options.values) - if (!value_element) throw new TypeError("Property name element was null.") - value_element.textContent = str_value ?? "" + } else if (type == 'enum' || type == 'combo') { + const str_value = LGraphCanvas.getPropertyPrintableValue( + value, + options.values + ) + if (!value_element) + throw new TypeError('Property name element was null.') + value_element.textContent = str_value ?? '' - value_element.addEventListener("click", function (event) { + value_element.addEventListener('click', function (event) { const values = options.values || [] - const propname = this.parentElement?.dataset["property"] + const propname = this.parentElement?.dataset['property'] const inner_clicked = (v: string | null) => { // node.setProperty(propname,v); // graphcanvas.dirty_canvas = true; @@ -7252,11 +7727,11 @@ export class LGraphCanvas implements CustomEventDispatcher values, { event, - className: "dark", - callback: inner_clicked, + className: 'dark', + callback: inner_clicked }, // @ts-expect-error - ref_window, + ref_window ) }) } @@ -7271,22 +7746,22 @@ export class LGraphCanvas implements CustomEventDispatcher return elem } - if (typeof root.onOpen == "function") root.onOpen() + if (typeof root.onOpen == 'function') root.onOpen() return root } closePanels(): void { type MightHaveClose = HTMLDivElement & Partial - document.querySelector("#node-panel")?.close?.() - document.querySelector("#option-panel")?.close?.() + document.querySelector('#node-panel')?.close?.() + document.querySelector('#option-panel')?.close?.() } showShowNodePanel(node: LGraphNode): void { this.SELECTED_NODE = node this.closePanels() const ref_window = this.getCanvasWindow() - const panel = this.createPanel(node.title || "", { + const panel = this.createPanel(node.title || '', { closable: true, window: ref_window, onOpen: () => { @@ -7295,69 +7770,97 @@ export class LGraphCanvas implements CustomEventDispatcher onClose: () => { this.NODEPANEL_IS_OPEN = false this.node_panel = null - }, + } }) this.node_panel = panel - panel.id = "node-panel" + panel.id = 'node-panel' panel.node = node - panel.classList.add("settings") + panel.classList.add('settings') const inner_refresh = () => { // clear - panel.content.innerHTML = "" + panel.content.innerHTML = '' // @ts-expect-error ctor props - panel.addHTML(`${node.type}${node.constructor.desc || ""}`) + panel.addHTML( + `${node.type}${node.constructor.desc || ''}` + ) - panel.addHTML("

Properties

") + panel.addHTML('

Properties

') - const fUpdate = (name: string, value: string | number | boolean | object | undefined) => { + const fUpdate = ( + name: string, + value: string | number | boolean | object | undefined + ) => { if (!this.graph) throw new NullGraphError() this.graph.beforeChange(node) switch (name) { - case "Title": - if (typeof value !== "string") throw new TypeError("Attempting to set title to non-string value.") + case 'Title': + if (typeof value !== 'string') + throw new TypeError( + 'Attempting to set title to non-string value.' + ) - node.title = value - break - case "Mode": { - if (typeof value !== "string") throw new TypeError("Attempting to set mode to non-string value.") + node.title = value + break + case 'Mode': { + if (typeof value !== 'string') + throw new TypeError('Attempting to set mode to non-string value.') - const kV = Object.values(LiteGraph.NODE_MODES).indexOf(value) - if (kV !== -1 && LiteGraph.NODE_MODES[kV]) { - node.changeMode(kV) - } else { - console.warn(`unexpected mode: ${value}`) + const kV = Object.values(LiteGraph.NODE_MODES).indexOf(value) + if (kV !== -1 && LiteGraph.NODE_MODES[kV]) { + node.changeMode(kV) + } else { + console.warn(`unexpected mode: ${value}`) + } + break } - break - } - case "Color": - if (typeof value !== "string") throw new TypeError("Attempting to set colour to non-string value.") + case 'Color': + if (typeof value !== 'string') + throw new TypeError( + 'Attempting to set colour to non-string value.' + ) - if (LGraphCanvas.node_colors[value]) { - node.color = LGraphCanvas.node_colors[value].color - node.bgcolor = LGraphCanvas.node_colors[value].bgcolor - } else { - console.warn(`unexpected color: ${value}`) - } - break - default: - node.setProperty(name, value) - break + if (LGraphCanvas.node_colors[value]) { + node.color = LGraphCanvas.node_colors[value].color + node.bgcolor = LGraphCanvas.node_colors[value].bgcolor + } else { + console.warn(`unexpected color: ${value}`) + } + break + default: + node.setProperty(name, value) + break } this.graph.afterChange() this.dirty_canvas = true } - panel.addWidget("string", "Title", node.title, {}, fUpdate) + panel.addWidget('string', 'Title', node.title, {}, fUpdate) - const mode = node.mode == null ? undefined : LiteGraph.NODE_MODES[node.mode] - panel.addWidget("combo", "Mode", mode, { values: LiteGraph.NODE_MODES }, fUpdate) + const mode = + node.mode == null ? undefined : LiteGraph.NODE_MODES[node.mode] + panel.addWidget( + 'combo', + 'Mode', + mode, + { values: LiteGraph.NODE_MODES }, + fUpdate + ) - const nodeCol = node.color !== undefined - ? Object.keys(LGraphCanvas.node_colors).filter(function (nK) { return LGraphCanvas.node_colors[nK].color == node.color }) - : "" + const nodeCol = + node.color !== undefined + ? Object.keys(LGraphCanvas.node_colors).filter(function (nK) { + return LGraphCanvas.node_colors[nK].color == node.color + }) + : '' - panel.addWidget("combo", "Color", nodeCol, { values: Object.keys(LGraphCanvas.node_colors) }, fUpdate) + panel.addWidget( + 'combo', + 'Color', + nodeCol, + { values: Object.keys(LGraphCanvas.node_colors) }, + fUpdate + ) for (const pName in node.properties) { const value = node.properties[pName] @@ -7374,62 +7877,67 @@ export class LGraphCanvas implements CustomEventDispatcher node.onShowCustomPanelInfo?.(panel) // clear - panel.footer.innerHTML = "" - panel.addButton("Delete", function () { - if (node.block_delete) return - if (!node.graph) throw new NullGraphError() + panel.footer.innerHTML = '' + panel + .addButton('Delete', function () { + if (node.block_delete) return + if (!node.graph) throw new NullGraphError() - node.graph.remove(node) - panel.close() - }).classList.add("delete") + node.graph.remove(node) + panel.close() + }) + .classList.add('delete') } panel.inner_showCodePad = function (propname: string) { - panel.classList.remove("settings") - panel.classList.add("centered") + panel.classList.remove('settings') + panel.classList.add('centered') panel.alt_content.innerHTML = "" - const textarea: HTMLTextAreaElement = panel.alt_content.querySelector("textarea") + const textarea: HTMLTextAreaElement = + panel.alt_content.querySelector('textarea') const fDoneWith = function () { panel.toggleAltContent(false) panel.toggleFooterVisibility(true) textarea.remove() - panel.classList.add("settings") - panel.classList.remove("centered") + panel.classList.add('settings') + panel.classList.remove('centered') inner_refresh() } textarea.value = String(node.properties[propname]) - textarea.addEventListener("keydown", function (e: KeyboardEvent) { - if (e.code == "Enter" && e.ctrlKey) { + textarea.addEventListener('keydown', function (e: KeyboardEvent) { + if (e.code == 'Enter' && e.ctrlKey) { node.setProperty(propname, textarea.value) fDoneWith() } }) panel.toggleAltContent(true) panel.toggleFooterVisibility(false) - textarea.style.height = "calc(100% - 40px)" + textarea.style.height = 'calc(100% - 40px)' - const assign = panel.addButton("Assign", function () { + const assign = panel.addButton('Assign', function () { node.setProperty(propname, textarea.value) fDoneWith() }) panel.alt_content.append(assign) - const button = panel.addButton("Close", fDoneWith) - button.style.float = "right" + const button = panel.addButton('Close', fDoneWith) + button.style.float = 'right' panel.alt_content.append(button) } inner_refresh() - if (!this.canvas.parentNode) throw new TypeError("showNodePanel - this.canvas.parentNode was null") + if (!this.canvas.parentNode) + throw new TypeError('showNodePanel - this.canvas.parentNode was null') this.canvas.parentNode.append(panel) } checkPanels(): void { if (!this.canvas) return - if (!this.canvas.parentNode) throw new TypeError("checkPanels - this.canvas.parentNode was null") - const panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog") + if (!this.canvas.parentNode) + throw new TypeError('checkPanels - this.canvas.parentNode was null') + const panels = this.canvas.parentNode.querySelectorAll('.litegraph.dialog') for (const panel of panels) { // @ts-expect-error Panel if (!panel.node) continue @@ -7445,122 +7953,140 @@ export class LGraphCanvas implements CustomEventDispatcher } else { options = [ { - content: "Add Node", + content: 'Add Node', has_submenu: true, - callback: LGraphCanvas.onMenuAdd, + callback: LGraphCanvas.onMenuAdd }, - { content: "Add Group", callback: LGraphCanvas.onGroupAdd }, + { content: 'Add Group', callback: LGraphCanvas.onGroupAdd } // { content: "Arrange", callback: that.graph.arrange }, // {content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } ] if (Object.keys(this.selected_nodes).length > 1) { - options.push({ - content: "Convert to Subgraph 🆕", - callback: () => { - if (!this.selectedItems.size) throw new Error("Convert to Subgraph: Nothing selected.") - this._graph.convertToSubgraph(this.selectedItems) + options.push( + { + content: 'Convert to Subgraph 🆕', + callback: () => { + if (!this.selectedItems.size) + throw new Error('Convert to Subgraph: Nothing selected.') + this._graph.convertToSubgraph(this.selectedItems) + } }, - }, { - content: "Align", - has_submenu: true, - callback: LGraphCanvas.onGroupAlign, - }) + { + content: 'Align', + has_submenu: true, + callback: LGraphCanvas.onGroupAlign + } + ) } } const extra = this.getExtraMenuOptions?.(this, options) - return Array.isArray(extra) - ? options.concat(extra) - : options + return Array.isArray(extra) ? options.concat(extra) : options } // called by processContextMenu to extract the menu list getNodeMenuOptions(node: LGraphNode) { - let options: (IContextMenuValue | IContextMenuValue | IContextMenuValue | IContextMenuValue | IContextMenuValue | null)[] + let options: ( + | IContextMenuValue + | IContextMenuValue + | IContextMenuValue + | IContextMenuValue + | IContextMenuValue<(typeof LiteGraph.VALID_SHAPES)[number]> + | null + )[] if (node.getMenuOptions) { options = node.getMenuOptions(this) } else { options = [ { - content: "Inputs", + content: 'Inputs', has_submenu: true, - disabled: true, + disabled: true }, { - content: "Outputs", + content: 'Outputs', has_submenu: true, disabled: true, - callback: LGraphCanvas.showMenuNodeOptionalOutputs, + callback: LGraphCanvas.showMenuNodeOptionalOutputs }, null, { - content: "Convert to Subgraph 🆕", + content: 'Convert to Subgraph 🆕', callback: () => { - if (!this.selectedItems.size) throw new Error("Convert to Subgraph: Nothing selected.") + if (!this.selectedItems.size) + throw new Error('Convert to Subgraph: Nothing selected.') this._graph.convertToSubgraph(this.selectedItems) - }, + } }, { - content: "Properties", + content: 'Properties', has_submenu: true, - callback: LGraphCanvas.onShowMenuNodeProperties, + callback: LGraphCanvas.onShowMenuNodeProperties }, { - content: "Properties Panel", - callback: function (item: any, options: any, e: any, menu: any, node: LGraphNode) { LGraphCanvas.active_canvas.showShowNodePanel(node) }, + content: 'Properties Panel', + callback: function ( + item: any, + options: any, + e: any, + menu: any, + node: LGraphNode + ) { + LGraphCanvas.active_canvas.showShowNodePanel(node) + } }, null, { - content: "Title", - callback: LGraphCanvas.onShowPropertyEditor, + content: 'Title', + callback: LGraphCanvas.onShowPropertyEditor }, { - content: "Mode", + content: 'Mode', has_submenu: true, - callback: LGraphCanvas.onMenuNodeMode, - }, + callback: LGraphCanvas.onMenuNodeMode + } ] if (node.resizable !== false) { options.push({ - content: "Resize", - callback: LGraphCanvas.onMenuResizeNode, + content: 'Resize', + callback: LGraphCanvas.onMenuResizeNode }) } if (node.collapsible) { options.push({ - content: node.collapsed ? "Expand" : "Collapse", - callback: LGraphCanvas.onMenuNodeCollapse, + content: node.collapsed ? 'Expand' : 'Collapse', + callback: LGraphCanvas.onMenuNodeCollapse }) } - if (node.widgets?.some(w => w.advanced)) { + if (node.widgets?.some((w) => w.advanced)) { options.push({ - content: node.showAdvanced ? "Hide Advanced" : "Show Advanced", - callback: LGraphCanvas.onMenuToggleAdvanced, + content: node.showAdvanced ? 'Hide Advanced' : 'Show Advanced', + callback: LGraphCanvas.onMenuToggleAdvanced }) } options.push( { - content: node.pinned ? "Unpin" : "Pin", + content: node.pinned ? 'Unpin' : 'Pin', callback: () => { for (const i in this.selected_nodes) { const node = this.selected_nodes[i] node.pin() } this.setDirty(true, true) - }, + } }, { - content: "Colors", + content: 'Colors', has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors, + callback: LGraphCanvas.onMenuNodeColors }, { - content: "Shapes", + content: 'Shapes', has_submenu: true, - callback: LGraphCanvas.onMenuNodeShapes, + callback: LGraphCanvas.onMenuNodeShapes }, - null, + null ) } @@ -7572,27 +8098,30 @@ export class LGraphCanvas implements CustomEventDispatcher if (node.clonable !== false) { options.push({ - content: "Clone", - callback: LGraphCanvas.onMenuNodeClone, + content: 'Clone', + callback: LGraphCanvas.onMenuNodeClone }) } if (Object.keys(this.selected_nodes).length > 1) { - options.push({ - content: "Align Selected To", - has_submenu: true, - callback: LGraphCanvas.onNodeAlign, - }, { - content: "Distribute Nodes", - has_submenu: true, - callback: LGraphCanvas.createDistributeMenu, - }) + options.push( + { + content: 'Align Selected To', + has_submenu: true, + callback: LGraphCanvas.onNodeAlign + }, + { + content: 'Distribute Nodes', + has_submenu: true, + callback: LGraphCanvas.createDistributeMenu + } + ) } options.push(null, { - content: "Remove", + content: 'Remove', disabled: !(node.removable !== false && !node.block_delete), - callback: LGraphCanvas.onMenuNodeRemove, + callback: LGraphCanvas.onMenuNodeRemove }) node.graph?.onGetNodeMenuOptions?.(options, node) @@ -7602,11 +8131,16 @@ export class LGraphCanvas implements CustomEventDispatcher /** @deprecated */ getGroupMenuOptions(group: LGraphGroup) { - console.warn("LGraphCanvas.getGroupMenuOptions is deprecated, use LGraphGroup.getMenuOptions instead") + console.warn( + 'LGraphCanvas.getGroupMenuOptions is deprecated, use LGraphGroup.getMenuOptions instead' + ) return group.getMenuOptions() } - processContextMenu(node: LGraphNode | undefined, event: CanvasPointerEvent): void { + processContextMenu( + node: LGraphNode | undefined, + event: CanvasPointerEvent + ): void { const canvas = LGraphCanvas.active_canvas const ref_window = canvas.getCanvasWindow() @@ -7615,7 +8149,7 @@ export class LGraphCanvas implements CustomEventDispatcher const options: IContextMenuOptions = { event, callback: inner_option_clicked, - extra: node, + extra: node } if (node) { @@ -7631,21 +8165,22 @@ export class LGraphCanvas implements CustomEventDispatcher menu_info = node.getSlotMenuOptions(slot) } else { if (slot.output?.links?.length || slot.input?.link != null) { - menu_info.push({ content: "Disconnect Links", slot }) + menu_info.push({ content: 'Disconnect Links', slot }) } const _slot = slot.input || slot.output - if (!_slot) throw new TypeError("Both in put and output slots were null when processing context menu.") + if (!_slot) + throw new TypeError( + 'Both in put and output slots were null when processing context menu.' + ) if (_slot.removable) { menu_info.push( - _slot.locked - ? "Cannot remove" - : { content: "Remove Slot", slot }, + _slot.locked ? 'Cannot remove' : { content: 'Remove Slot', slot } ) } - if (!_slot.nameLocked && !(("link" in _slot) && _slot.widget)) { - menu_info.push({ content: "Rename Slot", slot }) + if (!_slot.nameLocked && !('link' in _slot && _slot.widget)) { + menu_info.push({ content: 'Rename Slot', slot }) } if (node.getExtraSlotMenuOptions) { @@ -7653,12 +8188,12 @@ export class LGraphCanvas implements CustomEventDispatcher } } // @ts-expect-error Slot type can be number and has number checks - options.title = (slot.input ? slot.input.type : slot.output.type) || "*" + options.title = (slot.input ? slot.input.type : slot.output.type) || '*' if (slot.input && slot.input.type == LiteGraph.ACTION) - options.title = "Action" + options.title = 'Action' if (slot.output && slot.output.type == LiteGraph.EVENT) - options.title = "Event" + options.title = 'Event' } else { // on node menu_info = this.getNodeMenuOptions(node) @@ -7669,33 +8204,37 @@ export class LGraphCanvas implements CustomEventDispatcher // Check for reroutes if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) { - const reroute = this.graph.getRerouteOnPos(event.canvasX, event.canvasY, this.#visibleReroutes) + const reroute = this.graph.getRerouteOnPos( + event.canvasX, + event.canvasY, + this.#visibleReroutes + ) if (reroute) { - menu_info.unshift({ - content: "Delete Reroute", - callback: () => { - if (!this.graph) throw new NullGraphError() + menu_info.unshift( + { + content: 'Delete Reroute', + callback: () => { + if (!this.graph) throw new NullGraphError() - this.graph.removeReroute(reroute.id) + this.graph.removeReroute(reroute.id) + } }, - }, null) + null + ) } } - const group = this.graph.getGroupOnPos( - event.canvasX, - event.canvasY, - ) + const group = this.graph.getGroupOnPos(event.canvasX, event.canvasY) if (group) { // on group menu_info.push(null, { - content: "Edit Group", + content: 'Edit Group', has_submenu: true, submenu: { - title: "Group", + title: 'Group', extra: group, - options: group.getMenuOptions(), - }, + options: group.getMenuOptions() + } }) } } @@ -7706,20 +8245,27 @@ export class LGraphCanvas implements CustomEventDispatcher // @ts-expect-error Remove param ref_window - unused new LiteGraph.ContextMenu(menu_info, options, ref_window) - const createDialog = (options: IDialogOptions) => this.createDialog( - "Name", - options, - ) + const createDialog = (options: IDialogOptions) => + this.createDialog( + "Name", + options + ) const setDirty = () => this.setDirty(true) - function inner_option_clicked(v: IContextMenuValue, options: IDialogOptions) { + function inner_option_clicked( + v: IContextMenuValue, + options: IDialogOptions + ) { if (!v) return - if (v.content == "Remove Slot") { + if (v.content == 'Remove Slot') { if (!node?.graph) throw new NullGraphError() const info = v.slot - if (!info) throw new TypeError("Found-slot info was null when processing context menu.") + if (!info) + throw new TypeError( + 'Found-slot info was null when processing context menu.' + ) node.graph.beforeChange() if (info.input) { @@ -7729,11 +8275,14 @@ export class LGraphCanvas implements CustomEventDispatcher } node.graph.afterChange() return - } else if (v.content == "Disconnect Links") { + } else if (v.content == 'Disconnect Links') { if (!node?.graph) throw new NullGraphError() const info = v.slot - if (!info) throw new TypeError("Found-slot info was null when processing context menu.") + if (!info) + throw new TypeError( + 'Found-slot info was null when processing context menu.' + ) node.graph.beforeChange() if (info.output) { @@ -7743,20 +8292,26 @@ export class LGraphCanvas implements CustomEventDispatcher } node.graph.afterChange() return - } else if (v.content == "Rename Slot") { - if (!node) throw new TypeError("`node` was null when processing the context menu.") + } else if (v.content == 'Rename Slot') { + if (!node) + throw new TypeError( + '`node` was null when processing the context menu.' + ) const info = v.slot - if (!info) throw new TypeError("Found-slot info was null when processing context menu.") + if (!info) + throw new TypeError( + 'Found-slot info was null when processing context menu.' + ) const slot_info = info.input ? node.getInputInfo(info.slot) : node.getOutputInfo(info.slot) const dialog = createDialog(options) - const input = dialog.querySelector("input") + const input = dialog.querySelector('input') if (input && slot_info) { - input.value = slot_info.label || "" + input.value = slot_info.label || '' } const inner = function () { if (!node.graph) throw new NullGraphError() @@ -7771,18 +8326,21 @@ export class LGraphCanvas implements CustomEventDispatcher dialog.close() node.graph.afterChange() } - dialog.querySelector("button")?.addEventListener("click", inner) - if (!input) throw new TypeError("Input element was null when processing context menu.") + dialog.querySelector('button')?.addEventListener('click', inner) + if (!input) + throw new TypeError( + 'Input element was null when processing context menu.' + ) - input.addEventListener("keydown", function (e) { + input.addEventListener('keydown', function (e) { dialog.is_modified = true - if (e.key == "Escape") { + if (e.key == 'Escape') { // ESC dialog.close() - } else if (e.key == "Enter") { + } else if (e.key == 'Enter') { // save inner() - } else if ((e.target as Element).localName != "textarea") { + } else if ((e.target as Element).localName != 'textarea') { return } e.preventDefault() @@ -7811,7 +8369,10 @@ export class LGraphCanvas implements CustomEventDispatcher ? Array.from(this.selectedItems) : this.positionableItems const bounds = createBounds(items) - if (!bounds) throw new TypeError("Attempted to fit to view but could not calculate bounds.") + if (!bounds) + throw new TypeError( + 'Attempted to fit to view but could not calculate bounds.' + ) const setDirty = () => this.setDirty(true, true) this.ds.animateToBounds(bounds, setDirty, options) diff --git a/src/lib/litegraph/src/LGraphGroup.ts b/src/lib/litegraph/src/LGraphGroup.ts index d79869c9ae..f00f302e6a 100644 --- a/src/lib/litegraph/src/LGraphGroup.ts +++ b/src/lib/litegraph/src/LGraphGroup.ts @@ -1,3 +1,9 @@ +import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError' + +import type { LGraph } from './LGraph' +import { LGraphCanvas } from './LGraphCanvas' +import { LGraphNode } from './LGraphNode' +import { strokeShape } from './draw' import type { ColorOption, IColorable, @@ -5,25 +11,18 @@ import type { IPinnable, Point, Positionable, - Size, -} from "./interfaces" -import type { LGraph } from "./LGraph" -import type { ISerialisedGroup } from "./types/serialisation" - -import { NullGraphError } from "@/lib/litegraph/src/infrastructure/NullGraphError" - -import { strokeShape } from "./draw" -import { LGraphCanvas } from "./LGraphCanvas" -import { LGraphNode } from "./LGraphNode" -import { LiteGraph } from "./litegraph" + Size +} from './interfaces' +import { LiteGraph } from './litegraph' import { containsCentre, containsRect, createBounds, isInRectangle, isPointInRect, - snapPoint, -} from "./measure" + snapPoint +} from './measure' +import type { ISerialisedGroup } from './types/serialisation' export interface IGraphGroupFlags extends Record { pinned?: true @@ -34,7 +33,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { static minHeight = 80 static resizeLength = 10 static padding = 4 - static defaultColour = "#335" + static defaultColour = '#335' id: number color?: string @@ -45,7 +44,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { 10, 10, LGraphGroup.minWidth, - LGraphGroup.minHeight, + LGraphGroup.minHeight ]) _pos: Point = this._bounding.subarray(0, 2) @@ -60,10 +59,10 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { constructor(title?: string, id?: number) { // TODO: Object instantiation pattern requires too much boilerplate and null checking. ID should be passed in via constructor. this.id = id ?? -1 - this.title = title || "Group" + this.title = title || 'Group' const { pale_blue } = LGraphCanvas.node_colors - this.color = pale_blue ? pale_blue.groupcolor : "#AAA" + this.color = pale_blue ? pale_blue.groupcolor : '#AAA' } /** @inheritdoc {@link IColorable.setColorOption} */ @@ -77,9 +76,11 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { /** @inheritdoc {@link IColorable.getColorOption} */ getColorOption(): ColorOption | null { - return Object.values(LGraphCanvas.node_colors).find( - colorOption => colorOption.groupcolor === this.color, - ) ?? null + return ( + Object.values(LGraphCanvas.node_colors).find( + (colorOption) => colorOption.groupcolor === this.color + ) ?? null + ) } /** Position of the group, as x,y co-ordinates in graph space */ @@ -158,7 +159,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { bounding: [...b], color: this.color, font_size: this.font_size, - flags: this.flags, + flags: this.flags } } @@ -201,13 +202,17 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { // Title ctx.font = `${font_size}px ${LiteGraph.GROUP_FONT}` - ctx.textAlign = "left" - ctx.fillText(this.title + (this.pinned ? "📌" : ""), x + padding, y + font_size) + ctx.textAlign = 'left' + ctx.fillText( + this.title + (this.pinned ? '📌' : ''), + x + padding, + y + font_size + ) if (LiteGraph.highlight_selected_group && this.selected) { strokeShape(ctx, this._bounding, { title_height: this.titleHeight, - padding, + padding }) } } @@ -254,14 +259,12 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { // Move reroutes we overlap the centre point of for (const reroute of reroutes.values()) { - if (isPointInRect(reroute.pos, this._bounding)) - children.add(reroute) + if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute) } // Move groups we wholly contain for (const group of groups) { - if (containsRect(this._bounding, group._bounding)) - children.add(group) + if (containsRect(this._bounding, group._bounding)) children.add(group) } groups.sort((a, b) => { @@ -300,31 +303,35 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable { this.resizeTo([...this.children, ...this._nodes, ...nodes], padding) } - getMenuOptions(): (IContextMenuValue | IContextMenuValue | null)[] { + getMenuOptions(): ( + | IContextMenuValue + | IContextMenuValue + | null + )[] { return [ { - content: this.pinned ? "Unpin" : "Pin", + content: this.pinned ? 'Unpin' : 'Pin', callback: () => { if (this.pinned) this.unpin() else this.pin() this.setDirtyCanvas(false, true) - }, + } }, null, - { content: "Title", callback: LGraphCanvas.onShowPropertyEditor }, + { content: 'Title', callback: LGraphCanvas.onShowPropertyEditor }, { - content: "Color", + content: 'Color', has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors, + callback: LGraphCanvas.onMenuNodeColors }, { - content: "Font size", - property: "font_size", - type: "Number", - callback: LGraphCanvas.onShowPropertyEditor, + content: 'Font size', + property: 'font_size', + type: 'Number', + callback: LGraphCanvas.onShowPropertyEditor }, null, - { content: "Remove", callback: LGraphCanvas.onMenuNodeRemove }, + { content: 'Remove', callback: LGraphCanvas.onMenuNodeRemove } ] } diff --git a/src/lib/litegraph/src/LGraphIcon.ts b/src/lib/litegraph/src/LGraphIcon.ts index d185f18b6e..b5aa628a60 100644 --- a/src/lib/litegraph/src/LGraphIcon.ts +++ b/src/lib/litegraph/src/LGraphIcon.ts @@ -21,13 +21,13 @@ export class LGraphIcon { constructor({ unicode, - fontFamily = "PrimeIcons", - color = "#e6c200", + fontFamily = 'PrimeIcons', + color = '#e6c200', bgColor, fontSize = 16, circlePadding = 2, xOffset = 0, - yOffset = 0, + yOffset = 0 }: LGraphIconOptions) { this.unicode = unicode this.fontFamily = fontFamily @@ -46,8 +46,8 @@ export class LGraphIcon { const { font, textBaseline, textAlign, fillStyle } = ctx ctx.font = `${this.fontSize}px '${this.fontFamily}'` - ctx.textBaseline = "middle" - ctx.textAlign = "center" + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' const iconRadius = this.fontSize / 2 + this.circlePadding // Draw icon background circle if bgColor is set if (this.bgColor) { diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 5ddf8ab201..d6d1bbf31d 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -1,6 +1,15 @@ -import type { DragAndScale } from "./DragAndScale" -import type { IDrawBoundingOptions } from "./draw" -import type { ReadOnlyRectangle } from "./infrastructure/Rectangle" +import type { DragAndScale } from './DragAndScale' +import type { LGraph } from './LGraph' +import { BadgePosition, LGraphBadge } from './LGraphBadge' +import { LGraphButton, type LGraphButtonOptions } from './LGraphButton' +import { LGraphCanvas } from './LGraphCanvas' +import { LLink } from './LLink' +import type { Reroute, RerouteId } from './Reroute' +import { getNodeInputOnPos, getNodeOutputOnPos } from './canvas/measureSlots' +import type { IDrawBoundingOptions } from './draw' +import { NullGraphError } from './infrastructure/NullGraphError' +import type { ReadOnlyRectangle } from './infrastructure/Rectangle' +import { Rectangle } from './infrastructure/Rectangle' import type { ColorOption, CompassCorners, @@ -21,42 +30,53 @@ import type { ReadOnlyPoint, ReadOnlyRect, Rect, - Size, -} from "./interfaces" -import type { LGraph } from "./LGraph" -import type { Reroute, RerouteId } from "./Reroute" -import type { SubgraphInputNode } from "./subgraph/SubgraphInputNode" -import type { SubgraphOutputNode } from "./subgraph/SubgraphOutputNode" -import type { CanvasPointerEvent } from "./types/events" -import type { NodeLike } from "./types/NodeLike" -import type { ISerialisedNode, SubgraphIO } from "./types/serialisation" -import type { IBaseWidget, IWidgetOptions, TWidgetType, TWidgetValue } from "./types/widgets" - -import { getNodeInputOnPos, getNodeOutputOnPos } from "./canvas/measureSlots" -import { NullGraphError } from "./infrastructure/NullGraphError" -import { Rectangle } from "./infrastructure/Rectangle" -import { BadgePosition, LGraphBadge } from "./LGraphBadge" -import { LGraphButton, type LGraphButtonOptions } from "./LGraphButton" -import { LGraphCanvas } from "./LGraphCanvas" -import { type LGraphNodeConstructor, LiteGraph, type Subgraph, type SubgraphNode } from "./litegraph" -import { LLink } from "./LLink" -import { createBounds, isInRect, isInRectangle, isPointInRect, snapPoint } from "./measure" -import { NodeInputSlot } from "./node/NodeInputSlot" -import { NodeOutputSlot } from "./node/NodeOutputSlot" -import { inputAsSerialisable, isINodeInputSlot, isWidgetInputSlot, outputAsSerialisable } from "./node/slotUtils" + Size +} from './interfaces' +import { + type LGraphNodeConstructor, + LiteGraph, + type Subgraph, + type SubgraphNode +} from './litegraph' +import { + createBounds, + isInRect, + isInRectangle, + isPointInRect, + snapPoint +} from './measure' +import { NodeInputSlot } from './node/NodeInputSlot' +import { NodeOutputSlot } from './node/NodeOutputSlot' +import { + inputAsSerialisable, + isINodeInputSlot, + isWidgetInputSlot, + outputAsSerialisable +} from './node/slotUtils' +import type { SubgraphInputNode } from './subgraph/SubgraphInputNode' +import type { SubgraphOutputNode } from './subgraph/SubgraphOutputNode' +import type { NodeLike } from './types/NodeLike' +import type { CanvasPointerEvent } from './types/events' import { LGraphEventMode, NodeSlotType, RenderShape, - TitleMode, -} from "./types/globalEnums" -import { findFreeSlotOfType } from "./utils/collections" -import { warnDeprecated } from "./utils/feedback" -import { distributeSpace } from "./utils/spaceDistribution" -import { truncateText } from "./utils/textUtils" -import { toClass } from "./utils/type" -import { BaseWidget } from "./widgets/BaseWidget" -import { toConcreteWidget, type WidgetTypeMap } from "./widgets/widgetMap" + TitleMode +} from './types/globalEnums' +import type { ISerialisedNode, SubgraphIO } from './types/serialisation' +import type { + IBaseWidget, + IWidgetOptions, + TWidgetType, + TWidgetValue +} from './types/widgets' +import { findFreeSlotOfType } from './utils/collections' +import { warnDeprecated } from './utils/feedback' +import { distributeSpace } from './utils/spaceDistribution' +import { truncateText } from './utils/textUtils' +import { toClass } from './utils/type' +import { BaseWidget } from './widgets/BaseWidget' +import { type WidgetTypeMap, toConcreteWidget } from './widgets/widgetMap' // #region Types @@ -89,8 +109,8 @@ export interface ConnectByTypeOptions { /** Internal type used for type safety when implementing generic checks for inputs & outputs */ export interface IGenericLinkOrLinks { - links?: INodeOutputSlot["links"] - link?: INodeInputSlot["link"] + links?: INodeOutputSlot['links'] + link?: INodeInputSlot['link'] } export interface FindFreeSlotOptions { @@ -190,7 +210,9 @@ export interface LGraphNode { * @param type a type for the node */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable { +export class LGraphNode + implements NodeLike, Positionable, IPinnable, IColorable +{ // Static properties used by dynamic child classes static title?: string static MAX_CONSOLE?: number @@ -224,7 +246,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable graph: LGraph | Subgraph | null = null id: NodeId - type: string = "" + type: string = '' inputs: INodeInputSlot[] = [] outputs: INodeOutputSlot[] = [] @@ -271,7 +293,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable /** The bg color used to render the node. */ get renderingBgColor(): string { - return this.bgcolor || this.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR + return ( + this.bgcolor || this.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR + ) } /** The box color used to render the node. */ @@ -279,12 +303,13 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (this.boxcolor) return this.boxcolor if (LiteGraph.node_box_coloured_when_on) { - if (this.action_triggered) return "#FFF" - if (this.execute_triggered) return "#AAA" + if (this.action_triggered) return '#FFF' + if (this.execute_triggered) return '#AAA' } if (LiteGraph.node_box_coloured_by_mode) { - const modeColour = LiteGraph.NODE_MODES_COLORS[this.mode ?? LGraphEventMode.ALWAYS] + const modeColour = + LiteGraph.NODE_MODES_COLORS[this.mode ?? LGraphEventMode.ALWAYS] if (modeColour) return modeColour } return LiteGraph.NODE_DEFAULT_BOXCOLOR @@ -303,16 +328,22 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable /** @inheritdoc {@link IColorable.getColorOption} */ getColorOption(): ColorOption | null { - return Object.values(LGraphCanvas.node_colors).find( - colorOption => - colorOption.color === this.color && colorOption.bgcolor === this.bgcolor, - ) ?? null + return ( + Object.values(LGraphCanvas.node_colors).find( + (colorOption) => + colorOption.color === this.color && + colorOption.bgcolor === this.bgcolor + ) ?? null + ) } /** * The stroke styles that should be applied to the node. */ - strokeStyles: Record IDrawBoundingOptions | undefined> + strokeStyles: Record< + string, + (this: LGraphNode) => IDrawBoundingOptions | undefined + > /** * The progress of node execution. Used to render a progress bar. Value between 0 and 1. @@ -389,7 +420,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable /** The offset from {@link pos} to the top-left of {@link boundingRect}. */ get boundingOffset(): ReadOnlyPoint { - const { pos: [posX, posY], boundingRect: [bX, bY] } = this + const { + pos: [posX, posY], + boundingRect: [bX, bY] + } = this return [posX - bX, posY - bY] } @@ -432,25 +466,25 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable return this._shape } - set shape(v: RenderShape | "default" | "box" | "round" | "circle" | "card") { + set shape(v: RenderShape | 'default' | 'box' | 'round' | 'circle' | 'card') { switch (v) { - case "default": - delete this._shape - break - case "box": - this._shape = RenderShape.BOX - break - case "round": - this._shape = RenderShape.ROUND - break - case "circle": - this._shape = RenderShape.CIRCLE - break - case "card": - this._shape = RenderShape.CARD - break - default: - this._shape = v + case 'default': + delete this._shape + break + case 'box': + this._shape = RenderShape.BOX + break + case 'round': + this._shape = RenderShape.ROUND + break + case 'circle': + this._shape = RenderShape.CIRCLE + break + case 'card': + this._shape = RenderShape.CARD + break + default: + this._shape = v } } @@ -479,7 +513,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable type: unknown, output: INodeOutputSlot | SubgraphIO, node: LGraphNode | SubgraphInputNode, - slot: number, + slot: number ): boolean onConnectOutput?( this: LGraphNode, @@ -487,14 +521,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable type: unknown, input: INodeInputSlot | SubgraphIO, target_node: number | LGraphNode | SubgraphOutputNode, - target_slot: number, + target_slot: number ): boolean onResize?(this: LGraphNode, size: Size): void onPropertyChanged?( this: LGraphNode, name: string, value: unknown, - prev_value?: unknown, + prev_value?: unknown ): boolean /** Called for each connection that is created, updated, or removed. This includes "restored" connections when deserialising. */ onConnectionsChange?( @@ -503,7 +537,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable index: number, isConnected: boolean, link_info: LLink | null | undefined, - inputOrOutput: INodeInputSlot | INodeOutputSlot | SubgraphIO, + inputOrOutput: INodeInputSlot | INodeOutputSlot | SubgraphIO ): void onInputAdded?(this: LGraphNode, input: INodeInputSlot): void onOutputAdded?(this: LGraphNode, output: INodeOutputSlot): void @@ -512,18 +546,15 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable onExecute?( this: LGraphNode, param?: unknown, - options?: { action_call?: any }, + options?: { action_call?: any } ): void onAction?( this: LGraphNode, action: string, param: unknown, - options: { action_call?: string }, - ): void - onDrawBackground?( - this: LGraphNode, - ctx: CanvasRenderingContext2D, + options: { action_call?: string } ): void + onDrawBackground?(this: LGraphNode, ctx: CanvasRenderingContext2D): void onNodeCreated?(this: LGraphNode): void /** * Callback invoked by {@link connect} to override the target slot index. @@ -536,7 +567,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable onBeforeConnectInput?( this: LGraphNode, target_slot: number, - requested_slot?: number | string, + requested_slot?: number | string ): number | false | null onShowCustomPanelInfo?(this: LGraphNode, panel: any): void onAddPropertyToPanel?(this: LGraphNode, pName: string, panel: any): boolean @@ -545,7 +576,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable name: string, value: unknown, old_value: unknown, - w: IBaseWidget, + w: IBaseWidget ): void onDeselected?(this: LGraphNode): void onKeyUp?(this: LGraphNode, e: KeyboardEvent): void @@ -554,20 +585,20 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable getExtraMenuOptions?( this: LGraphNode, canvas: LGraphCanvas, - options: (IContextMenuValue | null)[], + options: (IContextMenuValue | null)[] ): (IContextMenuValue | null)[] getMenuOptions?(this: LGraphNode, canvas: LGraphCanvas): IContextMenuValue[] onAdded?(this: LGraphNode, graph: LGraph): void onDrawCollapsed?( this: LGraphNode, ctx: CanvasRenderingContext2D, - cavnas: LGraphCanvas, + cavnas: LGraphCanvas ): boolean onDrawForeground?( this: LGraphNode, ctx: CanvasRenderingContext2D, canvas: LGraphCanvas, - canvasElement: HTMLCanvasElement, + canvasElement: HTMLCanvasElement ): void onMouseLeave?(this: LGraphNode, e: CanvasPointerEvent): void /** @@ -577,7 +608,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable /** * Add extra menu options to the slot context menu. */ - getExtraSlotMenuOptions?(this: LGraphNode, slot: IFoundSlot): IContextMenuValue[] + getExtraSlotMenuOptions?( + this: LGraphNode, + slot: IFoundSlot + ): IContextMenuValue[] // FIXME: Re-typing onDropItem?(this: LGraphNode, event: Event): boolean @@ -585,24 +619,28 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable this: LGraphNode, data: string | ArrayBuffer, filename: any, - file: any, + file: any ): void onDropFile?(this: LGraphNode, file: any): void onInputClick?(this: LGraphNode, index: number, e: CanvasPointerEvent): void onInputDblClick?(this: LGraphNode, index: number, e: CanvasPointerEvent): void onOutputClick?(this: LGraphNode, index: number, e: CanvasPointerEvent): void - onOutputDblClick?(this: LGraphNode, index: number, e: CanvasPointerEvent): void + onOutputDblClick?( + this: LGraphNode, + index: number, + e: CanvasPointerEvent + ): void // TODO: Return type onGetPropertyInfo?(this: LGraphNode, property: string): any onNodeOutputAdd?(this: LGraphNode, value: unknown): void onNodeInputAdd?(this: LGraphNode, value: unknown): void onMenuNodeInputs?( this: LGraphNode, - entries: (IContextMenuValue | null)[], + entries: (IContextMenuValue | null)[] ): (IContextMenuValue | null)[] onMenuNodeOutputs?( this: LGraphNode, - entries: (IContextMenuValue | null)[], + entries: (IContextMenuValue | null)[] ): (IContextMenuValue | null)[] onMouseUp?(this: LGraphNode, e: CanvasPointerEvent, pos: Point): void onMouseEnter?(this: LGraphNode, e: CanvasPointerEvent): void @@ -611,21 +649,21 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable this: LGraphNode, e: CanvasPointerEvent, pos: Point, - canvas: LGraphCanvas, + canvas: LGraphCanvas ): boolean /** @param pos Offset from {@link LGraphNode.pos}. */ onDblClick?( this: LGraphNode, e: CanvasPointerEvent, pos: Point, - canvas: LGraphCanvas, + canvas: LGraphCanvas ): void /** @param pos Offset from {@link LGraphNode.pos}. */ onNodeTitleDblClick?( this: LGraphNode, e: CanvasPointerEvent, pos: Point, - canvas: LGraphCanvas, + canvas: LGraphCanvas ): void onDrawTitle?(this: LGraphNode, ctx: CanvasRenderingContext2D): void onDrawTitleText?( @@ -635,14 +673,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable size: Size, scale: number, title_text_font: string, - selected?: boolean, + selected?: boolean ): void onDrawTitleBox?( this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, - scale: number, + scale: number ): void onDrawTitleBar?( this: LGraphNode, @@ -650,14 +688,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable title_height: number, size: Size, scale: number, - fgcolor: any, + fgcolor: any ): void onRemoved?(this: LGraphNode): void onMouseMove?( this: LGraphNode, e: CanvasPointerEvent, pos: Point, - arg2: LGraphCanvas, + arg2: LGraphCanvas ): void onPropertyChange?(this: LGraphNode): void updateOutputData?(this: LGraphNode, origin_slot: number): void @@ -667,7 +705,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable return { padding: 12, lineWidth: 10, - color: LiteGraph.NODE_ERROR_COLOUR, + color: LiteGraph.NODE_ERROR_COLOUR } } } @@ -675,25 +713,29 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable #getSelectedStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined { if (this.selected) { return { - padding: this.has_errors ? 20 : undefined, + padding: this.has_errors ? 20 : undefined } } } constructor(title: string, type?: string) { this.id = LiteGraph.use_uuids ? LiteGraph.uuidv4() : -1 - this.title = title || "Unnamed" - this.type = type ?? "" + this.title = title || 'Unnamed' + this.type = type ?? '' this.size = [LiteGraph.NODE_WIDTH, 60] this.pos = [10, 10] this.strokeStyles = { error: this.#getErrorStrokeStyle, - selected: this.#getSelectedStrokeStyle, + selected: this.#getSelectedStrokeStyle } // Assign onMouseDown implementation - // @ts-ignore TODO: Fix after migration to frontend tsconfig rules - this.onMouseDown = (e: CanvasPointerEvent, pos: Point, canvas: LGraphCanvas): boolean => { + this.onMouseDown = ( + // @ts-ignore TODO: Fix after migration to frontend tsconfig rules + e: CanvasPointerEvent, + pos: Point, + canvas: LGraphCanvas + ): boolean => { // Check for title button clicks (only if not collapsed) if (this.title_buttons?.length && !this.flags.collapsed) { // pos contains the offset from the node's position, so we need to use node-relative coordinates @@ -702,7 +744,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable for (let i = 0; i < this.title_buttons.length; i++) { const button = this.title_buttons[i] - if (button.visible && button.isPointInside(nodeRelativeX, nodeRelativeY)) { + if ( + button.visible && + button.isPointInside(nodeRelativeX, nodeRelativeY) + ) { this.onTitleButtonClick(button, canvas) return true // Prevent default behavior } @@ -724,7 +769,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable this.graph._version++ } for (const j in info) { - if (j == "properties") { + if (j == 'properties') { // i don't want to clone properties, I want to reuse the old container for (const k in info.properties) { this.properties[k] = info.properties[k] @@ -736,8 +781,8 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable // @ts-expect-error #594 if (info[j] == null) { continue - // @ts-expect-error #594 - } else if (typeof info[j] == "object") { + // @ts-expect-error #594 + } else if (typeof info[j] == 'object') { // @ts-expect-error #594 if (this[j]?.configure) { // @ts-expect-error #594 @@ -758,24 +803,27 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } this.inputs ??= [] - this.inputs = this.inputs.map(input => toClass(NodeInputSlot, input, this)) + this.inputs = this.inputs.map((input) => + toClass(NodeInputSlot, input, this) + ) for (const [i, input] of this.inputs.entries()) { - const link = this.graph && input.link != null - ? this.graph._links.get(input.link) - : null + const link = + this.graph && input.link != null + ? this.graph._links.get(input.link) + : null this.onConnectionsChange?.(NodeSlotType.INPUT, i, true, link, input) this.onInputAdded?.(input) } this.outputs ??= [] - this.outputs = this.outputs.map(output => toClass(NodeOutputSlot, output, this)) + this.outputs = this.outputs.map((output) => + toClass(NodeOutputSlot, output, this) + ) for (const [i, output] of this.outputs.entries()) { if (!output.links) continue for (const linkId of output.links) { - const link = this.graph - ? this.graph._links.get(linkId) - : null + const link = this.graph ? this.graph._links.get(linkId) : null this.onConnectionsChange?.(NodeSlotType.OUTPUT, i, true, link, output) } this.onOutputAdded?.(output) @@ -788,12 +836,19 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable for (const w of this.widgets) { if (!w) continue - if (w.options?.property && this.properties[w.options.property] != undefined) - w.value = JSON.parse(JSON.stringify(this.properties[w.options.property])) + if ( + w.options?.property && + this.properties[w.options.property] != undefined + ) + w.value = JSON.parse( + JSON.stringify(this.properties[w.options.property]) + ) } if (info.widgets_values) { - const widgetsWithValue = this.widgets.filter(w => w.serialize !== false) + const widgetsWithValue = this.widgets.filter( + (w) => w.serialize !== false + ) for (let i = 0; i < info.widgets_values.length; ++i) { const widget = widgetsWithValue[i] if (widget) { @@ -822,16 +877,18 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable flags: LiteGraph.cloneObject(this.flags), order: this.order, mode: this.mode, - showAdvanced: this.showAdvanced, + showAdvanced: this.showAdvanced } // special case for when there were errors if (this.constructor === LGraphNode && this.last_serialization) return this.last_serialization - if (this.inputs) o.inputs = this.inputs.map(input => inputAsSerialisable(input)) - // @ts-ignore TODO: Fix after migration to frontend tsconfig rules - widget type mismatch - if (this.outputs) o.outputs = this.outputs.map(output => outputAsSerialisable(output)) + if (this.inputs) + o.inputs = this.inputs.map((input) => inputAsSerialisable(input)) + if (this.outputs) + // @ts-ignore TODO: Fix after migration to frontend tsconfig rules - widget type mismatch + o.outputs = this.outputs.map((output) => outputAsSerialisable(output)) if (this.title && this.title != this.constructor.title) o.title = this.title @@ -854,7 +911,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (this.boxcolor) o.boxcolor = this.boxcolor if (this.shape) o.shape = this.shape - if (this.onSerialize?.(o)) console.warn("node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter") + if (this.onSerialize?.(o)) + console.warn( + 'node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter' + ) return o } @@ -938,7 +998,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @param slot * @param data */ - setOutputData(slot: number, data: number | string | boolean | { toToolTip?(): string }): void { + setOutputData( + slot: number, + data: number | string | boolean | { toToolTip?(): string } + ): void { const { outputs } = this if (!outputs) return @@ -968,7 +1031,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ setOutputDataType(slot: number, type: ISlotType): void { const { outputs } = this - if (!outputs || (slot == -1 || slot >= outputs.length)) return + if (!outputs || slot == -1 || slot >= outputs.length) return const output_info = outputs[slot] if (!output_info) return @@ -1026,7 +1089,8 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ getInputDataType(slot: number): ISlotType | null { if (!this.inputs) return null - if (slot >= this.inputs.length || this.inputs[slot].link == null) return null + if (slot >= this.inputs.length || this.inputs[slot].link == null) + return null if (!this.graph) throw new NullGraphError() const link_id = this.inputs[slot].link @@ -1038,9 +1102,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (!node) return link.type const output_info = node.outputs[link.origin_slot] - return output_info - ? output_info.type - : null + return output_info ? output_info.type : null } /** @@ -1051,9 +1113,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ getInputDataByName(slot_name: string, force_update: boolean): unknown { const slot = this.findInputSlot(slot_name) - return slot == -1 - ? null - : this.getInputData(slot, force_update) + return slot == -1 ? null : this.getInputData(slot, force_update) } /** @@ -1159,7 +1219,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ isOutputConnected(slot: number): boolean { if (!this.outputs) return false - return slot < this.outputs.length && Number(this.outputs[slot].links?.length) > 0 + return ( + slot < this.outputs.length && Number(this.outputs[slot].links?.length) > 0 + ) } /** @@ -1202,29 +1264,29 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } addOnTriggerInput(): number { - const trigS = this.findInputSlot("onTrigger") + const trigS = this.findInputSlot('onTrigger') if (trigS == -1) { - this.addInput("onTrigger", LiteGraph.EVENT, { - nameLocked: true, + this.addInput('onTrigger', LiteGraph.EVENT, { + nameLocked: true }) - return this.findInputSlot("onTrigger") + return this.findInputSlot('onTrigger') } return trigS } addOnExecutedOutput(): number { - const trigS = this.findOutputSlot("onExecuted") + const trigS = this.findOutputSlot('onExecuted') if (trigS == -1) { - this.addOutput("onExecuted", LiteGraph.ACTION, { - nameLocked: true, + this.addOutput('onExecuted', LiteGraph.ACTION, { + nameLocked: true }) - return this.findOutputSlot("onExecuted") + return this.findOutputSlot('onExecuted') } return trigS } onAfterExecuteNode(param: unknown, options?: { action_call?: any }) { - const trigS = this.findOutputSlot("onExecuted") + const trigS = this.findOutputSlot('onExecuted') if (trigS != -1) { this.triggerSlot(trigS, param, null, options) } @@ -1232,27 +1294,27 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable changeMode(modeTo: number): boolean { switch (modeTo) { - case LGraphEventMode.ON_EVENT: - break + case LGraphEventMode.ON_EVENT: + break - case LGraphEventMode.ON_TRIGGER: - this.addOnTriggerInput() - this.addOnExecutedOutput() - break + case LGraphEventMode.ON_TRIGGER: + this.addOnTriggerInput() + this.addOnExecutedOutput() + break - case LGraphEventMode.NEVER: - break + case LGraphEventMode.NEVER: + break - case LGraphEventMode.ALWAYS: - break + case LGraphEventMode.ALWAYS: + break // @ts-expect-error Not impl. - case LiteGraph.ON_REQUEST: - break + case LiteGraph.ON_REQUEST: + break - default: - return false - break + default: + return false + break } this.mode = modeTo return true @@ -1294,16 +1356,16 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable actionDo( action: string, param: unknown, - options: { action_call?: string }, + options: { action_call?: string } ): void { options = options || {} if (this.onAction) { // enable this to give the event an ID - options.action_call ||= `${this.id}_${action || "action"}_${Math.floor(Math.random() * 9999)}` + options.action_call ||= `${this.id}_${action || 'action'}_${Math.floor(Math.random() * 9999)}` if (!this.graph) throw new NullGraphError() // @ts-expect-error deprecated - this.graph.nodes_actioning[this.id] = action || "actioning" + this.graph.nodes_actioning[this.id] = action || 'actioning' this.onAction(action, param, options) // @ts-expect-error deprecated this.graph.nodes_actioning[this.id] = false @@ -1327,7 +1389,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable trigger( action: string, param: unknown, - options: { action_call?: any }, + options: { action_call?: any } ): void { const { outputs } = this if (!outputs || !outputs.length) { @@ -1357,18 +1419,20 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable slot: number, param: unknown, link_id: number | null, - options?: { action_call?: any }, + options?: { action_call?: any } ): void { options = options || {} if (!this.outputs) return if (slot == null) { - console.error("slot must be a number") + console.error('slot must be a number') return } - if (typeof slot !== "number") - console.warn("slot must be a number, use node.trigger('name') if you want to use a string") + if (typeof slot !== 'number') + console.warn( + "slot must be a number, use node.trigger('name') if you want to use a string" + ) const output = this.outputs[slot] if (!output) return @@ -1454,7 +1518,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable const newSize = this.computeSize() this.setSize([ Math.max(this.size[0], newSize[0]), - Math.max(this.size[1], newSize[1]), + Math.max(this.size[1], newSize[1]) ]) } @@ -1467,7 +1531,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable name: string, default_value: NodeProperty | undefined, type?: string, - extra_info?: Partial, + extra_info?: Partial ): INodePropertyInfo { const o: INodePropertyInfo = { name, type, default_value } if (extra_info) Object.assign(o, extra_info) @@ -1487,11 +1551,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable addOutput>( name: string, type: ISlotType, - extra_info?: TProperties, + extra_info?: TProperties ): INodeOutputSlot & TProperties { const output = Object.assign( new NodeOutputSlot({ name, type, links: null }, this), - extra_info, + extra_info ) this.outputs ||= [] @@ -1535,12 +1599,16 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @param type string defining the input type ("vec3","number",...), it its a generic one use 0 * @param extra_info this can be used to have special properties of an input (label, color, position, etc) */ - addInput>(name: string, type: ISlotType, extra_info?: TProperties): INodeInputSlot & TProperties { + addInput>( + name: string, + type: ISlotType, + extra_info?: TProperties + ): INodeInputSlot & TProperties { type ||= 0 const input = Object.assign( new NodeInputSlot({ name, type, link: null }, this), - extra_info, + extra_info ) this.inputs ||= [] @@ -1584,8 +1652,8 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable const { inputs, outputs, widgets } = this let rows = Math.max( - inputs ? inputs.filter(input => !isWidgetInputSlot(input)).length : 1, - outputs ? outputs.length : 1, + inputs ? inputs.filter((input) => !isWidgetInputSlot(input)).length : 1, + outputs ? outputs.length : 1 ) const size = out || new Float32Array([0, 0]) rows = Math.max(rows, 1) @@ -1594,14 +1662,15 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable const padLeft = LiteGraph.NODE_TITLE_HEIGHT const padRight = padLeft * 0.33 - const title_width = padLeft + compute_text_size(this.title, this.titleFontStyle) + padRight + const title_width = + padLeft + compute_text_size(this.title, this.titleFontStyle) + padRight let input_width = 0 let widgetWidth = 0 let output_width = 0 if (inputs) { for (const input of inputs) { - const text = input.label || input.localized_name || input.name || "" + const text = input.label || input.localized_name || input.name || '' const text_width = compute_text_size(text, this.innerFontStyle) if (isWidgetInputSlot(input)) { const widget = this.getWidgetFromSlot(input) @@ -1616,25 +1685,30 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (outputs) { for (const output of outputs) { - const text = output.label || output.localized_name || output.name || "" + const text = output.label || output.localized_name || output.name || '' const text_width = compute_text_size(text, this.innerFontStyle) - if (output_width < text_width) - output_width = text_width + if (output_width < text_width) output_width = text_width } } const minWidth = LiteGraph.NODE_WIDTH * (widgets?.length ? 1.5 : 1) // Text + slot width + centre padding const centrePadding = input_width && output_width ? 5 : 0 - const slotsWidth = input_width + output_width + (2 * LiteGraph.NODE_SLOT_HEIGHT) + centrePadding + const slotsWidth = + input_width + + output_width + + 2 * LiteGraph.NODE_SLOT_HEIGHT + + centrePadding // Total distance from edge of node to the inner edge of the widget 'previous' arrow button - const widgetMargin = BaseWidget.margin + BaseWidget.arrowMargin + BaseWidget.arrowWidth - const widgetPadding = BaseWidget.minValueWidth + (2 * widgetMargin) + const widgetMargin = + BaseWidget.margin + BaseWidget.arrowMargin + BaseWidget.arrowWidth + const widgetPadding = BaseWidget.minValueWidth + 2 * widgetMargin if (widgetWidth) widgetWidth += widgetPadding size[0] = Math.max(slotsWidth, widgetWidth, title_width, minWidth) - size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT + size[1] = + (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT // Get widget height & expand size if necessary let widgets_height = 0 @@ -1661,16 +1735,16 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } // compute height using widgets height - if (this.widgets_up) - size[1] = Math.max(size[1], widgets_height) + if (this.widgets_up) size[1] = Math.max(size[1], widgets_height) else if (this.widgets_start_y != null) size[1] = Math.max(size[1], widgets_height + this.widgets_start_y) - else - size[1] += widgets_height + else size[1] += widgets_height function compute_text_size(text: string, fontStyle: string) { - return LGraphCanvas._measureText?.(text, fontStyle) ?? + return ( + LGraphCanvas._measureText?.(text, fontStyle) ?? font_size * (text?.length ?? 0) * 0.6 + ) } if (this.constructor.min_height && size[1] < this.constructor.min_height) { @@ -1685,14 +1759,15 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable inResizeCorner(canvasX: number, canvasY: number): boolean { const rows = this.outputs ? this.outputs.length : 1 - const outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT + const outputs_offset = + (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT return isInRectangle( canvasX, canvasY, this.pos[0] + this.size[0] - 15, this.pos[1] + Math.max(this.size[1] - 15, outputs_offset), 20, - 20, + 20 ) } @@ -1702,14 +1777,21 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @param canvasY Y position in canvas coordinates * @returns The compass corner the point is in, otherwise `undefined`. */ - findResizeDirection(canvasX: number, canvasY: number): CompassCorners | undefined { + findResizeDirection( + canvasX: number, + canvasY: number + ): CompassCorners | undefined { if (this.resizable === false) return const { boundingRect } = this if (!boundingRect.containsXy(canvasX, canvasY)) return // Check corners first (they take priority over edges) - return boundingRect.findContainingCorner(canvasX, canvasY, LGraphNode.resizeHandleSize) + return boundingRect.findContainingCorner( + canvasX, + canvasY, + LGraphNode.resizeHandleSize + ) } /** @@ -1733,7 +1815,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } // litescene mode using the constructor // @ts-expect-error deprecated https://github.com/Comfy-Org/litegraph.js/issues/639 - if (this.constructor[`@${property}`]) info = this.constructor[`@${property}`] + if (this.constructor[`@${property}`]) + // @ts-expect-error deprecated https://github.com/Comfy-Org/litegraph.js/issues/639 + info = this.constructor[`@${property}`] if (this.constructor.widgets_info?.[property]) info = this.constructor.widgets_info[property] @@ -1745,7 +1829,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable info ||= {} info.type ||= typeof this.properties[property] - if (info.widget == "combo") info.type = "enum" + if (info.widget == 'combo') info.type = 'enum' return info } @@ -1759,27 +1843,29 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @param options the object that contains special properties of this widget * @returns the created widget object */ - addWidget( + addWidget< + Type extends TWidgetType, + TValue extends WidgetTypeMap[Type]['value'] + >( type: Type, name: string, value: TValue, - callback: IBaseWidget["callback"] | string | null, - options?: IWidgetOptions | string, + callback: IBaseWidget['callback'] | string | null, + options?: IWidgetOptions | string ): WidgetTypeMap[Type] | IBaseWidget { this.widgets ||= [] - if (!options && callback && typeof callback === "object") { + if (!options && callback && typeof callback === 'object') { options = callback callback = null } // options can be the property name options ||= {} - if (typeof options === "string") - options = { property: options } + if (typeof options === 'string') options = { property: options } // callback can be the property name - if (callback && typeof callback === "string") { + if (callback && typeof callback === 'string') { options.property = callback callback = null } @@ -1789,9 +1875,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable type: type.toLowerCase(), name: name, value: value, - callback: typeof callback !== "function" ? undefined : callback, + callback: typeof callback !== 'function' ? undefined : callback, options, - y: 0, + y: 0 } if (w.options.y !== undefined) { @@ -1799,9 +1885,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } if (!callback && !w.options.callback && !w.options.property) { - console.warn("LiteGraph addWidget(...) without a callback or property assigned") + console.warn( + 'LiteGraph addWidget(...) without a callback or property assigned' + ) } - if (type == "combo" && !w.options.values) { + if (type == 'combo' && !w.options.values) { throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }" } @@ -1811,8 +1899,8 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } addCustomWidget( - custom_widget: TPlainWidget, - ): TPlainWidget | WidgetTypeMap[TPlainWidget["type"]] { + custom_widget: TPlainWidget + ): TPlainWidget | WidgetTypeMap[TPlainWidget['type']] { this.widgets ||= [] const widget = toConcreteWidget(custom_widget, this, false) ?? custom_widget this.widgets.push(widget) @@ -1828,22 +1916,23 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable onTitleButtonClick(button: LGraphButton, canvas: LGraphCanvas): void { // Dispatch event for button click - canvas.dispatch("litegraph:node-title-button-clicked", { + canvas.dispatch('litegraph:node-title-button-clicked', { node: this, - button: button, + button: button }) } removeWidgetByName(name: string): void { - const widget = this.widgets?.find(x => x.name === name) + const widget = this.widgets?.find((x) => x.name === name) if (widget) this.removeWidget(widget) } removeWidget(widget: IBaseWidget): void { - if (!this.widgets) throw new Error("removeWidget called on node without widgets") + if (!this.widgets) + throw new Error('removeWidget called on node without widgets') const widgetIndex = this.widgets.indexOf(widget) - if (widgetIndex === -1) throw new Error("Widget not found on this node") + if (widgetIndex === -1) throw new Error('Widget not found on this node') // Clean up slot references to prevent memory leaks if (this.inputs) { @@ -1862,7 +1951,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable try { this.removeWidget(widget) } catch (error) { - console.debug("Failed to remove widget", error) + console.debug('Failed to remove widget', error) } } @@ -1898,9 +1987,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable ctx.font = this.innerFontStyle this._collapsed_width = Math.min( this.size[0], - ctx.measureText(this.getTitle() ?? "").width + LiteGraph.NODE_TITLE_HEIGHT * 2, + ctx.measureText(this.getTitle() ?? '').width + + LiteGraph.NODE_TITLE_HEIGHT * 2 ) - out[2] = (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + out[2] = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH out[3] = LiteGraph.NODE_TITLE_HEIGHT } } @@ -1965,7 +2055,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable this.pos[0], this.pos[1] - squareLength, squareLength, - squareLength, + squareLength ) } @@ -2041,7 +2131,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable getWidgetOnPos( canvasX: number, canvasY: number, - includeDisabled = false, + includeDisabled = false ): IBaseWidget | undefined { const { widgets, pos, size } = this if (!widgets?.length) return @@ -2058,7 +2148,8 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable continue } - const h = widget.computedHeight ?? + const h = + widget.computedHeight ?? widget.computeSize?.(nodeWidth)[1] ?? LiteGraph.NODE_WIDGET_HEIGHT @@ -2078,8 +2169,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @param returnObj if the obj itself wanted * @returns the slot (-1 if not found) */ - findInputSlot(name: string, returnObj?: TReturn): number - findInputSlot(name: string, returnObj?: TReturn): INodeInputSlot + findInputSlot( + name: string, + returnObj?: TReturn + ): number + findInputSlot( + name: string, + returnObj?: TReturn + ): INodeInputSlot findInputSlot(name: string, returnObj: boolean = false) { const { inputs } = this if (!inputs) return -1 @@ -2098,8 +2195,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @param returnObj if the obj itself wanted * @returns the slot (-1 if not found) */ - findOutputSlot(name: string, returnObj?: TReturn): number - findOutputSlot(name: string, returnObj?: TReturn): INodeOutputSlot + findOutputSlot( + name: string, + returnObj?: TReturn + ): number + findOutputSlot( + name: string, + returnObj?: TReturn + ): INodeOutputSlot findOutputSlot(name: string, returnObj: boolean = false) { const { outputs } = this if (!outputs) return -1 @@ -2118,10 +2221,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @returns The index of the first matching slot, the slot itself if returnObj is true, or -1 if not found. */ findInputSlotFree( - optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }, + optsIn?: FindFreeSlotOptions & { returnObj?: TReturn } ): number findInputSlotFree( - optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }, + optsIn?: FindFreeSlotOptions & { returnObj?: TReturn } ): INodeInputSlot | -1 findInputSlotFree(optsIn?: FindFreeSlotOptions) { return this.#findFreeSlot(this.inputs, optsIn) @@ -2133,10 +2236,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @returns The index of the first matching slot, the slot itself if returnObj is true, or -1 if not found. */ findOutputSlotFree( - optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }, + optsIn?: FindFreeSlotOptions & { returnObj?: TReturn } ): number findOutputSlotFree( - optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }, + optsIn?: FindFreeSlotOptions & { returnObj?: TReturn } ): INodeOutputSlot | -1 findOutputSlotFree(optsIn?: FindFreeSlotOptions) { return this.#findFreeSlot(this.outputs, optsIn) @@ -2148,11 +2251,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ #findFreeSlot( slots: TSlot[], - options?: FindFreeSlotOptions, + options?: FindFreeSlotOptions ): TSlot | number { const defaults = { returnObj: false, - typesNotAccepted: [], + typesNotAccepted: [] } const opts = Object.assign(defaults, options || {}) const length = slots?.length @@ -2174,26 +2277,26 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): number findInputSlotByType( type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): INodeInputSlot findInputSlotByType( type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ) { return this.#findSlotByType( this.inputs, type, returnObj, preferFreeSlot, - doNotUseOccupied, + doNotUseOccupied ) } @@ -2204,26 +2307,26 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): number findOutputSlotByType( type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): INodeOutputSlot findOutputSlotByType( type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ) { return this.#findSlotByType( this.outputs, type, returnObj, preferFreeSlot, - doNotUseOccupied, + doNotUseOccupied ) } @@ -2240,44 +2343,44 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): number findSlotByType( input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): INodeInputSlot | -1 findSlotByType( input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): INodeOutputSlot | -1 findSlotByType( input: boolean, type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): number | INodeOutputSlot | INodeInputSlot { return input ? this.#findSlotByType( - this.inputs, - type, - returnObj, - preferFreeSlot, - doNotUseOccupied, - ) + this.inputs, + type, + returnObj, + preferFreeSlot, + doNotUseOccupied + ) : this.#findSlotByType( - this.outputs, - type, - returnObj, - preferFreeSlot, - doNotUseOccupied, - ) + this.outputs, + type, + returnObj, + preferFreeSlot, + doNotUseOccupied + ) } /** @@ -2297,31 +2400,32 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, - doNotUseOccupied?: boolean, + doNotUseOccupied?: boolean ): TSlot | number { const length = slots?.length if (!length) return -1 // Empty string and * match anything (type: 0) - if (type == "" || type == "*") type = 0 - const sourceTypes = String(type).toLowerCase().split(",") + if (type == '' || type == '*') type = 0 + const sourceTypes = String(type).toLowerCase().split(',') // Run the search let occupiedSlot: number | TSlot | null = null for (let i = 0; i < length; ++i) { const slot: TSlot & IGenericLinkOrLinks = slots[i] - const destTypes = slot.type == "0" || slot.type == "*" - ? ["0"] - : String(slot.type).toLowerCase().split(",") + const destTypes = + slot.type == '0' || slot.type == '*' + ? ['0'] + : String(slot.type).toLowerCase().split(',') for (const sourceType of sourceTypes) { // TODO: Remove _event_ entirely. - const source = sourceType == "_event_" ? LiteGraph.EVENT : sourceType + const source = sourceType == '_event_' ? LiteGraph.EVENT : sourceType for (const destType of destTypes) { - const dest = destType == "_event_" ? LiteGraph.EVENT : destType + const dest = destType == '_event_' ? LiteGraph.EVENT : destType - if (source == dest || source === "*" || dest === "*") { + if (source == dest || source === '*' || dest === '*') { if (preferFreeSlot && (slot.links?.length || slot.link != null)) { // In case we can't find a free slot. occupiedSlot ??= returnObj ? slot : i @@ -2349,24 +2453,27 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable findInputs: boolean, node: LGraphNode, slotType: ISlotType, - options?: ConnectByTypeOptions, + options?: ConnectByTypeOptions ): number | undefined { // LEGACY: Old options names - if (options && typeof options === "object") { - if ("firstFreeIfInputGeneralInCase" in options) options.wildcardToTyped = !!options.firstFreeIfInputGeneralInCase - if ("firstFreeIfOutputGeneralInCase" in options) options.wildcardToTyped = !!options.firstFreeIfOutputGeneralInCase - if ("generalTypeInCase" in options) options.typedToWildcard = !!options.generalTypeInCase + if (options && typeof options === 'object') { + if ('firstFreeIfInputGeneralInCase' in options) + options.wildcardToTyped = !!options.firstFreeIfInputGeneralInCase + if ('firstFreeIfOutputGeneralInCase' in options) + options.wildcardToTyped = !!options.firstFreeIfOutputGeneralInCase + if ('generalTypeInCase' in options) + options.typedToWildcard = !!options.generalTypeInCase } const optsDef: ConnectByTypeOptions = { createEventInCase: true, wildcardToTyped: true, - typedToWildcard: true, + typedToWildcard: true } const opts = Object.assign(optsDef, options) if (!this.graph) throw new NullGraphError() - if (node && typeof node === "number") { + if (node && typeof node === 'number') { const nodeById = this.graph.getNodeById(node) if (!nodeById) return @@ -2389,7 +2496,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable // connect to the first free input slot if not found a specific type and this output is general if ( opts.wildcardToTyped && - (slotType == 0 || slotType == "*" || slotType == "") + (slotType == 0 || slotType == '*' || slotType == '') ) { const opt = { typesNotAccepted: [LiteGraph.EVENT] } const nonEventSlot = findInputs @@ -2409,8 +2516,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @param type The {@link ISlotType type} of slot to find * @returns The index and slot if found, otherwise `undefined`. */ - findOutputByType(type: ISlotType): { index: number, slot: INodeOutputSlot } | undefined { - return findFreeSlotOfType(this.outputs, type, output => !output.links?.length) + findOutputByType( + type: ISlotType + ): { index: number; slot: INodeOutputSlot } | undefined { + return findFreeSlotOfType( + this.outputs, + type, + (output) => !output.links?.length + ) } /** @@ -2423,8 +2536,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @param type The {@link ISlotType type} of slot to find * @returns The index and slot if found, otherwise `undefined`. */ - findInputByType(type: ISlotType): { index: number, slot: INodeInputSlot } | undefined { - return findFreeSlotOfType(this.inputs, type, input => input.link == null) + findInputByType( + type: ISlotType + ): { index: number; slot: INodeInputSlot } | undefined { + return findFreeSlotOfType(this.inputs, type, (input) => input.link == null) } /** @@ -2438,18 +2553,23 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable slot: number | string, target_node: LGraphNode, target_slotType: ISlotType, - optsIn?: ConnectByTypeOptions, + optsIn?: ConnectByTypeOptions ): LLink | null { const slotIndex = this.findConnectByTypeSlot( true, target_node, target_slotType, - optsIn, + optsIn ) if (slotIndex !== undefined) return this.connect(slot, target_node, slotIndex, optsIn?.afterRerouteId) - console.debug("[connectByType]: no way to connect type:", target_slotType, "to node:", target_node) + console.debug( + '[connectByType]: no way to connect type:', + target_slotType, + 'to node:', + target_node + ) return null } @@ -2464,32 +2584,42 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable slot: number | string, source_node: LGraphNode, source_slotType: ISlotType, - optsIn?: ConnectByTypeOptions, + optsIn?: ConnectByTypeOptions ): LLink | null { // LEGACY: Old options names - if (typeof optsIn === "object") { - if ("firstFreeIfInputGeneralInCase" in optsIn) optsIn.wildcardToTyped = !!optsIn.firstFreeIfInputGeneralInCase - if ("generalTypeInCase" in optsIn) optsIn.typedToWildcard = !!optsIn.generalTypeInCase + if (typeof optsIn === 'object') { + if ('firstFreeIfInputGeneralInCase' in optsIn) + optsIn.wildcardToTyped = !!optsIn.firstFreeIfInputGeneralInCase + if ('generalTypeInCase' in optsIn) + optsIn.typedToWildcard = !!optsIn.generalTypeInCase } const slotIndex = this.findConnectByTypeSlot( false, source_node, source_slotType, - optsIn, + optsIn ) if (slotIndex !== undefined) return source_node.connect(slotIndex, this, slot, optsIn?.afterRerouteId) - console.debug("[connectByType]: no way to connect type:", source_slotType, "to node:", source_node) + console.debug( + '[connectByType]: no way to connect type:', + source_slotType, + 'to node:', + source_node + ) return null } canConnectTo( node: NodeLike, toSlot: INodeInputSlot | SubgraphIO, - fromSlot: INodeOutputSlot | SubgraphIO, + fromSlot: INodeOutputSlot | SubgraphIO ) { - return this.id !== node.id && LiteGraph.isValidConnection(fromSlot.type, toSlot.type) + return ( + this.id !== node.id && + LiteGraph.isValidConnection(fromSlot.type, toSlot.type) + ) } /** @@ -2503,7 +2633,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable slot: number | string, target_node: LGraphNode, target_slot: ISlotType, - afterRerouteId?: RerouteId, + afterRerouteId?: RerouteId ): LLink | null { // Allow legacy API support for searching target_slot by string, without mutating the input variables let targetIndex: number | null @@ -2512,49 +2642,53 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (!graph) { // could be connected before adding it to a graph // due to link ids being associated with graphs - console.log("Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them.") + console.log( + "Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them." + ) return null } // seek for the output slot - if (typeof slot === "string") { + if (typeof slot === 'string') { slot = this.findOutputSlot(slot) if (slot == -1) { - if (LiteGraph.debug) console.log(`Connect: Error, no slot of name ${slot}`) + if (LiteGraph.debug) + console.log(`Connect: Error, no slot of name ${slot}`) return null } } else if (!outputs || slot >= outputs.length) { - if (LiteGraph.debug) console.log("Connect: Error, slot number not found") + if (LiteGraph.debug) console.log('Connect: Error, slot number not found') return null } - if (target_node && typeof target_node === "number") { + if (target_node && typeof target_node === 'number') { const nodeById = graph.getNodeById(target_node) - if (!nodeById) throw "target node is null" + if (!nodeById) throw 'target node is null' target_node = nodeById } - if (!target_node) throw "target node is null" + if (!target_node) throw 'target node is null' // avoid loopback if (target_node == this) return null // you can specify the slot by name - if (typeof target_slot === "string") { + if (typeof target_slot === 'string') { targetIndex = target_node.findInputSlot(target_slot) if (targetIndex == -1) { - if (LiteGraph.debug) console.log(`Connect: Error, no slot of name ${targetIndex}`) + if (LiteGraph.debug) + console.log(`Connect: Error, no slot of name ${targetIndex}`) return null } } else if (target_slot === LiteGraph.EVENT) { // TODO: Events if (LiteGraph.do_add_triggers_slots) { target_node.changeMode(LGraphEventMode.ON_TRIGGER) - targetIndex = target_node.findInputSlot("onTrigger") + targetIndex = target_node.findInputSlot('onTrigger') } else { return null } - } else if (typeof target_slot === "number") { + } else if (typeof target_slot === 'number') { targetIndex = target_slot } else { targetIndex = 0 @@ -2563,8 +2697,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable // Allow target node to change slot if (target_node.onBeforeConnectInput) { // This way node can choose another slot (or make a new one?) - const requestedIndex = target_node.onBeforeConnectInput(targetIndex, target_slot) - targetIndex = typeof requestedIndex === "number" ? requestedIndex : null + const requestedIndex = target_node.onBeforeConnectInput( + targetIndex, + target_slot + ) + targetIndex = typeof requestedIndex === 'number' ? requestedIndex : null } if ( @@ -2572,7 +2709,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable !target_node.inputs || targetIndex >= target_node.inputs.length ) { - if (LiteGraph.debug) console.log("Connect: Error, slot number not found") + if (LiteGraph.debug) console.log('Connect: Error, slot number not found') return null } @@ -2582,7 +2719,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (!output) return null if (output.links?.length) { - if (output.type === LiteGraph.EVENT && !LiteGraph.allow_multi_output_for_events) { + if ( + output.type === LiteGraph.EVENT && + !LiteGraph.allow_multi_output_for_events + ) { graph.beforeChange() // @ts-expect-error Unused param this.disconnectOutput(slot, false, { doProcessChange: false }) @@ -2605,19 +2745,19 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable output: INodeOutputSlot, inputNode: LGraphNode, input: INodeInputSlot, - afterRerouteId: RerouteId | undefined, + afterRerouteId: RerouteId | undefined ): LLink | null | undefined { const { graph } = this if (!graph) throw new NullGraphError() const outputIndex = this.outputs.indexOf(output) if (outputIndex === -1) { - console.warn("connectSlots: output not found") + console.warn('connectSlots: output not found') return } const inputIndex = inputNode.inputs.indexOf(input) if (inputIndex === -1) { - console.warn("connectSlots: input not found") + console.warn('connectSlots: input not found') return } @@ -2628,9 +2768,25 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } // Allow nodes to block connection - if (inputNode.onConnectInput?.(inputIndex, output.type, output, this, outputIndex) === false) + if ( + inputNode.onConnectInput?.( + inputIndex, + output.type, + output, + this, + outputIndex + ) === false + ) return null - if (this.onConnectOutput?.(outputIndex, input.type, input, inputNode, inputIndex) === false) + if ( + this.onConnectOutput?.( + outputIndex, + input.type, + input, + inputNode, + inputIndex + ) === false + ) return null // if there is something already plugged there, disconnect @@ -2646,7 +2802,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable outputIndex, inputNode.id, inputIndex, - afterRerouteId, + afterRerouteId ) // add to graph links list @@ -2684,7 +2840,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable outputIndex, true, link, - output, + output ) inputNode.onConnectionsChange?.( @@ -2692,7 +2848,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable inputIndex, true, link, - input, + input ) this.setDirtyCanvas(false, true) @@ -2701,26 +2857,31 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable return link } - connectFloatingReroute(pos: Point, slot: INodeInputSlot | INodeOutputSlot, afterRerouteId?: RerouteId): Reroute { + connectFloatingReroute( + pos: Point, + slot: INodeInputSlot | INodeOutputSlot, + afterRerouteId?: RerouteId + ): Reroute { const { graph, id } = this if (!graph) throw new NullGraphError() // Assertion: It's either there or it isn't. const inputIndex = this.inputs.indexOf(slot as INodeInputSlot) const outputIndex = this.outputs.indexOf(slot as INodeOutputSlot) - if (inputIndex === -1 && outputIndex === -1) throw new Error("Invalid slot") + if (inputIndex === -1 && outputIndex === -1) throw new Error('Invalid slot') - const slotType = outputIndex === -1 ? "input" : "output" + const slotType = outputIndex === -1 ? 'input' : 'output' const reroute = graph.setReroute({ pos, parentId: afterRerouteId, linkIds: [], - floating: { slotType }, + floating: { slotType } }) const parentReroute = graph.getReroute(afterRerouteId) - const fromLastFloatingReroute = parentReroute?.floating?.slotType === "output" + const fromLastFloatingReroute = + parentReroute?.floating?.slotType === 'output' // Adding from an ouput, or a floating reroute that is NOT the tip of an existing floating chain if (afterRerouteId == null || !fromLastFloatingReroute) { @@ -2730,7 +2891,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable outputIndex === -1 ? -1 : id, outputIndex, inputIndex === -1 ? -1 : id, - inputIndex, + inputIndex ) link.parentId = reroute.id graph.addFloatingLink(link) @@ -2738,10 +2899,12 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } // Adding a new floating reroute from the tip of a floating chain. - if (!parentReroute) throw new Error("[connectFloatingReroute] Parent reroute not found") + if (!parentReroute) + throw new Error('[connectFloatingReroute] Parent reroute not found') - const link = parentReroute.getFloatingLinks("output")?.[0] - if (!link) throw new Error("[connectFloatingReroute] Floating link not found") + const link = parentReroute.getFloatingLinks('output')?.[0] + if (!link) + throw new Error('[connectFloatingReroute] Floating link not found') reroute.floatingLinkIds.add(link.id) link.parentId = reroute.id @@ -2757,14 +2920,15 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @returns if it was disconnected successfully */ disconnectOutput(slot: string | number, target_node?: LGraphNode): boolean { - if (typeof slot === "string") { + if (typeof slot === 'string') { slot = this.findOutputSlot(slot) if (slot == -1) { - if (LiteGraph.debug) console.log(`Connect: Error, no slot of name ${slot}`) + if (LiteGraph.debug) + console.log(`Connect: Error, no slot of name ${slot}`) return false } } else if (!this.outputs || slot >= this.outputs.length) { - if (LiteGraph.debug) console.log("Connect: Error, slot number not found") + if (LiteGraph.debug) console.log('Connect: Error, slot number not found') return false } @@ -2788,10 +2952,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (!graph) throw new NullGraphError() if (target_node) { - const target = typeof target_node === "number" - ? graph.getNodeById(target_node) - : target_node - if (!target) throw "Target Node not found" + const target = + typeof target_node === 'number' + ? graph.getNodeById(target_node) + : target_node + if (!target) throw 'Target Node not found' for (const [i, link_id] of links.entries()) { const link_info = graph._links.get(link_id) @@ -2805,7 +2970,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable input.link = null // remove the link from the links pool - link_info.disconnect(graph, "input") + link_info.disconnect(graph, 'input') graph._version++ // link_info hasn't been modified so its ok @@ -2814,14 +2979,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable link_info.target_slot, false, link_info, - input, + input ) this.onConnectionsChange?.( NodeSlotType.OUTPUT, slot, false, link_info, - output, + output ) break @@ -2846,18 +3011,18 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable link_info.target_slot, false, link_info, - input, + input ) } // remove the link from the links pool - link_info.disconnect(graph, "input") + link_info.disconnect(graph, 'input') this.onConnectionsChange?.( NodeSlotType.OUTPUT, slot, false, link_info, - output, + output ) } output.links = null @@ -2875,22 +3040,23 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ disconnectInput(slot: number | string, keepReroutes?: boolean): boolean { // Allow search by string - if (typeof slot === "string") { + if (typeof slot === 'string') { slot = this.findInputSlot(slot) if (slot == -1) { - if (LiteGraph.debug) console.log(`Connect: Error, no slot of name ${slot}`) + if (LiteGraph.debug) + console.log(`Connect: Error, no slot of name ${slot}`) return false } } else if (!this.inputs || slot >= this.inputs.length) { if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found") + console.log('Connect: Error, slot number not found') } return false } const input = this.inputs[slot] if (!input) { - console.debug("disconnectInput: input not found", slot, this.inputs) + console.debug('disconnectInput: input not found', slot, this.inputs) return false } @@ -2912,20 +3078,26 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable const link_info = graph._links.get(link_id) if (link_info) { // Let SubgraphInput do the disconnect. - if (link_info.origin_id === -10 && "inputNode" in graph) { + if (link_info.origin_id === -10 && 'inputNode' in graph) { graph.inputNode._disconnectNodeInput(this, input, link_info) return true } const target_node = graph.getNodeById(link_info.origin_id) if (!target_node) { - console.debug("disconnectInput: target node not found", link_info.origin_id) + console.debug( + 'disconnectInput: target node not found', + link_info.origin_id + ) return false } const output = target_node.outputs[link_info.origin_slot] - if (!(output?.links?.length)) { - console.debug("disconnectInput: output not found", link_info.origin_slot) + if (!output?.links?.length) { + console.debug( + 'disconnectInput: output not found', + link_info.origin_slot + ) return false } @@ -2938,7 +3110,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } } - link_info.disconnect(graph, keepReroutes ? "output" : undefined) + link_info.disconnect(graph, keepReroutes ? 'output' : undefined) if (graph) graph._version++ this.onConnectionsChange?.( @@ -2946,14 +3118,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable slot, false, link_info, - input, + input ) target_node.onConnectionsChange?.( NodeSlotType.OUTPUT, i, false, link_info, - output, + output ) } } @@ -2973,7 +3145,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point { out ||= new Float32Array(2) - const { pos: [nodeX, nodeY], inputs, outputs } = this + const { + pos: [nodeX, nodeY], + inputs, + outputs + } = this if (this.flags.collapsed) { const w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH @@ -3009,9 +3185,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable ? 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[0] = is_input ? nodeX + offset : nodeX + this.size[0] + 1 - offset out[1] = nodeY + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + @@ -3024,7 +3198,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ get #defaultVerticalInputs() { return this.inputs.filter( - slot => !slot.pos && !(this.widgets?.length && isWidgetInputSlot(slot)), + (slot) => !slot.pos && !(this.widgets?.length && isWidgetInputSlot(slot)) ) } @@ -3052,7 +3226,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @returns Position of the centre of the input slot in graph co-ordinates. */ getInputSlotPos(input: INodeInputSlot): Point { - const { pos: [nodeX, nodeY] } = this + const { + pos: [nodeX, nodeY] + } = this if (this.flags.collapsed) { const halfTitle = LiteGraph.NODE_TITLE_HEIGHT * 0.5 @@ -3079,7 +3255,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @returns Position of the output slot */ getOutputPos(slot: number): Point { - const { pos: [nodeX, nodeY], outputs, size: [width] } = this + const { + pos: [nodeX, nodeY], + outputs, + size: [width] + } = this if (this.flags.collapsed) { const width = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH @@ -3115,24 +3295,27 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable this.console ||= [] this.console.push(msg) // @ts-expect-error deprecated - if (this.console.length > LGraphNode.MAX_CONSOLE) - this.console.shift() + if (this.console.length > LGraphNode.MAX_CONSOLE) this.console.shift() } /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ setDirtyCanvas(dirty_foreground: boolean, dirty_background?: boolean): void { - this.graph?.canvasAction(c => c.setDirty(dirty_foreground, dirty_background)) + this.graph?.canvasAction((c) => + c.setDirty(dirty_foreground, dirty_background) + ) } loadImage(url: string): HTMLImageElement { - interface AsyncImageElement extends HTMLImageElement { ready?: boolean } + interface AsyncImageElement extends HTMLImageElement { + ready?: boolean + } const img: AsyncImageElement = new Image() img.src = LiteGraph.node_images_path + url img.ready = false const dirty = () => this.setDirtyCanvas(true) - img.addEventListener("load", function (this: AsyncImageElement) { + img.addEventListener('load', function (this: AsyncImageElement) { this.ready = true dirty() }) @@ -3144,7 +3327,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * @deprecated Use {@link LGraphCanvas.pointer} instead. */ captureInput(v: boolean): void { - warnDeprecated("[DEPRECATED] captureInput will be removed in a future version. Please use LGraphCanvas.pointer (CanvasPointer) instead.") + warnDeprecated( + '[DEPRECATED] captureInput will be removed in a future version. Please use LGraphCanvas.pointer (CanvasPointer) instead.' + ) if (!this.graph || !this.graph.list_of_graphcanvas) return const list = this.graph.list_of_graphcanvas @@ -3181,7 +3366,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * Toggles advanced mode of the node, showing advanced widgets */ toggleAdvanced() { - if (!this.widgets?.some(w => w.advanced)) return + if (!this.widgets?.some((w) => w.advanced)) return if (!this.graph) throw new NullGraphError() this.graph._version++ this.showAdvanced = !this.showAdvanced @@ -3215,7 +3400,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable localToScreen(x: number, y: number, dragAndScale: DragAndScale): Point { return [ (x + this.pos[0]) * dragAndScale.scale + dragAndScale.offset[0], - (y + this.pos[1]) * dragAndScale.scale + dragAndScale.offset[1], + (y + this.pos[1]) * dragAndScale.scale + dragAndScale.offset[1] ] } @@ -3240,13 +3425,18 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } drawBadges(ctx: CanvasRenderingContext2D, { gap = 2 } = {}): void { - const badgeInstances = this.badges.map(badge => - badge instanceof LGraphBadge ? badge : badge()) + const badgeInstances = this.badges.map((badge) => + badge instanceof LGraphBadge ? badge : badge() + ) const isLeftAligned = this.badgePosition === BadgePosition.TopLeft let currentX = isLeftAligned ? 0 - : this.width - badgeInstances.reduce((acc, badge) => acc + badge.getWidth(ctx) + gap, 0) + : this.width - + badgeInstances.reduce( + (acc, badge) => acc + badge.getWidth(ctx) + gap, + 0 + ) const y = -(LiteGraph.NODE_TITLE_HEIGHT + gap) for (const badge of badgeInstances) { @@ -3258,11 +3448,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable /** * Renders the node's title bar background */ - drawTitleBarBackground(ctx: CanvasRenderingContext2D, { - scale, - title_height = LiteGraph.NODE_TITLE_HEIGHT, - low_quality = false, - }: DrawTitleOptions): void { + drawTitleBarBackground( + ctx: CanvasRenderingContext2D, + { + scale, + title_height = LiteGraph.NODE_TITLE_HEIGHT, + low_quality = false + }: DrawTitleOptions + ): void { const fgcolor = this.renderingColor const shape = this.renderingShape const size = this.renderingSize @@ -3293,11 +3486,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable title_height, this.collapsed ? [LiteGraph.ROUND_RADIUS] - : [LiteGraph.ROUND_RADIUS, LiteGraph.ROUND_RADIUS, 0, 0], + : [LiteGraph.ROUND_RADIUS, LiteGraph.ROUND_RADIUS, 0, 0] ) } ctx.fill() - ctx.shadowColor = "transparent" + ctx.shadowColor = 'transparent' } /** @@ -3305,12 +3498,15 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * when clicked toggles the node's collapsed state. The term `title box` comes * from the original LiteGraph implementation. */ - drawTitleBox(ctx: CanvasRenderingContext2D, { - scale, - low_quality = false, - title_height = LiteGraph.NODE_TITLE_HEIGHT, - box_size = 10, - }: DrawTitleBoxOptions): void { + drawTitleBox( + ctx: CanvasRenderingContext2D, + { + scale, + low_quality = false, + title_height = LiteGraph.NODE_TITLE_HEIGHT, + box_size = 10 + }: DrawTitleBoxOptions + ): void { const size = this.renderingSize const shape = this.renderingShape @@ -3323,14 +3519,14 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable [RenderShape.ROUND, RenderShape.CIRCLE, RenderShape.CARD].includes(shape) ) { if (low_quality) { - ctx.fillStyle = "black" + ctx.fillStyle = 'black' ctx.beginPath() ctx.arc( title_height * 0.5, title_height * -0.5, box_size * 0.5 + 1, 0, - Math.PI * 2, + Math.PI * 2 ) ctx.fill() } @@ -3341,7 +3537,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable title_height * 0.5 - box_size * 0.5, title_height * -0.5 - box_size * 0.5, box_size, - box_size, + box_size ) } else { ctx.beginPath() @@ -3350,18 +3546,18 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable title_height * -0.5, box_size * 0.5, 0, - Math.PI * 2, + Math.PI * 2 ) ctx.fill() } } else { if (low_quality) { - ctx.fillStyle = "black" + ctx.fillStyle = 'black' ctx.fillRect( (title_height - box_size) * 0.5 - 1, (title_height + box_size) * -0.5 - 1, box_size + 2, - box_size + 2, + box_size + 2 ) } ctx.fillStyle = this.renderingBoxColor @@ -3369,7 +3565,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable (title_height - box_size) * 0.5, (title_height + box_size) * -0.5, box_size, - box_size, + box_size ) } } @@ -3377,12 +3573,15 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable /** * Renders the node's title text. */ - drawTitleText(ctx: CanvasRenderingContext2D, { - scale, - default_title_color, - low_quality = false, - title_height = LiteGraph.NODE_TITLE_HEIGHT, - }: DrawTitleTextOptions): void { + drawTitleText( + ctx: CanvasRenderingContext2D, + { + scale, + default_title_color, + low_quality = false, + title_height = LiteGraph.NODE_TITLE_HEIGHT + }: DrawTitleTextOptions + ): void { const size = this.renderingSize const selected = this.selected @@ -3393,7 +3592,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable size, scale, this.titleFontStyle, - selected, + selected ) return } @@ -3405,7 +3604,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable ctx.font = this.titleFontStyle const rawTitle = this.getTitle() ?? `❌ ${this.type}` - const title = String(rawTitle) + (this.pinned ? "📌" : "") + const title = String(rawTitle) + (this.pinned ? '📌' : '') if (title) { if (selected) { ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR @@ -3443,11 +3642,11 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable displayTitle = truncateText(ctx, title, availableWidth) } - ctx.textAlign = "left" + ctx.textAlign = 'left' ctx.fillText( displayTitle, title_height, - LiteGraph.NODE_TITLE_TEXT_Y - title_height, + LiteGraph.NODE_TITLE_TEXT_Y - title_height ) } } @@ -3480,7 +3679,8 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (input.link == null) continue const output = outputs[index] - if (!output || !LiteGraph.isValidConnection(input.type, output.type)) continue + if (!output || !LiteGraph.isValidConnection(input.type, output.type)) + continue const inLink = _links.get(input.link) if (!inLink) continue @@ -3511,10 +3711,15 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable } return madeAnyConnections - function bypassAllLinks(output: INodeOutputSlot, inNode: LGraphNode, inLink: LLink, graph: LGraph) { + function bypassAllLinks( + output: INodeOutputSlot, + inNode: LGraphNode, + inLink: LLink, + graph: LGraph + ) { const outLinks = output.links - ?.map(x => _links.get(x)) - .filter(x => !!x) + ?.map((x) => _links.get(x)) + .filter((x) => !!x) if (!outLinks?.length) return for (const outLink of outLinks) { @@ -3525,7 +3730,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable inLink.origin_slot, outNode, outLink.target_slot, - inLink.parentId, + inLink.parentId ) madeAnyConnections ||= !!result } @@ -3536,18 +3741,15 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * Returns `true` if the widget is visible, otherwise `false`. */ isWidgetVisible(widget: IBaseWidget): boolean { - const isHidden = ( - this.collapsed || - widget.hidden || - (widget.advanced && !this.showAdvanced) - ) + const isHidden = + this.collapsed || widget.hidden || (widget.advanced && !this.showAdvanced) return !isHidden } - drawWidgets(ctx: CanvasRenderingContext2D, { - lowQuality = false, - editorAlpha = 1, - }: DrawWidgetsOptions): void { + drawWidgets( + ctx: CanvasRenderingContext2D, + { lowQuality = false, editorAlpha = 1 }: DrawWidgetsOptions + ): void { if (!this.widgets) return const nodeWidth = this.size[0] @@ -3561,22 +3763,28 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (!this.isWidgetVisible(widget)) continue const { y } = widget - const outlineColour = widget.advanced ? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR : LiteGraph.WIDGET_OUTLINE_COLOR + const outlineColour = widget.advanced + ? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR + : LiteGraph.WIDGET_OUTLINE_COLOR widget.last_y = y // Disable widget if it is disabled or if the value is passed from socket connection. - widget.computedDisabled = widget.disabled || this.getSlotFromWidget(widget)?.link != null + widget.computedDisabled = + widget.disabled || this.getSlotFromWidget(widget)?.link != null ctx.strokeStyle = outlineColour - ctx.fillStyle = "#222" - ctx.textAlign = "left" + ctx.fillStyle = '#222' + ctx.textAlign = 'left' if (widget.computedDisabled) ctx.globalAlpha *= 0.5 const width = widget.width || nodeWidth - if (typeof widget.draw === "function") { + if (typeof widget.draw === 'function') { widget.draw(ctx, this, width, y, H, lowQuality) } else { - toConcreteWidget(widget, this, false)?.drawWidget(ctx, { width, showText }) + toConcreteWidget(widget, this, false)?.drawWidget(ctx, { + width, + showText + }) } ctx.globalAlpha = editorAlpha } @@ -3606,12 +3814,20 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable return [...this.inputs, ...this.outputs] } - #measureSlot(slot: NodeInputSlot | NodeOutputSlot, slotIndex: number, isInput: boolean): void { - const pos = isInput ? this.getInputPos(slotIndex) : this.getOutputPos(slotIndex) + #measureSlot( + slot: NodeInputSlot | NodeOutputSlot, + slotIndex: number, + isInput: boolean + ): void { + const pos = isInput + ? this.getInputPos(slotIndex) + : this.getOutputPos(slotIndex) slot.boundingRect[0] = pos[0] - LiteGraph.NODE_SLOT_HEIGHT * 0.5 slot.boundingRect[1] = pos[1] - LiteGraph.NODE_SLOT_HEIGHT * 0.5 - slot.boundingRect[2] = slot.isWidgetInputSlot ? BaseWidget.margin : LiteGraph.NODE_SLOT_HEIGHT + slot.boundingRect[2] = slot.isWidgetInputSlot + ? BaseWidget.margin + : LiteGraph.NODE_SLOT_HEIGHT slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT } @@ -3637,7 +3853,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable #getMouseOverSlot(slot: INodeSlot): INodeSlot | null { const isInput = isINodeInputSlot(slot) - const mouseOverId = this.mouseOver?.[isInput ? "inputId" : "outputId"] ?? -1 + const mouseOverId = this.mouseOver?.[isInput ? 'inputId' : 'outputId'] ?? -1 if (mouseOverId === -1) { return null } @@ -3656,8 +3872,13 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable /** * Returns the input slot that is associated with the given widget. */ - getSlotFromWidget(widget: IBaseWidget | undefined): INodeInputSlot | undefined { - if (widget) return this.inputs.find(slot => isWidgetInputSlot(slot) && slot.widget.name === widget.name) + getSlotFromWidget( + widget: IBaseWidget | undefined + ): INodeInputSlot | undefined { + if (widget) + return this.inputs.find( + (slot) => isWidgetInputSlot(slot) && slot.widget.name === widget.name + ) } /** @@ -3665,18 +3886,16 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ getWidgetFromSlot(slot: INodeInputSlot): IBaseWidget | undefined { if (!isWidgetInputSlot(slot)) return - return this.widgets?.find(w => w.name === slot.widget.name) + return this.widgets?.find((w) => w.name === slot.widget.name) } /** * Draws the node's input and output slots. */ - drawSlots(ctx: CanvasRenderingContext2D, { - fromSlot, - colorContext, - editorAlpha, - lowQuality, - }: DrawSlotsOptions) { + drawSlots( + ctx: CanvasRenderingContext2D, + { fromSlot, colorContext, editorAlpha, lowQuality }: DrawSlotsOptions + ) { for (const slot of [...this.#concreteInputs, ...this.#concreteOutputs]) { const isValidTarget = fromSlot && slot.isValidTarget(fromSlot) const isMouseOverSlot = this.#isMouseOverSlot(slot) @@ -3701,7 +3920,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable slot.draw(ctx, { colorContext, lowQuality, - highlight, + highlight }) } } @@ -3718,9 +3937,8 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (!this.widgets || !this.widgets.length) return const bodyHeight = this.bodyHeight - const startY = this.widgets_start_y ?? ( - (this.widgets_up ? 0 : widgetStartY) + 2 - ) + const startY = + this.widgets_start_y ?? (this.widgets_up ? 0 : widgetStartY) + 2 let freeSpace = bodyHeight - startY @@ -3742,7 +3960,7 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable growableWidgets.push({ minHeight, prefHeight: maxHeight, - w, + w }) } else { const height = LiteGraph.NODE_WIDGET_HEIGHT + 4 @@ -3756,9 +3974,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable this.freeWidgetSpace = freeSpace // Prepare space requests for distribution - const spaceRequests = growableWidgets.map(d => ({ + const spaceRequests = growableWidgets.map((d) => ({ minSize: d.minHeight, - maxSize: d.prefHeight, + maxSize: d.prefHeight })) // Distribute space among DOM widgets @@ -3794,7 +4012,10 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable #arrangeWidgetInputSlots(): void { if (!this.widgets) return - const slotByWidgetName = new Map() + const slotByWidgetName = new Map< + string, + INodeInputSlot & { index: number } + >() for (const [i, slot] of this.inputs.entries()) { if (!isWidgetInputSlot(slot)) continue @@ -3822,8 +4043,12 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable * have been removed from the ecosystem. */ _setConcreteSlots(): void { - this.#concreteInputs = this.inputs.map(slot => toClass(NodeInputSlot, slot, this)) - this.#concreteOutputs = this.outputs.map(slot => toClass(NodeOutputSlot, slot, this)) + this.#concreteInputs = this.inputs.map((slot) => + toClass(NodeInputSlot, slot, this) + ) + this.#concreteOutputs = this.outputs.map((slot) => + toClass(NodeOutputSlot, slot, this) + ) } /** @@ -3831,7 +4056,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable */ arrange(): void { const slotsBounds = this.#measureSlots() - const widgetStartY = slotsBounds ? slotsBounds[1] + slotsBounds[3] - this.pos[1] : 0 + const widgetStartY = slotsBounds + ? slotsBounds[1] + slotsBounds[3] - this.pos[1] + : 0 this.#arrangeWidgets(widgetStartY) this.#arrangeWidgetInputSlots() } @@ -3844,13 +4071,8 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable if (!this.progress) return const originalFillStyle = ctx.fillStyle - ctx.fillStyle = "green" - ctx.fillRect( - 0, - 0, - this.width * this.progress, - 6, - ) + ctx.fillStyle = 'green' + ctx.fillRect(0, 0, this.width * this.progress, 6) ctx.fillStyle = originalFillStyle } } diff --git a/src/lib/litegraph/src/LLink.ts b/src/lib/litegraph/src/LLink.ts index 299473598a..b8de0f9304 100644 --- a/src/lib/litegraph/src/LLink.ts +++ b/src/lib/litegraph/src/LLink.ts @@ -1,3 +1,10 @@ +import { + SUBGRAPH_INPUT_ID, + SUBGRAPH_OUTPUT_ID +} from '@/lib/litegraph/src/constants' + +import type { LGraphNode, NodeId } from './LGraphNode' +import type { Reroute, RerouteId } from './Reroute' import type { CanvasColour, INodeInputSlot, @@ -5,15 +12,14 @@ import type { ISlotType, LinkNetwork, LinkSegment, - ReadonlyLinkNetwork, -} from "./interfaces" -import type { LGraphNode, NodeId } from "./LGraphNode" -import type { Reroute, RerouteId } from "./Reroute" -import type { Serialisable, SerialisableLLink, SubgraphIO } from "./types/serialisation" - -import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from "@/lib/litegraph/src/constants" - -import { Subgraph } from "./litegraph" + ReadonlyLinkNetwork +} from './interfaces' +import { Subgraph } from './litegraph' +import type { + Serialisable, + SerialisableLLink, + SubgraphIO +} from './types/serialisation' export type LinkId = number @@ -23,15 +29,15 @@ export type SerialisedLLinkArray = [ origin_slot: number, target_id: NodeId, target_slot: number, - type: ISlotType, + type: ISlotType ] // Resolved connection union; eliminates subgraph in/out as a possibility export type ResolvedConnection = BaseResolvedConnection & ( - (ResolvedSubgraphInput & ResolvedNormalOutput) | - (ResolvedNormalInput & ResolvedSubgraphOutput) | - (ResolvedNormalInput & ResolvedNormalOutput) + | (ResolvedSubgraphInput & ResolvedNormalOutput) + | (ResolvedNormalInput & ResolvedSubgraphOutput) + | (ResolvedNormalInput & ResolvedNormalOutput) ) interface BaseResolvedConnection { @@ -75,7 +81,10 @@ interface ResolvedSubgraphOutput { subgraphInput: SubgraphIO } -type BasicReadonlyNetwork = Pick +type BasicReadonlyNetwork = Pick< + ReadonlyLinkNetwork, + 'getNodeById' | 'links' | 'getLink' | 'inputNode' | 'outputNode' +> // this is the class in charge of storing link information export class LLink implements LinkSegment, Serialisable { @@ -115,7 +124,7 @@ export class LLink implements LinkSegment, Serialisable { } public set color(value: CanvasColour) { - this.#color = value === "" ? null : value + this.#color = value === '' ? null : value } public get isFloatingOutput(): boolean { @@ -147,7 +156,7 @@ export class LLink implements LinkSegment, Serialisable { origin_slot: number, target_id: NodeId, target_slot: number, - parentId?: RerouteId, + parentId?: RerouteId ) { this.id = id this.type = type @@ -180,7 +189,7 @@ export class LLink implements LinkSegment, Serialisable { data.origin_slot, data.target_id, data.target_slot, - data.parentId, + data.parentId ) } @@ -190,18 +199,16 @@ export class LLink implements LinkSegment, Serialisable { * this reroute or the reroute before it. Otherwise, an empty array. */ static getReroutes( - network: Pick, - linkSegment: LinkSegment, + network: Pick, + linkSegment: LinkSegment ): Reroute[] { if (!linkSegment.parentId) return [] - return network.reroutes - .get(linkSegment.parentId) - ?.getReroutes() ?? [] + return network.reroutes.get(linkSegment.parentId)?.getReroutes() ?? [] } static getFirstReroute( - network: Pick, - linkSegment: LinkSegment, + network: Pick, + linkSegment: LinkSegment ): Reroute | undefined { return LLink.getReroutes(network, linkSegment).at(0) } @@ -215,9 +222,9 @@ export class LLink implements LinkSegment, Serialisable { * @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected. */ static findNextReroute( - network: Pick, + network: Pick, linkSegment: LinkSegment, - rerouteId: RerouteId, + rerouteId: RerouteId ): Reroute | null | undefined { if (!linkSegment.parentId) return return network.reroutes @@ -231,7 +238,10 @@ export class LLink implements LinkSegment, Serialisable { * @param linkId The ID of the link to get the origin node of * @returns The origin node of the link, or `undefined` if the link is not found or the origin node is not found */ - static getOriginNode(network: BasicReadonlyNetwork, linkId: LinkId): LGraphNode | undefined { + static getOriginNode( + network: BasicReadonlyNetwork, + linkId: LinkId + ): LGraphNode | undefined { const id = network.links.get(linkId)?.origin_id return network.getNodeById(id) ?? undefined } @@ -242,7 +252,10 @@ export class LLink implements LinkSegment, Serialisable { * @param linkId The ID of the link to get the target node of * @returns The target node of the link, or `undefined` if the link is not found or the target node is not found */ - static getTargetNode(network: BasicReadonlyNetwork, linkId: LinkId): LGraphNode | undefined { + static getTargetNode( + network: BasicReadonlyNetwork, + linkId: LinkId + ): LGraphNode | undefined { const id = network.links.get(linkId)?.target_id return network.getNodeById(id) ?? undefined } @@ -256,7 +269,10 @@ export class LLink implements LinkSegment, Serialisable { * Whilst the performance difference should in most cases be negligible, * it is recommended to use simpler methods where appropriate. */ - static resolve(linkId: LinkId | null | undefined, network: BasicReadonlyNetwork): ResolvedConnection | undefined { + static resolve( + linkId: LinkId | null | undefined, + network: BasicReadonlyNetwork + ): ResolvedConnection | undefined { return network.getLink(linkId)?.resolve(network) } @@ -268,7 +284,10 @@ export class LLink implements LinkSegment, Serialisable { * @returns An array of resolved connections. If a link is not found, it is not included in the array. * @see {@link LLink.resolve} */ - static resolveMany(linkIds: Iterable, network: BasicReadonlyNetwork): ResolvedConnection[] { + static resolveMany( + linkIds: Iterable, + network: BasicReadonlyNetwork + ): ResolvedConnection[] { const resolved: ResolvedConnection[] = [] for (const id of linkIds) { const r = network.getLink(id)?.resolve(network) @@ -286,21 +305,45 @@ export class LLink implements LinkSegment, Serialisable { * it is recommended to use simpler methods where appropriate. */ resolve(network: BasicReadonlyNetwork): ResolvedConnection { - const inputNode = this.target_id === -1 ? undefined : network.getNodeById(this.target_id) ?? undefined + const inputNode = + this.target_id === -1 + ? undefined + : network.getNodeById(this.target_id) ?? undefined const input = inputNode?.inputs[this.target_slot] - const subgraphInput = this.originIsIoNode ? network.inputNode?.slots[this.origin_slot] : undefined + const subgraphInput = this.originIsIoNode + ? network.inputNode?.slots[this.origin_slot] + : undefined if (subgraphInput) { return { inputNode, input, subgraphInput, link: this } } - const outputNode = this.origin_id === -1 ? undefined : network.getNodeById(this.origin_id) ?? undefined + const outputNode = + this.origin_id === -1 + ? undefined + : network.getNodeById(this.origin_id) ?? undefined const output = outputNode?.outputs[this.origin_slot] - const subgraphOutput = this.targetIsIoNode ? network.outputNode?.slots[this.target_slot] : undefined + const subgraphOutput = this.targetIsIoNode + ? network.outputNode?.slots[this.target_slot] + : undefined if (subgraphOutput) { - return { outputNode, output, subgraphInput: undefined, subgraphOutput, link: this } + return { + outputNode, + output, + subgraphInput: undefined, + subgraphOutput, + link: this + } } - return { inputNode, outputNode, input, output, subgraphInput, subgraphOutput, link: this } + return { + inputNode, + outputNode, + input, + output, + subgraphInput, + subgraphOutput, + link: this + } } configure(o: LLink | SerialisedLLinkArray) { @@ -348,12 +391,12 @@ export class LLink implements LinkSegment, Serialisable { * @param parentId The parent reroute ID of the link * @returns A new LLink that is floating */ - toFloating(slotType: "input" | "output", parentId: RerouteId): LLink { + toFloating(slotType: 'input' | 'output', parentId: RerouteId): LLink { const exported = this.asSerialisable() exported.id = -1 exported.parentId = parentId - if (slotType === "input") { + if (slotType === 'input') { exported.origin_id = -1 exported.origin_slot = -1 } else { @@ -370,31 +413,32 @@ export class LLink implements LinkSegment, Serialisable { * @param keepReroutes If `undefined`, reroutes will be automatically removed if no links remain. * If `input` or `output`, reroutes will not be automatically removed, and retain a connection to the input or output, respectively. */ - disconnect(network: LinkNetwork, keepReroutes?: "input" | "output"): void { + disconnect(network: LinkNetwork, keepReroutes?: 'input' | 'output'): void { const reroutes = LLink.getReroutes(network, this) const lastReroute = reroutes.at(-1) // When floating from output, 1-to-1 ratio of floating link to final reroute (tree-like) - const outputFloating = keepReroutes === "output" && + const outputFloating = + keepReroutes === 'output' && lastReroute?.linkIds.size === 1 && lastReroute.floatingLinkIds.size === 0 // When floating from inputs, the final (input side) reroute may have many floating links - if (outputFloating || (keepReroutes === "input" && lastReroute)) { + if (outputFloating || (keepReroutes === 'input' && lastReroute)) { const newLink = LLink.create(this) newLink.id = -1 - if (keepReroutes === "input") { + if (keepReroutes === 'input') { newLink.origin_id = -1 newLink.origin_slot = -1 - lastReroute.floating = { slotType: "input" } + lastReroute.floating = { slotType: 'input' } } else { newLink.target_id = -1 newLink.target_slot = -1 - lastReroute.floating = { slotType: "output" } + lastReroute.floating = { slotType: 'output' } } network.addFloatingLink(newLink) @@ -410,9 +454,12 @@ export class LLink implements LinkSegment, Serialisable { if (this.originIsIoNode && network instanceof Subgraph) { const subgraphInput = network.inputs.at(this.origin_slot) - if (!subgraphInput) throw new Error("Invalid link - subgraph input not found") + if (!subgraphInput) + throw new Error('Invalid link - subgraph input not found') - subgraphInput.events.dispatch("input-disconnected", { input: subgraphInput }) + subgraphInput.events.dispatch('input-disconnected', { + input: subgraphInput + }) } } @@ -427,7 +474,7 @@ export class LLink implements LinkSegment, Serialisable { this.origin_slot, this.target_id, this.target_slot, - this.type, + this.type ] } @@ -438,7 +485,7 @@ export class LLink implements LinkSegment, Serialisable { origin_slot: this.origin_slot, target_id: this.target_id, target_slot: this.target_slot, - type: this.type, + type: this.type } if (this.parentId) copy.parentId = this.parentId return copy diff --git a/src/lib/litegraph/src/LiteGraphGlobal.ts b/src/lib/litegraph/src/LiteGraphGlobal.ts index c930a39ebe..895b650941 100644 --- a/src/lib/litegraph/src/LiteGraphGlobal.ts +++ b/src/lib/litegraph/src/LiteGraphGlobal.ts @@ -1,29 +1,28 @@ -import type { Dictionary, ISlotType, Rect, WhenNullish } from "./interfaces" - -import { InputIndicators } from "./canvas/InputIndicators" -import { ContextMenu } from "./ContextMenu" -import { CurveEditor } from "./CurveEditor" -import { DragAndScale } from "./DragAndScale" -import { LabelPosition, SlotDirection, SlotShape, SlotType } from "./draw" -import { Rectangle } from "./infrastructure/Rectangle" -import { LGraph } from "./LGraph" -import { LGraphCanvas } from "./LGraphCanvas" -import { LGraphGroup } from "./LGraphGroup" -import { LGraphNode } from "./LGraphNode" -import { LLink } from "./LLink" -import { distance, isInsideRectangle, overlapBounding } from "./measure" -import { Reroute } from "./Reroute" -import { SubgraphIONodeBase } from "./subgraph/SubgraphIONodeBase" -import { SubgraphSlot } from "./subgraph/SubgraphSlotBase" +import { ContextMenu } from './ContextMenu' +import { CurveEditor } from './CurveEditor' +import { DragAndScale } from './DragAndScale' +import { LGraph } from './LGraph' +import { LGraphCanvas } from './LGraphCanvas' +import { LGraphGroup } from './LGraphGroup' +import { LGraphNode } from './LGraphNode' +import { LLink } from './LLink' +import { Reroute } from './Reroute' +import { InputIndicators } from './canvas/InputIndicators' +import { LabelPosition, SlotDirection, SlotShape, SlotType } from './draw' +import { Rectangle } from './infrastructure/Rectangle' +import type { Dictionary, ISlotType, Rect, WhenNullish } from './interfaces' +import { distance, isInsideRectangle, overlapBounding } from './measure' +import { SubgraphIONodeBase } from './subgraph/SubgraphIONodeBase' +import { SubgraphSlot } from './subgraph/SubgraphSlotBase' import { LGraphEventMode, LinkDirection, LinkRenderType, NodeSlotType, RenderShape, - TitleMode, -} from "./types/globalEnums" -import { createUuidv4 } from "./utils/uuid" + TitleMode +} from './types/globalEnums' +import { createUuidv4 } from './utils/uuid' /** * The Global Scope. It contains all the registered node classes. @@ -48,44 +47,47 @@ export class LiteGraphGlobal { NODE_MIN_WIDTH = 50 NODE_COLLAPSED_RADIUS = 10 NODE_COLLAPSED_WIDTH = 80 - NODE_TITLE_COLOR = "#999" - NODE_SELECTED_TITLE_COLOR = "#FFF" + NODE_TITLE_COLOR = '#999' + NODE_SELECTED_TITLE_COLOR = '#FFF' NODE_TEXT_SIZE = 14 - NODE_TEXT_COLOR = "#AAA" - NODE_TEXT_HIGHLIGHT_COLOR = "#EEE" + NODE_TEXT_COLOR = '#AAA' + NODE_TEXT_HIGHLIGHT_COLOR = '#EEE' NODE_SUBTEXT_SIZE = 12 - NODE_DEFAULT_COLOR = "#333" - NODE_DEFAULT_BGCOLOR = "#353535" - NODE_DEFAULT_BOXCOLOR = "#666" + NODE_DEFAULT_COLOR = '#333' + NODE_DEFAULT_BGCOLOR = '#353535' + NODE_DEFAULT_BOXCOLOR = '#666' NODE_DEFAULT_SHAPE = RenderShape.ROUND - NODE_BOX_OUTLINE_COLOR = "#FFF" - NODE_ERROR_COLOUR = "#E00" - NODE_FONT = "Arial" + NODE_BOX_OUTLINE_COLOR = '#FFF' + NODE_ERROR_COLOUR = '#E00' + NODE_FONT = 'Arial' - DEFAULT_FONT = "Arial" - DEFAULT_SHADOW_COLOR = "rgba(0,0,0,0.5)" + DEFAULT_FONT = 'Arial' + DEFAULT_SHADOW_COLOR = 'rgba(0,0,0,0.5)' DEFAULT_GROUP_FONT = 24 DEFAULT_GROUP_FONT_SIZE?: any - GROUP_FONT = "Arial" + GROUP_FONT = 'Arial' - WIDGET_BGCOLOR = "#222" - WIDGET_OUTLINE_COLOR = "#666" - WIDGET_ADVANCED_OUTLINE_COLOR = "rgba(56, 139, 253, 0.8)" - WIDGET_TEXT_COLOR = "#DDD" - WIDGET_SECONDARY_TEXT_COLOR = "#999" - WIDGET_DISABLED_TEXT_COLOR = "#666" + WIDGET_BGCOLOR = '#222' + WIDGET_OUTLINE_COLOR = '#666' + WIDGET_ADVANCED_OUTLINE_COLOR = 'rgba(56, 139, 253, 0.8)' + WIDGET_TEXT_COLOR = '#DDD' + WIDGET_SECONDARY_TEXT_COLOR = '#999' + WIDGET_DISABLED_TEXT_COLOR = '#666' - LINK_COLOR = "#9A9" - EVENT_LINK_COLOR = "#A86" - CONNECTING_LINK_COLOR = "#AFA" + LINK_COLOR = '#9A9' + EVENT_LINK_COLOR = '#A86' + CONNECTING_LINK_COLOR = '#AFA' /** avoid infinite loops */ MAX_NUMBER_OF_NODES = 10_000 /** default node position */ DEFAULT_POSITION = [100, 100] /** ,"circle" */ - VALID_SHAPES = ["default", "box", "round", "card"] satisfies ("default" | Lowercase)[] + VALID_SHAPES = ['default', 'box', 'round', 'card'] satisfies ( + | 'default' + | Lowercase + )[] ROUND_RADIUS = 8 // shapes are used for nodes but also for slots @@ -108,9 +110,9 @@ export class LiteGraphGlobal { ACTION = -1 as const /** helper, will add "On Request" and more in the future */ - NODE_MODES = ["Always", "On Event", "Never", "On Trigger"] + NODE_MODES = ['Always', 'On Event', 'Never', 'On Trigger'] /** use with node_box_coloured_by_mode */ - NODE_MODES_COLORS = ["#666", "#422", "#333", "#224", "#626"] + NODE_MODES_COLORS = ['#666', '#422', '#333', '#224', '#626'] ALWAYS = LGraphEventMode.ALWAYS ON_EVENT = LGraphEventMode.ON_EVENT NEVER = LGraphEventMode.NEVER @@ -123,7 +125,7 @@ export class LiteGraphGlobal { CENTER = LinkDirection.CENTER /** helper */ - LINK_RENDER_MODES = ["Straight", "Linear", "Spline"] + LINK_RENDER_MODES = ['Straight', 'Linear', 'Spline'] HIDDEN_LINK = LinkRenderType.HIDDEN_LINK STRAIGHT_LINK = LinkRenderType.STRAIGHT_LINK LINEAR_LINK = LinkRenderType.LINEAR_LINK @@ -135,11 +137,11 @@ export class LiteGraphGlobal { AUTOHIDE_TITLE = TitleMode.AUTOHIDE_TITLE /** arrange nodes vertically */ - VERTICAL_LAYOUT = "vertical" + VERTICAL_LAYOUT = 'vertical' /** used to redirect calls */ proxy = null - node_images_path = "" + node_images_path = '' debug = false catch_exceptions = true @@ -249,7 +251,7 @@ export class LiteGraphGlobal { release_link_on_empty_shows_menu = false /** "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) */ - pointerevents_method = "pointer" + pointerevents_method = 'pointer' /** * [true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected @@ -277,7 +279,9 @@ export class LiteGraphGlobal { * Array of callbacks to execute when Litegraph first reports a deprecated API being used. * @see alwaysRepeatWarnings By default, will not repeat identical messages. */ - onDeprecationWarning: ((message: string, source?: object) => void)[] = [console.warn] + onDeprecationWarning: ((message: string, source?: object) => void)[] = [ + console.warn + ] /** * If `true`, mouse wheel events will be interpreted as trackpad gestures. @@ -300,7 +304,7 @@ export class LiteGraphGlobal { * "legacy": Enable dragging on left-click (original behavior) * @default "legacy" */ - canvasNavigationMode: "standard" | "legacy" = "legacy" + canvasNavigationMode: 'standard' | 'legacy' = 'legacy' /** * If `true`, widget labels and values will both be truncated (proportionally to size), @@ -336,22 +340,34 @@ export class LiteGraphGlobal { Reroute = Reroute constructor() { - Object.defineProperty(this, "Classes", { writable: false }) + Object.defineProperty(this, 'Classes', { writable: false }) } Classes = { - get SubgraphSlot() { return SubgraphSlot }, - get SubgraphIONodeBase() { return SubgraphIONodeBase }, + get SubgraphSlot() { + return SubgraphSlot + }, + get SubgraphIONodeBase() { + return SubgraphIONodeBase + }, // Rich drawing - get Rectangle() { return Rectangle }, + get Rectangle() { + return Rectangle + }, // Debug / helpers - get InputIndicators() { return InputIndicators }, + get InputIndicators() { + return InputIndicators + } } onNodeTypeRegistered?(type: string, base_class: typeof LGraphNode): void - onNodeTypeReplaced?(type: string, base_class: typeof LGraphNode, prev: unknown): void + onNodeTypeReplaced?( + type: string, + base_class: typeof LGraphNode, + prev: unknown + ): void /** * Register a node class so it can be listed when the user wants to create a new one @@ -360,14 +376,14 @@ export class LiteGraphGlobal { */ registerNodeType(type: string, base_class: typeof LGraphNode): void { if (!base_class.prototype) - throw "Cannot register a simple object, it must be a class with a prototype" + throw 'Cannot register a simple object, it must be a class with a prototype' base_class.type = type - if (this.debug) console.log("Node registered:", type) + if (this.debug) console.log('Node registered:', type) const classname = base_class.name - const pos = type.lastIndexOf("/") + const pos = type.lastIndexOf('/') base_class.category = type.substring(0, pos) base_class.title ||= classname @@ -380,7 +396,7 @@ export class LiteGraphGlobal { const prev = this.registered_node_types[type] if (prev && this.debug) { - console.log("replacing node type:", type) + console.log('replacing node type:', type) } this.registered_node_types[type] = base_class @@ -391,10 +407,12 @@ export class LiteGraphGlobal { // warnings if (base_class.prototype.onPropertyChange) - console.warn(`LiteGraph node class ${type} has onPropertyChange method, it must be called onPropertyChanged with d at the end`) + console.warn( + `LiteGraph node class ${type} has onPropertyChange method, it must be called onPropertyChanged with d at the end` + ) // TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types - if (this.auto_load_slot_types) new base_class(base_class.title || "tmpnode") + if (this.auto_load_slot_types) new base_class(base_class.title || 'tmpnode') } /** @@ -402,9 +420,8 @@ export class LiteGraphGlobal { * @param type name of the node or the node constructor itself */ unregisterNodeType(type: string | typeof LGraphNode): void { - const base_class = typeof type === "string" - ? this.registered_node_types[type] - : type + const base_class = + typeof type === 'string' ? this.registered_node_types[type] : type if (!base_class) throw `node type not found: ${String(type)}` delete this.registered_node_types[String(base_class.type)] @@ -421,28 +438,30 @@ export class LiteGraphGlobal { registerNodeAndSlotType( type: LGraphNode, slot_type: ISlotType, - out?: boolean, + out?: boolean ): void { out ||= false - // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor. - const base_class = typeof type === "string" && this.registered_node_types[type] !== "anonymous" - ? this.registered_node_types[type] - : type + const base_class = + typeof type === 'string' && + // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor. + this.registered_node_types[type] !== 'anonymous' + ? this.registered_node_types[type] + : type // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor. const class_type = base_class.constructor.type let allTypes = [] - if (typeof slot_type === "string") { - allTypes = slot_type.split(",") + if (typeof slot_type === 'string') { + allTypes = slot_type.split(',') } else if (slot_type == this.EVENT || slot_type == this.ACTION) { - allTypes = ["_event_"] + allTypes = ['_event_'] } else { - allTypes = ["*"] + allTypes = ['*'] } for (let slotType of allTypes) { - if (slotType === "") slotType = "*" + if (slotType === '') slotType = '*' const register = out ? this.registered_slot_out_types @@ -453,9 +472,7 @@ export class LiteGraphGlobal { if (!nodes.includes(class_type)) nodes.push(class_type) // check if is a new type - const types = out - ? this.slot_types_out - : this.slot_types_in + const types = out ? this.slot_types_out : this.slot_types_in const type = slotType.toLowerCase() if (!types.includes(type)) { @@ -484,7 +501,7 @@ export class LiteGraphGlobal { createNode( type: string, title?: string, - options?: Dictionary, + options?: Dictionary ): LGraphNode | null { const base_class = this.registered_node_types[type] if (!base_class) { @@ -551,7 +568,7 @@ export class LiteGraphGlobal { const type = this.registered_node_types[i] if (type.filter != filter) continue - if (category == "") { + if (category == '') { if (type.category == null) r.push(type) } else if (type.category == category) { r.push(type) @@ -567,7 +584,7 @@ export class LiteGraphGlobal { * @returns array with all the names of the categories */ getNodeTypesCategories(filter?: string): string[] { - const categories: Dictionary = { "": 1 } + const categories: Dictionary = { '': 1 } for (const i in this.registered_node_types) { const type = this.registered_node_types[i] if (type.category && !type.skip_list) { @@ -585,14 +602,14 @@ export class LiteGraphGlobal { // debug purposes: reloads all the js scripts that matches a wildcard reloadNodes(folder_wildcard: string): void { - const tmp = document.getElementsByTagName("script") + const tmp = document.getElementsByTagName('script') // weird, this array changes by its own, so we use a copy const script_files = [] for (const element of tmp) { script_files.push(element) } - const docHeadObj = document.getElementsByTagName("head")[0] + const docHeadObj = document.getElementsByTagName('head')[0] folder_wildcard = document.location.href + folder_wildcard for (const script_file of script_files) { @@ -601,24 +618,27 @@ export class LiteGraphGlobal { continue try { - if (this.debug) console.log("Reloading:", src) - const dynamicScript = document.createElement("script") - dynamicScript.type = "text/javascript" + if (this.debug) console.log('Reloading:', src) + const dynamicScript = document.createElement('script') + dynamicScript.type = 'text/javascript' dynamicScript.src = src docHeadObj.append(dynamicScript) script_file.remove() } catch (error) { if (this.throw_errors) throw error - if (this.debug) console.log("Error while reloading", src) + if (this.debug) console.log('Error while reloading', src) } } - if (this.debug) console.log("Nodes reloaded") + if (this.debug) console.log('Nodes reloaded') } // separated just to improve if it doesn't work /** @deprecated Prefer {@link structuredClone} */ - cloneObject(obj: T, target?: T): WhenNullish { + cloneObject( + obj: T, + target?: T + ): WhenNullish { if (obj == null) return null as WhenNullish const r = JSON.parse(JSON.stringify(obj)) @@ -641,8 +661,8 @@ export class LiteGraphGlobal { * @returns true if they can be connected */ isValidConnection(type_a: ISlotType, type_b: ISlotType): boolean { - if (type_a == "" || type_a === "*") type_a = 0 - if (type_b == "" || type_b === "*") type_b = 0 + if (type_a == '' || type_a === '*') type_a = 0 + if (type_b == '' || type_b === '*') type_b = 0 // If generic in/output, matching types (valid for triggers), or event/action types if ( !type_a || @@ -660,16 +680,14 @@ export class LiteGraphGlobal { type_b = type_b.toLowerCase() // For nodes supporting multiple connection types - if (!type_a.includes(",") && !type_b.includes(",")) - return type_a == type_b + if (!type_a.includes(',') && !type_b.includes(',')) return type_a == type_b // Check all permutations to see if one is valid - const supported_types_a = type_a.split(",") - const supported_types_b = type_b.split(",") + const supported_types_a = type_a.split(',') + const supported_types_b = type_b.split(',') for (const a of supported_types_a) { for (const b of supported_types_b) { - if (this.isValidConnection(a, b)) - return true + if (this.isValidConnection(a, b)) return true } } @@ -679,105 +697,154 @@ export class LiteGraphGlobal { // used to create nodes from wrapping functions getParameterNames(func: (...args: any) => any): string[] { return String(func) - .replaceAll(/\/\/.*$/gm, "") // strip single-line comments - .replaceAll(/\s+/g, "") // strip white space - .replaceAll(/\/\*[^*/]*\*\//g, "") // strip multi-line comments /**/ - .split("){", 1)[0] - .replace(/^[^(]*\(/, "") // extract the parameters - .replaceAll(/=[^,]+/g, "") // strip any ES6 defaults - .split(",") + .replaceAll(/\/\/.*$/gm, '') // strip single-line comments + .replaceAll(/\s+/g, '') // strip white space + .replaceAll(/\/\*[^*/]*\*\//g, '') // strip multi-line comments /**/ + .split('){', 1)[0] + .replace(/^[^(]*\(/, '') // extract the parameters + .replaceAll(/=[^,]+/g, '') // strip any ES6 defaults + .split(',') .filter(Boolean) // split & filter [""] } /* helper for interaction: pointer, touch, mouse Listeners used by LGraphCanvas DragAndScale ContextMenu */ - pointerListenerAdd(oDOM: Node, sEvIn: string, fCall: (e: Event) => boolean | void, capture = false): void { - if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall !== "function") return + pointerListenerAdd( + oDOM: Node, + sEvIn: string, + fCall: (e: Event) => boolean | void, + capture = false + ): void { + if ( + !oDOM || + !oDOM.addEventListener || + !sEvIn || + typeof fCall !== 'function' + ) + return let sMethod = this.pointerevents_method let sEvent = sEvIn // UNDER CONSTRUCTION // convert pointerevents to touch event when not available - if (sMethod == "pointer" && !window.PointerEvent) { + if (sMethod == 'pointer' && !window.PointerEvent) { console.warn("sMethod=='pointer' && !window.PointerEvent") - console.log(`Converting pointer[${sEvent}] : down move up cancel enter TO touchstart touchmove touchend, etc ..`) + console.log( + `Converting pointer[${sEvent}] : down move up cancel enter TO touchstart touchmove touchend, etc ..` + ) switch (sEvent) { - case "down": { - sMethod = "touch" - sEvent = "start" - break - } - case "move": { - sMethod = "touch" - // sEvent = "move"; - break - } - case "up": { - sMethod = "touch" - sEvent = "end" - break - } - case "cancel": { - sMethod = "touch" - // sEvent = "cancel"; - break - } - case "enter": { - console.log("debug: Should I send a move event?") // ??? - break - } - // case "over": case "out": not used at now - default: { - console.warn(`PointerEvent not available in this browser ? The event ${sEvent} would not be called`) - } + case 'down': { + sMethod = 'touch' + sEvent = 'start' + break + } + case 'move': { + sMethod = 'touch' + // sEvent = "move"; + break + } + case 'up': { + sMethod = 'touch' + sEvent = 'end' + break + } + case 'cancel': { + sMethod = 'touch' + // sEvent = "cancel"; + break + } + case 'enter': { + console.log('debug: Should I send a move event?') // ??? + break + } + // case "over": case "out": not used at now + default: { + console.warn( + `PointerEvent not available in this browser ? The event ${sEvent} would not be called` + ) + } } } switch (sEvent) { - // @ts-expect-error - // both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - oDOM.addEventListener(sMethod + sEvent, fCall, capture) - } - // @ts-expect-error - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (sMethod != "mouse") { - return oDOM.addEventListener(sMethod + sEvent, fCall, capture) + // both pointer and move events + case 'down': + case 'up': + case 'move': + case 'over': + case 'out': + // @ts-expect-error - intentional fallthrough + case 'enter': { + oDOM.addEventListener(sMethod + sEvent, fCall, capture) } - } - // not "pointer" || "mouse" - default: - return oDOM.addEventListener(sEvent, fCall, capture) + // only pointerevents + case 'leave': + case 'cancel': + case 'gotpointercapture': + // @ts-expect-error - intentional fallthrough + case 'lostpointercapture': { + if (sMethod != 'mouse') { + return oDOM.addEventListener(sMethod + sEvent, fCall, capture) + } + } + // not "pointer" || "mouse" + default: + return oDOM.addEventListener(sEvent, fCall, capture) } } - pointerListenerRemove(oDOM: Node, sEvent: string, fCall: (e: Event) => boolean | void, capture = false): void { - if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall !== "function") return + pointerListenerRemove( + oDOM: Node, + sEvent: string, + fCall: (e: Event) => boolean | void, + capture = false + ): void { + if ( + !oDOM || + !oDOM.removeEventListener || + !sEvent || + typeof fCall !== 'function' + ) + return switch (sEvent) { - // @ts-expect-error - // both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - if (this.pointerevents_method == "pointer" || this.pointerevents_method == "mouse") { - oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture) + // both pointer and move events + case 'down': + case 'up': + case 'move': + case 'over': + case 'out': + // @ts-expect-error - intentional fallthrough + case 'enter': { + if ( + this.pointerevents_method == 'pointer' || + this.pointerevents_method == 'mouse' + ) { + oDOM.removeEventListener( + this.pointerevents_method + sEvent, + fCall, + capture + ) + } } - } - // @ts-expect-error - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (this.pointerevents_method == "pointer") { - return oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture) + // only pointerevents + case 'leave': + case 'cancel': + case 'gotpointercapture': + // @ts-expect-error - intentional fallthrough + case 'lostpointercapture': { + if (this.pointerevents_method == 'pointer') { + return oDOM.removeEventListener( + this.pointerevents_method + sEvent, + fCall, + capture + ) + } } - } - // not "pointer" || "mouse" - default: - return oDOM.removeEventListener(sEvent, fCall, capture) + // not "pointer" || "mouse" + default: + return oDOM.removeEventListener(sEvent, fCall, capture) } } @@ -788,17 +855,11 @@ export class LiteGraphGlobal { distance = distance colorToString(c: [number, number, number, number]): string { - return ( - `rgba(${ - Math.round(c[0] * 255).toFixed() - },${ - Math.round(c[1] * 255).toFixed() - },${ - Math.round(c[2] * 255).toFixed() - },${ - c.length == 4 ? c[3].toFixed(2) : "1.0" - })` - ) + return `rgba(${Math.round(c[0] * 255).toFixed()},${Math.round( + c[1] * 255 + ).toFixed()},${Math.round(c[2] * 255).toFixed()},${ + c.length == 4 ? c[3].toFixed(2) : '1.0' + })` } isInsideRectangle = isInsideRectangle @@ -837,12 +898,12 @@ export class LiteGraphGlobal { // format of a hex triplet - the kind we use for HTML colours. The function // will return an array with three values. hex2num(hex: string): number[] { - if (hex.charAt(0) == "#") { + if (hex.charAt(0) == '#') { hex = hex.slice(1) - // Remove the '#' char - if there is one. + // Remove the '#' char - if there is one. } hex = hex.toUpperCase() - const hex_alphabets = "0123456789ABCDEF" + const hex_alphabets = '0123456789ABCDEF' const value = new Array(3) let k = 0 let int1, int2 @@ -858,8 +919,8 @@ export class LiteGraphGlobal { // Give a array with three values as the argument and the function will return // the corresponding hex triplet. num2hex(triplet: number[]): string { - const hex_alphabets = "0123456789ABCDEF" - let hex = "#" + const hex_alphabets = '0123456789ABCDEF' + let hex = '#' let int1, int2 for (let i = 0; i < 3; i++) { int1 = triplet[i] / 16 @@ -871,11 +932,13 @@ export class LiteGraphGlobal { } closeAllContextMenus(ref_window: Window = window): void { - const elements = [...ref_window.document.querySelectorAll(".litecontextmenu")] + const elements = [ + ...ref_window.document.querySelectorAll('.litecontextmenu') + ] if (!elements.length) return for (const element of elements) { - if ("close" in element && typeof element.close === "function") { + if ('close' in element && typeof element.close === 'function') { element.close() } else { element.remove() @@ -903,7 +966,7 @@ export class LiteGraphGlobal { if (origin.prototype.__lookupGetter__(i)) { target.prototype.__defineGetter__( i, - origin.prototype.__lookupGetter__(i), + origin.prototype.__lookupGetter__(i) ) } else { target.prototype[i] = origin.prototype[i] @@ -913,7 +976,7 @@ export class LiteGraphGlobal { if (origin.prototype.__lookupSetter__(i)) { target.prototype.__defineSetter__( i, - origin.prototype.__lookupSetter__(i), + origin.prototype.__lookupSetter__(i) ) } } diff --git a/src/lib/litegraph/src/MapProxyHandler.ts b/src/lib/litegraph/src/MapProxyHandler.ts index 3935d8ce63..664ecb3d38 100644 --- a/src/lib/litegraph/src/MapProxyHandler.ts +++ b/src/lib/litegraph/src/MapProxyHandler.ts @@ -2,23 +2,25 @@ * Temporary workaround until downstream consumers migrate to Map. * A brittle wrapper with many flaws, but should be fine for simple maps using int indexes. */ -export class MapProxyHandler implements ProxyHandler> { +export class MapProxyHandler + implements ProxyHandler> +{ getOwnPropertyDescriptor( target: Map, - p: string | symbol, + p: string | symbol ): PropertyDescriptor | undefined { const value = this.get(target, p) if (value) { return { configurable: true, enumerable: true, - value, + value } } } has(target: Map, p: string | symbol): boolean { - if (typeof p === "symbol") return false + if (typeof p === 'symbol') return false const int = parseInt(p, 10) return target.has(!isNaN(int) ? int : p) @@ -31,14 +33,18 @@ export class MapProxyHandler implements ProxyHandler> get(target: Map, p: string | symbol): any { // Workaround does not support link IDs of "values", "entries", "constructor", etc. if (p in target) return Reflect.get(target, p, target) - if (typeof p === "symbol") return + if (typeof p === 'symbol') return const int = parseInt(p, 10) return target.get(!isNaN(int) ? int : p) } - set(target: Map, p: string | symbol, newValue: any): boolean { - if (typeof p === "symbol") return false + set( + target: Map, + p: string | symbol, + newValue: any + ): boolean { + if (typeof p === 'symbol') return false const int = parseInt(p, 10) target.set(!isNaN(int) ? int : p, newValue) diff --git a/src/lib/litegraph/src/Reroute.ts b/src/lib/litegraph/src/Reroute.ts index a6344056d0..886930227c 100644 --- a/src/lib/litegraph/src/Reroute.ts +++ b/src/lib/litegraph/src/Reroute.ts @@ -1,3 +1,6 @@ +import { LGraphBadge } from './LGraphBadge' +import type { LGraphNode, NodeId } from './LGraphNode' +import { LLink, type LinkId } from './LLink' import type { CanvasColour, INodeInputSlot, @@ -6,22 +9,18 @@ import type { LinkSegment, Point, Positionable, - ReadonlyLinkNetwork, ReadOnlyRect, -} from "./interfaces" -import type { LGraphNode, NodeId } from "./LGraphNode" -import type { Serialisable, SerialisableReroute } from "./types/serialisation" - -import { LGraphBadge } from "./LGraphBadge" -import { type LinkId, LLink } from "./LLink" -import { distance, isPointInRect } from "./measure" + ReadonlyLinkNetwork +} from './interfaces' +import { distance, isPointInRect } from './measure' +import type { Serialisable, SerialisableReroute } from './types/serialisation' export type RerouteId = number /** The input or output slot that an incomplete reroute link is connected to. */ export interface FloatingRerouteSlot { /** Floating connection to an input or output */ - slotType: "input" | "output" + slotType: 'input' | 'output' } /** @@ -31,7 +30,9 @@ export interface FloatingRerouteSlot { * Stores only primitive values (IDs) to reference other items in its network, * and a `WeakRef` to a {@link LinkNetwork} to resolve them. */ -export class Reroute implements Positionable, LinkSegment, Serialisable { +export class Reroute + implements Positionable, LinkSegment, Serialisable +{ static radius: number = 10 /** Maximum distance from reroutes to their bezier curve control points. */ static maxSplineOffset: number = 80 @@ -75,7 +76,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable= 2)) - throw new TypeError("Reroute.pos is an x,y point, and expects an indexable with at least two values.") + throw new TypeError( + 'Reroute.pos is an x,y point, and expects an indexable with at least two values.' + ) this.#pos[0] = value[0] this.#pos[1] = value[1] } @@ -135,7 +138,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable, - floatingLinkIds?: Iterable, + floatingLinkIds?: Iterable ) { this.#network = new WeakRef(network) this.parentId = parentId @@ -223,7 +220,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable, - floating?: FloatingRerouteSlot, + floating?: FloatingRerouteSlot ): void { this.parentId = parentId if (pos) this.pos = pos @@ -236,7 +233,10 @@ export class Reroute implements Positionable, LinkSegment, Serialisable, floatingLinks: ReadonlyMap): boolean { + validateLinks( + links: ReadonlyMap, + floatingLinks: ReadonlyMap + ): boolean { const { linkIds, floatingLinkIds } = this for (const linkId of linkIds) { if (!links.has(linkId)) linkIds.delete(linkId) @@ -282,7 +282,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable(), + visited = new Set() ): Reroute | null | undefined { if (this.#parentId === withParentId) return this if (visited.has(this)) return null @@ -291,8 +291,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable, - links: ReadonlyMap, + links: ReadonlyMap ) { for (const linkId of linkIds) { const link = links.get(linkId) @@ -355,11 +358,11 @@ export class Reroute implements Positionable, LinkSegment, Serialisable this.#lastRenderTime)) return this.#lastRenderTime = lastRenderTime @@ -484,11 +497,14 @@ export class Reroute implements Positionable, LinkSegment, Serialisable Math.PI * 0.5) diff += Math.PI - const dist = Math.min(Reroute.maxSplineOffset, distance(linkStart, this.#pos) * 0.25) + const dist = Math.min( + Reroute.maxSplineOffset, + distance(linkStart, this.#pos) * 0.25 + ) // Store results const originDiff = originToReroute - diff @@ -505,7 +521,10 @@ export class Reroute implements Positionable, LinkSegment, Serialisable, links: ReadonlyMap) { + function calculateAngles( + linkIds: Iterable, + links: ReadonlyMap + ) { for (const linkId of linkIds) { const link = links.get(linkId) const pos = getNextPos(network, link, id) @@ -532,26 +551,26 @@ export class Reroute implements Positionable, LinkSegment, Serialisable): void { + connectToInput( + node: LGraphNode, + input: INodeInputSlot, + _events?: CustomEventTarget + ): void { const floatingLink = this.link floatingLink.target_id = node.id floatingLink.target_slot = node.inputs.indexOf(input) @@ -129,7 +151,11 @@ export class FloatingRenderLink implements RenderLink { input._floatingLinks.add(floatingLink) } - connectToOutput(node: LGraphNode, output: INodeOutputSlot, _events?: CustomEventTarget): void { + connectToOutput( + node: LGraphNode, + output: INodeOutputSlot, + _events?: CustomEventTarget + ): void { const floatingLink = this.link floatingLink.origin_id = node.id floatingLink.origin_slot = node.outputs.indexOf(output) @@ -139,7 +165,10 @@ export class FloatingRenderLink implements RenderLink { output._floatingLinks.add(floatingLink) } - connectToSubgraphInput(input: SubgraphInput, _events?: CustomEventTarget): void { + connectToSubgraphInput( + input: SubgraphInput, + _events?: CustomEventTarget + ): void { const floatingLink = this.link floatingLink.origin_id = SUBGRAPH_INPUT_ID floatingLink.origin_slot = input.parent.slots.indexOf(input) @@ -149,7 +178,10 @@ export class FloatingRenderLink implements RenderLink { input._floatingLinks.add(floatingLink) } - connectToSubgraphOutput(output: SubgraphOutput, _events?: CustomEventTarget): void { + connectToSubgraphOutput( + output: SubgraphOutput, + _events?: CustomEventTarget + ): void { const floatingLink = this.link floatingLink.origin_id = SUBGRAPH_OUTPUT_ID floatingLink.origin_slot = output.parent.slots.indexOf(output) @@ -162,8 +194,8 @@ export class FloatingRenderLink implements RenderLink { connectToRerouteInput( // @ts-ignore TODO: Fix after migration to frontend tsconfig rules reroute: Reroute, - { node: inputNode, input }: { node: LGraphNode, input: INodeInputSlot }, - events: CustomEventTarget, + { node: inputNode, input }: { node: LGraphNode; input: INodeInputSlot }, + events: CustomEventTarget ) { const floatingLink = this.link floatingLink.target_id = inputNode.id @@ -173,7 +205,7 @@ export class FloatingRenderLink implements RenderLink { input._floatingLinks ??= new Set() input._floatingLinks.add(floatingLink) - events.dispatch("input-moved", this) + events.dispatch('input-moved', this) } connectToRerouteOutput( @@ -181,7 +213,7 @@ export class FloatingRenderLink implements RenderLink { reroute: Reroute, outputNode: LGraphNode, output: INodeOutputSlot, - events: CustomEventTarget, + events: CustomEventTarget ) { const floatingLink = this.link floatingLink.origin_id = outputNode.id @@ -191,6 +223,6 @@ export class FloatingRenderLink implements RenderLink { output._floatingLinks ??= new Set() output._floatingLinks.add(floatingLink) - events.dispatch("output-moved", this) + events.dispatch('output-moved', this) } } diff --git a/src/lib/litegraph/src/canvas/InputIndicators.ts b/src/lib/litegraph/src/canvas/InputIndicators.ts index 71be30265e..ba014a7f29 100644 --- a/src/lib/litegraph/src/canvas/InputIndicators.ts +++ b/src/lib/litegraph/src/canvas/InputIndicators.ts @@ -1,4 +1,4 @@ -import type { LGraphCanvas } from "@/lib/litegraph/src/LGraphCanvas" +import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' /** * A class that can be added to the render cycle to show pointer / keyboard status symbols. @@ -19,11 +19,11 @@ export class InputIndicators implements Disposable { startAngle = 0 endAngle = Math.PI * 2 - inactiveColour = "#ffffff10" - colour1 = "#ff5f00" - colour2 = "#00ff7c" - colour3 = "#dea7ff" - fontString = "bold 12px Arial" + inactiveColour = '#ffffff10' + colour1 = '#ff5f00' + colour2 = '#00ff7c' + colour3 = '#dea7ff' + fontString = 'bold 12px Arial' // #endregion // #region state @@ -51,14 +51,14 @@ 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", () => { + signal.addEventListener('abort', () => { canvas.drawFrontCanvas = origDrawFrontCanvas }) @@ -92,8 +92,8 @@ export class InputIndicators implements Disposable { this.ctrlDown = e.ctrlKey this.altDown = e.altKey this.shiftDown = e.shiftKey - this.undoDown = e.ctrlKey && e.code === "KeyZ" && e.type === "keydown" - this.redoDown = e.ctrlKey && e.code === "KeyY" && e.type === "keydown" + this.undoDown = e.ctrlKey && e.code === 'KeyZ' && e.type === 'keydown' + this.redoDown = e.ctrlKey && e.code === 'KeyY' && e.type === 'keydown' } draw() { @@ -108,7 +108,7 @@ export class InputIndicators implements Disposable { colour1, colour2, colour3, - fontString, + fontString } = this const { fillStyle, font } = ctx @@ -120,11 +120,26 @@ export class InputIndicators implements Disposable { const textY = mouseDotY - 15 ctx.font = fontString - textMarker(textX + 0, textY, "Shift", this.shiftDown ? colour1 : inactiveColour) - textMarker(textX + 45, textY + 20, "Alt", this.altDown ? colour2 : inactiveColour) - textMarker(textX + 30, textY, "Control", this.ctrlDown ? colour3 : inactiveColour) - textMarker(textX - 30, textY, "↩️", this.undoDown ? "#000" : "transparent") - textMarker(textX + 45, textY, "↪️", this.redoDown ? "#000" : "transparent") + textMarker( + textX + 0, + textY, + 'Shift', + this.shiftDown ? colour1 : inactiveColour + ) + textMarker( + textX + 45, + textY + 20, + 'Alt', + this.altDown ? colour2 : inactiveColour + ) + textMarker( + textX + 30, + textY, + 'Control', + this.ctrlDown ? colour3 : inactiveColour + ) + textMarker(textX - 30, textY, '↩️', this.undoDown ? '#000' : 'transparent') + textMarker(textX + 45, textY, '↪️', this.redoDown ? '#000' : 'transparent') ctx.beginPath() drawDot(mouseDotX, mouseDotY) @@ -137,8 +152,10 @@ export class InputIndicators implements Disposable { const middleButtonColour = this.mouse1Down ? colour2 : inactiveColour const rightButtonColour = this.mouse2Down ? colour3 : inactiveColour if (this.mouse0Down) mouseMarker(mouseDotX, mouseDotY, leftButtonColour) - if (this.mouse1Down) mouseMarker(mouseDotX + 15, mouseDotY, middleButtonColour) - if (this.mouse2Down) mouseMarker(mouseDotX + 30, mouseDotY, rightButtonColour) + if (this.mouse1Down) + mouseMarker(mouseDotX + 15, mouseDotY, middleButtonColour) + if (this.mouse2Down) + mouseMarker(mouseDotX + 30, mouseDotY, rightButtonColour) ctx.fillStyle = fillStyle ctx.font = font diff --git a/src/lib/litegraph/src/canvas/LinkConnector.ts b/src/lib/litegraph/src/canvas/LinkConnector.ts index 8a44a68f58..b42c12b2dc 100644 --- a/src/lib/litegraph/src/canvas/LinkConnector.ts +++ b/src/lib/litegraph/src/canvas/LinkConnector.ts @@ -1,31 +1,41 @@ -import type { RenderLink } from "./RenderLink" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { ConnectingLink, ItemLocator, LinkNetwork, LinkSegment } from "@/lib/litegraph/src/interfaces" -import type { INodeInputSlot, INodeOutputSlot } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" -import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events" -import type { IBaseWidget } from "@/lib/litegraph/src/types/widgets" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LLink } from '@/lib/litegraph/src/LLink' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import { + SUBGRAPH_INPUT_ID, + SUBGRAPH_OUTPUT_ID +} from '@/lib/litegraph/src/constants' +import { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { + ConnectingLink, + ItemLocator, + LinkNetwork, + LinkSegment +} from '@/lib/litegraph/src/interfaces' +import type { + INodeInputSlot, + INodeOutputSlot +} from '@/lib/litegraph/src/interfaces' +import { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import { SubgraphInputNode } from '@/lib/litegraph/src/subgraph/SubgraphInputNode' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import { SubgraphOutputNode } from '@/lib/litegraph/src/subgraph/SubgraphOutputNode' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' -import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from "@/lib/litegraph/src/constants" -import { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import { LLink } from "@/lib/litegraph/src/LLink" -import { Subgraph } from "@/lib/litegraph/src/subgraph/Subgraph" -import { SubgraphInputNode } from "@/lib/litegraph/src/subgraph/SubgraphInputNode" -import { SubgraphOutputNode } from "@/lib/litegraph/src/subgraph/SubgraphOutputNode" -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" - -import { FloatingRenderLink } from "./FloatingRenderLink" -import { MovingInputLink } from "./MovingInputLink" -import { MovingLinkBase } from "./MovingLinkBase" -import { MovingOutputLink } from "./MovingOutputLink" -import { ToInputFromIoNodeLink } from "./ToInputFromIoNodeLink" -import { ToInputRenderLink } from "./ToInputRenderLink" -import { ToOutputFromIoNodeLink } from "./ToOutputFromIoNodeLink" -import { ToOutputFromRerouteLink } from "./ToOutputFromRerouteLink" -import { ToOutputRenderLink } from "./ToOutputRenderLink" +import { FloatingRenderLink } from './FloatingRenderLink' +import { MovingInputLink } from './MovingInputLink' +import { MovingLinkBase } from './MovingLinkBase' +import { MovingOutputLink } from './MovingOutputLink' +import type { RenderLink } from './RenderLink' +import { ToInputFromIoNodeLink } from './ToInputFromIoNodeLink' +import { ToInputRenderLink } from './ToInputRenderLink' +import { ToOutputFromIoNodeLink } from './ToOutputFromIoNodeLink' +import { ToOutputFromRerouteLink } from './ToOutputFromRerouteLink' +import { ToOutputRenderLink } from './ToOutputRenderLink' /** * A Litegraph state object for the {@link LinkConnector}. @@ -38,7 +48,7 @@ export interface LinkConnectorState { * - When `undefined`, no operation is being performed. * - A change in this property indicates the start or end of dragging links. */ - connectingTo: "input" | "output" | undefined + connectingTo: 'input' | 'output' | undefined multi: boolean /** When `true`, existing links are being repositioned. Otherwise, new links are being created. */ draggingExistingLinks: boolean @@ -80,7 +90,7 @@ export class LinkConnector { connectingTo: undefined, multi: false, draggingExistingLinks: false, - snapLinksPos: undefined, + snapLinksPos: undefined } readonly events = new CustomEventTarget() @@ -121,7 +131,7 @@ export class LinkConnector { /** Drag an existing link to a different input. */ moveInputLink(network: LinkNetwork, input: INodeInputSlot): void { - if (this.isConnecting) throw new Error("Already dragging links.") + if (this.isConnecting) throw new Error('Already dragging links.') const { state, inputLinks, renderLinks } = this @@ -133,15 +143,30 @@ export class LinkConnector { try { const reroute = network.reroutes.get(floatingLink.parentId) - if (!reroute) throw new Error(`Invalid reroute id: [${floatingLink.parentId}] for floating link id: [${floatingLink.id}].`) + if (!reroute) + throw new Error( + `Invalid reroute id: [${floatingLink.parentId}] for floating link id: [${floatingLink.id}].` + ) - const renderLink = new FloatingRenderLink(network, floatingLink, "input", reroute) - const mayContinue = this.events.dispatch("before-move-input", renderLink) + const renderLink = new FloatingRenderLink( + network, + floatingLink, + 'input', + reroute + ) + const mayContinue = this.events.dispatch( + 'before-move-input', + renderLink + ) if (mayContinue === false) return renderLinks.push(renderLink) } catch (error) { - console.warn(`Could not create render link for link id: [${floatingLink.id}].`, floatingLink, error) + console.warn( + `Could not create render link for link id: [${floatingLink.id}].`, + floatingLink, + error + ) } floatingLink._dragging = true @@ -156,24 +181,37 @@ export class LinkConnector { // since they don't have a regular output node const subgraphInput = network.inputNode?.slots[link.origin_slot] if (!subgraphInput) { - console.warn(`Could not find subgraph input for slot [${link.origin_slot}]`) + console.warn( + `Could not find subgraph input for slot [${link.origin_slot}]` + ) return } try { const reroute = network.getReroute(link.parentId) - const renderLink = new ToInputFromIoNodeLink(network, network.inputNode, subgraphInput, reroute, LinkDirection.CENTER, link) + const renderLink = new ToInputFromIoNodeLink( + network, + network.inputNode, + subgraphInput, + reroute, + LinkDirection.CENTER, + link + ) // Note: We don't dispatch the before-move-input event for subgraph input links // as the event type doesn't support ToInputFromIoNodeLink renderLinks.push(renderLink) - this.listenUntilReset("input-moved", () => { - link.disconnect(network, "input") + this.listenUntilReset('input-moved', () => { + link.disconnect(network, 'input') }) } catch (error) { - console.warn(`Could not create render link for subgraph input link id: [${link.id}].`, link, error) + console.warn( + `Could not create render link for subgraph input link id: [${link.id}].`, + link, + error + ) return } @@ -185,18 +223,25 @@ export class LinkConnector { const reroute = network.getReroute(link.parentId) const renderLink = new MovingInputLink(network, link, reroute) - const mayContinue = this.events.dispatch("before-move-input", renderLink) + const mayContinue = this.events.dispatch( + 'before-move-input', + renderLink + ) if (mayContinue === false) return renderLinks.push(renderLink) - this.listenUntilReset("input-moved", (e) => { - if ("link" in e.detail && e.detail.link) { - e.detail.link.disconnect(network, "output") + this.listenUntilReset('input-moved', (e) => { + if ('link' in e.detail && e.detail.link) { + e.detail.link.disconnect(network, 'output') } }) } catch (error) { - console.warn(`Could not create render link for link id: [${link.id}].`, link, error) + console.warn( + `Could not create render link for link id: [${link.id}].`, + link, + error + ) return } @@ -205,7 +250,7 @@ export class LinkConnector { } } - state.connectingTo = "input" + state.connectingTo = 'input' state.draggingExistingLinks = true this.#setLegacyLinks(false) @@ -213,7 +258,7 @@ export class LinkConnector { /** Drag all links from an output to a new output. */ moveOutputLink(network: LinkNetwork, output: INodeOutputSlot): void { - if (this.isConnecting) throw new Error("Already dragging links.") + if (this.isConnecting) throw new Error('Already dragging links.') const { state, renderLinks } = this @@ -222,16 +267,31 @@ export class LinkConnector { for (const floatingLink of output._floatingLinks.values()) { try { const reroute = LLink.getFirstReroute(network, floatingLink) - if (!reroute) throw new Error(`Invalid reroute id: [${floatingLink.parentId}] for floating link id: [${floatingLink.id}].`) + if (!reroute) + throw new Error( + `Invalid reroute id: [${floatingLink.parentId}] for floating link id: [${floatingLink.id}].` + ) - const renderLink = new FloatingRenderLink(network, floatingLink, "output", reroute) - const mayContinue = this.events.dispatch("before-move-output", renderLink) + const renderLink = new FloatingRenderLink( + network, + floatingLink, + 'output', + reroute + ) + const mayContinue = this.events.dispatch( + 'before-move-output', + renderLink + ) if (mayContinue === false) continue renderLinks.push(renderLink) this.floatingLinks.push(floatingLink) } catch (error) { - console.warn(`Could not create render link for link id: [${floatingLink.id}].`, floatingLink, error) + console.warn( + `Could not create render link for link id: [${floatingLink.id}].`, + floatingLink, + error + ) } } } @@ -252,14 +312,26 @@ export class LinkConnector { this.outputLinks.push(link) try { - const renderLink = new MovingOutputLink(network, link, firstReroute, LinkDirection.RIGHT) + const renderLink = new MovingOutputLink( + network, + link, + firstReroute, + LinkDirection.RIGHT + ) - const mayContinue = this.events.dispatch("before-move-output", renderLink) + const mayContinue = this.events.dispatch( + 'before-move-output', + renderLink + ) if (mayContinue === false) continue renderLinks.push(renderLink) } catch (error) { - console.warn(`Could not create render link for link id: [${link.id}].`, link, error) + console.warn( + `Could not create render link for link id: [${link.id}].`, + link, + error + ) continue } } @@ -269,7 +341,7 @@ export class LinkConnector { state.draggingExistingLinks = true state.multi = true - state.connectingTo = "output" + state.connectingTo = 'output' this.#setLegacyLinks(true) } @@ -280,14 +352,19 @@ export class LinkConnector { * @param node The node the link is being dragged from * @param output The output slot that the link is being dragged from */ - dragNewFromOutput(network: LinkNetwork, node: LGraphNode, output: INodeOutputSlot, fromReroute?: Reroute): void { - if (this.isConnecting) throw new Error("Already dragging links.") + dragNewFromOutput( + network: LinkNetwork, + node: LGraphNode, + output: INodeOutputSlot, + fromReroute?: Reroute + ): void { + if (this.isConnecting) throw new Error('Already dragging links.') const { state } = this const renderLink = new ToInputRenderLink(network, node, output, fromReroute) this.renderLinks.push(renderLink) - state.connectingTo = "input" + state.connectingTo = 'input' this.#setLegacyLinks(false) } @@ -298,36 +375,61 @@ export class LinkConnector { * @param node The node the link is being dragged from * @param input The input slot that the link is being dragged from */ - dragNewFromInput(network: LinkNetwork, node: LGraphNode, input: INodeInputSlot, fromReroute?: Reroute): void { - if (this.isConnecting) throw new Error("Already dragging links.") + dragNewFromInput( + network: LinkNetwork, + node: LGraphNode, + input: INodeInputSlot, + fromReroute?: Reroute + ): void { + if (this.isConnecting) throw new Error('Already dragging links.') const { state } = this const renderLink = new ToOutputRenderLink(network, node, input, fromReroute) this.renderLinks.push(renderLink) - state.connectingTo = "output" + state.connectingTo = 'output' this.#setLegacyLinks(true) } - dragNewFromSubgraphInput(network: LinkNetwork, inputNode: SubgraphInputNode, input: SubgraphInput, fromReroute?: Reroute): void { - if (this.isConnecting) throw new Error("Already dragging links.") + dragNewFromSubgraphInput( + network: LinkNetwork, + inputNode: SubgraphInputNode, + input: SubgraphInput, + fromReroute?: Reroute + ): void { + if (this.isConnecting) throw new Error('Already dragging links.') - const renderLink = new ToInputFromIoNodeLink(network, inputNode, input, fromReroute) + const renderLink = new ToInputFromIoNodeLink( + network, + inputNode, + input, + fromReroute + ) this.renderLinks.push(renderLink) - this.state.connectingTo = "input" + this.state.connectingTo = 'input' this.#setLegacyLinks(false) } - dragNewFromSubgraphOutput(network: LinkNetwork, outputNode: SubgraphOutputNode, output: SubgraphOutput, fromReroute?: Reroute): void { - if (this.isConnecting) throw new Error("Already dragging links.") + dragNewFromSubgraphOutput( + network: LinkNetwork, + outputNode: SubgraphOutputNode, + output: SubgraphOutput, + fromReroute?: Reroute + ): void { + if (this.isConnecting) throw new Error('Already dragging links.') - const renderLink = new ToOutputFromIoNodeLink(network, outputNode, output, fromReroute) + const renderLink = new ToOutputFromIoNodeLink( + network, + outputNode, + output, + fromReroute + ) this.renderLinks.push(renderLink) - this.state.connectingTo = "output" + this.state.connectingTo = 'output' this.#setLegacyLinks(true) } @@ -338,28 +440,33 @@ export class LinkConnector { * @param reroute The reroute that the link is being dragged from */ dragFromReroute(network: LinkNetwork, reroute: Reroute): void { - if (this.isConnecting) throw new Error("Already dragging links.") + if (this.isConnecting) throw new Error('Already dragging links.') const link = reroute.firstLink ?? reroute.firstFloatingLink if (!link) { - console.warn("No link found for reroute.") + console.warn('No link found for reroute.') return } if (link.origin_id === SUBGRAPH_INPUT_ID) { if (!(network instanceof Subgraph)) { - console.warn("Subgraph input link found in non-subgraph network.") + console.warn('Subgraph input link found in non-subgraph network.') return } const input = network.inputs.at(link.origin_slot) - if (!input) throw new Error("No subgraph input found for link.") + if (!input) throw new Error('No subgraph input found for link.') - const renderLink = new ToInputFromIoNodeLink(network, network.inputNode, input, reroute) + const renderLink = new ToInputFromIoNodeLink( + network, + network.inputNode, + input, + reroute + ) renderLink.fromDirection = LinkDirection.NONE this.renderLinks.push(renderLink) - this.state.connectingTo = "input" + this.state.connectingTo = 'input' this.#setLegacyLinks(false) return @@ -367,21 +474,26 @@ export class LinkConnector { const outputNode = network.getNodeById(link.origin_id) if (!outputNode) { - console.warn("No output node found for link.", link) + console.warn('No output node found for link.', link) return } const outputSlot = outputNode.outputs.at(link.origin_slot) if (!outputSlot) { - console.warn("No output slot found for link.", link) + console.warn('No output slot found for link.', link) return } - const renderLink = new ToInputRenderLink(network, outputNode, outputSlot, reroute) + const renderLink = new ToInputRenderLink( + network, + outputNode, + outputSlot, + reroute + ) renderLink.fromDirection = LinkDirection.NONE this.renderLinks.push(renderLink) - this.state.connectingTo = "input" + this.state.connectingTo = 'input' this.#setLegacyLinks(false) } @@ -392,28 +504,33 @@ export class LinkConnector { * @param reroute The reroute that the link is being dragged from */ dragFromRerouteToOutput(network: LinkNetwork, reroute: Reroute): void { - if (this.isConnecting) throw new Error("Already dragging links.") + if (this.isConnecting) throw new Error('Already dragging links.') const link = reroute.firstLink ?? reroute.firstFloatingLink if (!link) { - console.warn("No link found for reroute.") + console.warn('No link found for reroute.') return } if (link.target_id === SUBGRAPH_OUTPUT_ID) { if (!(network instanceof Subgraph)) { - console.warn("Subgraph output link found in non-subgraph network.") + console.warn('Subgraph output link found in non-subgraph network.') return } const output = network.outputs.at(link.target_slot) - if (!output) throw new Error("No subgraph output found for link.") + if (!output) throw new Error('No subgraph output found for link.') - const renderLink = new ToOutputFromIoNodeLink(network, network.outputNode, output, reroute) + const renderLink = new ToOutputFromIoNodeLink( + network, + network.outputNode, + output, + reroute + ) renderLink.fromDirection = LinkDirection.NONE this.renderLinks.push(renderLink) - this.state.connectingTo = "output" + this.state.connectingTo = 'output' this.#setLegacyLinks(false) return @@ -421,27 +538,33 @@ export class LinkConnector { const inputNode = network.getNodeById(link.target_id) if (!inputNode) { - console.warn("No input node found for link.", link) + console.warn('No input node found for link.', link) return } const inputSlot = inputNode.inputs.at(link.target_slot) if (!inputSlot) { - console.warn("No input slot found for link.", link) + console.warn('No input slot found for link.', link) return } - const renderLink = new ToOutputFromRerouteLink(network, inputNode, inputSlot, reroute, this) + const renderLink = new ToOutputFromRerouteLink( + network, + inputNode, + inputSlot, + reroute, + this + ) renderLink.fromDirection = LinkDirection.LEFT this.renderLinks.push(renderLink) - this.state.connectingTo = "output" + this.state.connectingTo = 'output' this.#setLegacyLinks(true) } dragFromLinkSegment(network: LinkNetwork, linkSegment: LinkSegment): void { - if (this.isConnecting) throw new Error("Already dragging links.") + if (this.isConnecting) throw new Error('Already dragging links.') const { state } = this if (linkSegment.origin_id == null || linkSegment.origin_slot == null) return @@ -457,7 +580,7 @@ export class LinkConnector { renderLink.fromDirection = LinkDirection.NONE this.renderLinks.push(renderLink) - state.connectingTo = "input" + state.connectingTo = 'input' this.#setLegacyLinks(false) } @@ -468,7 +591,10 @@ export class LinkConnector { */ dropLinks(locator: ItemLocator, event: CanvasPointerEvent): void { if (!this.isConnecting) { - const mayContinue = this.events.dispatch("before-drop-links", { renderLinks: this.renderLinks, event }) + const mayContinue = this.events.dispatch('before-drop-links', { + renderLinks: this.renderLinks, + event + }) if (mayContinue === false) return } @@ -495,31 +621,44 @@ export class LinkConnector { } } } finally { - this.events.dispatch("after-drop-links", { renderLinks: this.renderLinks, event }) + this.events.dispatch('after-drop-links', { + renderLinks: this.renderLinks, + event + }) } } - dropOnIoNode(ioNode: SubgraphInputNode | SubgraphOutputNode, event: CanvasPointerEvent): void { + dropOnIoNode( + ioNode: SubgraphInputNode | SubgraphOutputNode, + event: CanvasPointerEvent + ): void { const { renderLinks, state } = this const { connectingTo } = state const { canvasX, canvasY } = event - if (connectingTo === "input" && ioNode instanceof SubgraphOutputNode) { + if (connectingTo === 'input' && ioNode instanceof SubgraphOutputNode) { const output = ioNode.getSlotInPosition(canvasX, canvasY) - if (!output) throw new Error("No output slot found for link.") + if (!output) throw new Error('No output slot found for link.') for (const link of renderLinks) { link.connectToSubgraphOutput(output, this.events) } - } else if (connectingTo === "output" && ioNode instanceof SubgraphInputNode) { + } else if ( + connectingTo === 'output' && + ioNode instanceof SubgraphInputNode + ) { const input = ioNode.getSlotInPosition(canvasX, canvasY) - if (!input) throw new Error("No input slot found for link.") + if (!input) throw new Error('No input slot found for link.') for (const link of renderLinks) { link.connectToSubgraphInput(input, this.events) } } else { - console.error("Invalid connectingTo state &/ ioNode", connectingTo, ioNode) + console.error( + 'Invalid connectingTo state &/ ioNode', + connectingTo, + ioNode + ) } } @@ -529,10 +668,10 @@ export class LinkConnector { const { canvasX, canvasY } = event // Do nothing if every connection would loop back - if (renderLinks.every(link => link.node === node)) return + if (renderLinks.every((link) => link.node === node)) return // To output - if (connectingTo === "output") { + if (connectingTo === 'output') { const output = node.getOutputOnPos([canvasX, canvasY]) if (output) { @@ -540,8 +679,8 @@ export class LinkConnector { } else { this.connectToNode(node, event) } - // To input - } else if (connectingTo === "input") { + // To input + } else if (connectingTo === 'input') { const input = node.getInputOnPos([canvasX, canvasY]) const inputOrSocket = input ?? node.getSlotFromWidget(this.overWidget) @@ -556,12 +695,18 @@ export class LinkConnector { } dropOnReroute(reroute: Reroute, event: CanvasPointerEvent): void { - const mayContinue = this.events.dispatch("dropped-on-reroute", { reroute, event }) + const mayContinue = this.events.dispatch('dropped-on-reroute', { + reroute, + event + }) if (mayContinue === false) return // Connecting to input - if (this.state.connectingTo === "input") { - if (this.renderLinks.length !== 1) throw new Error(`Attempted to connect ${this.renderLinks.length} input links to a reroute.`) + if (this.state.connectingTo === 'input') { + if (this.renderLinks.length !== 1) + throw new Error( + `Attempted to connect ${this.renderLinks.length} input links to a reroute.` + ) const renderLink = this.renderLinks[0] this._connectOutputToReroute(reroute, renderLink) @@ -571,7 +716,7 @@ export class LinkConnector { // Connecting to output for (const link of this.renderLinks) { - if (link.toType !== "output") continue + if (link.toType !== 'output') continue const result = reroute.findSourceOutput() if (!result) continue @@ -589,7 +734,7 @@ export class LinkConnector { if (!results?.length) return const maybeReroutes = reroute.getReroutes() - if (maybeReroutes === null) throw new Error("Reroute loop detected.") + if (maybeReroutes === null) throw new Error('Reroute loop detected.') const originalReroutes = maybeReroutes.slice(0, -1).reverse() @@ -612,10 +757,24 @@ export class LinkConnector { } // Filter before any connections are re-created - const filtered = results.filter(result => renderLink.toType === "input" && canConnectInputLinkToReroute(renderLink, result.node, result.input, reroute)) + const filtered = results.filter( + (result) => + renderLink.toType === 'input' && + canConnectInputLinkToReroute( + renderLink, + result.node, + result.input, + reroute + ) + ) for (const result of filtered) { - renderLink.connectToRerouteInput(reroute, result, this.events, originalReroutes) + renderLink.connectToRerouteInput( + reroute, + result, + this.events, + originalReroutes + ) } return @@ -623,7 +782,7 @@ export class LinkConnector { dropOnNothing(event: CanvasPointerEvent): void { // For external event only. - const mayContinue = this.events.dispatch("dropped-on-canvas", event) + const mayContinue = this.events.dispatch('dropped-on-canvas', event) if (mayContinue === false) return this.disconnectLinks() @@ -648,9 +807,11 @@ export class LinkConnector { * @param event Contains the drop location, in canvas space */ connectToNode(node: LGraphNode, event: CanvasPointerEvent): void { - const { state: { connectingTo } } = this + const { + state: { connectingTo } + } = this - const mayContinue = this.events.dispatch("dropped-on-node", { node, event }) + const mayContinue = this.events.dispatch('dropped-on-node', { node, event }) if (mayContinue === false) return // Assume all links are the same type, disallow loopback @@ -658,22 +819,26 @@ export class LinkConnector { if (!firstLink) return // Use a single type check before looping; ensures all dropped links go to the same slot - if (connectingTo === "output") { + if (connectingTo === 'output') { // Dropping new output link const output = node.findOutputByType(firstLink.fromSlot.type)?.slot - console.debug("out", node, output, firstLink.fromSlot) + console.debug('out', node, output, firstLink.fromSlot) if (output === undefined) { - console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`) + console.warn( + `Could not find slot for link type: [${firstLink.fromSlot.type}].` + ) return } this.#dropOnOutput(node, output) - } else if (connectingTo === "input") { + } else if (connectingTo === 'input') { // Dropping new input link const input = node.findInputByType(firstLink.fromSlot.type)?.slot - console.debug("in", node, input, firstLink.fromSlot) + console.debug('in', node, input, firstLink.fromSlot) if (input === undefined) { - console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`) + console.warn( + `Could not find slot for link type: [${firstLink.fromSlot.type}].` + ) return } @@ -692,9 +857,17 @@ export class LinkConnector { #dropOnOutput(node: LGraphNode, output: INodeOutputSlot): void { for (const link of this.renderLinks) { if (!link.canConnectToOutput(node, output)) { - if (link instanceof MovingOutputLink && link.link.parentId !== undefined) { + if ( + link instanceof MovingOutputLink && + link.link.parentId !== undefined + ) { // Reconnect link without reroutes - link.outputNode.connectSlots(link.outputSlot, link.inputNode, link.inputSlot, undefined!) + link.outputNode.connectSlots( + link.outputSlot, + link.inputNode, + link.inputSlot, + undefined! + ) } continue } @@ -704,15 +877,19 @@ export class LinkConnector { } isInputValidDrop(node: LGraphNode, input: INodeInputSlot): boolean { - return this.renderLinks.some(link => link.canConnectToInput(node, input)) + return this.renderLinks.some((link) => link.canConnectToInput(node, input)) } isNodeValidDrop(node: LGraphNode): boolean { - if (this.state.connectingTo === "output") { - return node.outputs.some(output => this.renderLinks.some(link => link.canConnectToOutput(node, output))) + if (this.state.connectingTo === 'output') { + return node.outputs.some((output) => + this.renderLinks.some((link) => link.canConnectToOutput(node, output)) + ) } - return node.inputs.some(input => this.renderLinks.some(link => link.canConnectToInput(node, input))) + return node.inputs.some((input) => + this.renderLinks.some((link) => link.canConnectToInput(node, input)) + ) } /** @@ -721,14 +898,15 @@ export class LinkConnector { * @returns `true` if any of the current links being connected are valid for the given reroute. */ isRerouteValidDrop(reroute: Reroute): boolean { - if (this.state.connectingTo === "input") { + if (this.state.connectingTo === 'input') { const results = reroute.findTargetInputs() if (!results?.length) return false for (const { node, input } of results) { for (const renderLink of this.renderLinks) { - if (renderLink.toType !== "input") continue - if (canConnectInputLinkToReroute(renderLink, node, input, reroute)) return true + if (renderLink.toType !== 'input') continue + if (canConnectInputLinkToReroute(renderLink, node, input, reroute)) + return true } } } else { @@ -738,7 +916,7 @@ export class LinkConnector { const { node, output } = result for (const renderLink of this.renderLinks) { - if (renderLink.toType !== "output") continue + if (renderLink.toType !== 'output') continue if (!renderLink.canConnectToReroute(reroute)) continue if (renderLink.canConnectToOutput(node, output)) return true } @@ -750,10 +928,13 @@ export class LinkConnector { /** Sets connecting_links, used by some extensions still. */ #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 + const input = fromSlotIsInput ? (link.fromSlot as INodeInputSlot) : null + const output = fromSlotIsInput ? null : (link.fromSlot as INodeOutputSlot) - const afterRerouteId = link instanceof MovingLinkBase ? link.link?.parentId : link.fromReroute?.id + const afterRerouteId = + link instanceof MovingLinkBase + ? link.link?.parentId + : link.fromReroute?.id return { node: link.node as LGraphNode, @@ -761,7 +942,7 @@ export class LinkConnector { input, output, pos: link.fromPos, - afterRerouteId, + afterRerouteId } satisfies ConnectingLink }) this.#setConnectingLinks(links) @@ -780,7 +961,7 @@ export class LinkConnector { outputLinks: [...this.outputLinks], floatingLinks: [...this.floatingLinks], state: { ...this.state }, - network, + network } } @@ -792,10 +973,14 @@ export class LinkConnector { listenUntilReset( eventName: K, listener: Parameters>[1], - options?: Parameters>[2], + options?: Parameters>[2] ) { this.events.addEventListener(eventName, listener, options) - this.events.addEventListener("reset", () => this.events.removeEventListener(eventName, listener), { once: true }) + this.events.addEventListener( + 'reset', + () => this.events.removeEventListener(eventName, listener), + { once: true } + ) } /** @@ -804,10 +989,17 @@ export class LinkConnector { * Effectively cancels moving or connecting links. */ reset(force = false): void { - const mayContinue = this.events.dispatch("reset", force) + const mayContinue = this.events.dispatch('reset', force) if (mayContinue === false) return - const { state, outputLinks, inputLinks, hiddenReroutes, renderLinks, floatingLinks } = this + const { + state, + outputLinks, + inputLinks, + hiddenReroutes, + renderLinks, + floatingLinks + } = this if (!force && state.connectingTo === undefined) return state.connectingTo = undefined @@ -830,10 +1022,14 @@ export class LinkConnector { /** Validates that a single {@link RenderLink} can be dropped on the specified reroute. */ function canConnectInputLinkToReroute( - link: ToInputRenderLink | MovingInputLink | FloatingRenderLink | ToInputFromIoNodeLink, + link: + | ToInputRenderLink + | MovingInputLink + | FloatingRenderLink + | ToInputFromIoNodeLink, inputNode: LGraphNode, input: INodeInputSlot, - reroute: Reroute, + reroute: Reroute ): boolean { const { fromReroute } = link @@ -851,7 +1047,8 @@ function canConnectInputLinkToReroute( if (link instanceof ToInputRenderLink) { if (reroute.parentId == null) { // Link would make no change - output to reroute - if (reroute.firstLink?.hasOrigin(link.node.id, link.fromSlotIndex)) return false + if (reroute.firstLink?.hasOrigin(link.node.id, link.fromSlotIndex)) + return false } else if (link.fromReroute?.id === reroute.parentId) { return false } diff --git a/src/lib/litegraph/src/canvas/MovingInputLink.ts b/src/lib/litegraph/src/canvas/MovingInputLink.ts index 9294acb9f9..fb094222cf 100644 --- a/src/lib/litegraph/src/canvas/MovingInputLink.ts +++ b/src/lib/litegraph/src/canvas/MovingInputLink.ts @@ -1,19 +1,23 @@ -import type { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { INodeInputSlot, INodeOutputSlot, LinkNetwork, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { LLink } from "@/lib/litegraph/src/LLink" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" -import type { NodeLike } from "@/lib/litegraph/src/types/NodeLike" -import type { SubgraphIO } from "@/lib/litegraph/src/types/serialisation" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LLink } from '@/lib/litegraph/src/LLink' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { + INodeInputSlot, + INodeOutputSlot, + LinkNetwork, + Point +} from '@/lib/litegraph/src/interfaces' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation' -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" - -import { MovingLinkBase } from "./MovingLinkBase" +import { MovingLinkBase } from './MovingLinkBase' export class MovingInputLink extends MovingLinkBase { - override readonly toType = "input" + override readonly toType = 'input' readonly node: LGraphNode readonly fromSlot: INodeOutputSlot @@ -21,8 +25,13 @@ export class MovingInputLink extends MovingLinkBase { readonly fromDirection: LinkDirection readonly fromSlotIndex: number - constructor(network: LinkNetwork, link: LLink, fromReroute?: Reroute, dragDirection: LinkDirection = LinkDirection.CENTER) { - super(network, link, "input", fromReroute, dragDirection) + constructor( + network: LinkNetwork, + link: LLink, + fromReroute?: Reroute, + dragDirection: LinkDirection = LinkDirection.CENTER + ) { + super(network, link, 'input', fromReroute, dragDirection) this.node = this.outputNode this.fromSlot = this.outputSlot @@ -31,7 +40,10 @@ export class MovingInputLink extends MovingLinkBase { this.fromSlotIndex = this.outputIndex } - canConnectToInput(inputNode: NodeLike, input: INodeInputSlot | SubgraphIO): boolean { + canConnectToInput( + inputNode: NodeLike, + input: INodeInputSlot | SubgraphIO + ): boolean { return this.node.canConnectTo(inputNode, input, this.outputSlot) } @@ -43,33 +55,53 @@ export class MovingInputLink extends MovingLinkBase { return reroute.origin_id !== this.inputNode.id } - connectToInput(inputNode: LGraphNode, input: INodeInputSlot, events: CustomEventTarget): LLink | null | undefined { + connectToInput( + inputNode: LGraphNode, + input: INodeInputSlot, + events: CustomEventTarget + ): LLink | null | undefined { if (input === this.inputSlot) return this.inputNode.disconnectInput(this.inputIndex, true) - const link = this.outputNode.connectSlots(this.outputSlot, inputNode, input, this.fromReroute?.id) - if (link) events.dispatch("input-moved", this) + const link = this.outputNode.connectSlots( + this.outputSlot, + inputNode, + input, + this.fromReroute?.id + ) + if (link) events.dispatch('input-moved', this) return link } connectToOutput(): never { - throw new Error("MovingInputLink cannot connect to an output.") + throw new Error('MovingInputLink cannot connect to an output.') } connectToSubgraphInput(): void { - throw new Error("MovingInputLink cannot connect to a subgraph input.") + throw new Error('MovingInputLink cannot connect to a subgraph input.') } - connectToSubgraphOutput(output: SubgraphOutput, events?: CustomEventTarget): void { - const newLink = output.connect(this.fromSlot, this.node, this.fromReroute?.id) - events?.dispatch("link-created", newLink) + connectToSubgraphOutput( + output: SubgraphOutput, + events?: CustomEventTarget + ): void { + const newLink = output.connect( + this.fromSlot, + this.node, + this.fromReroute?.id + ) + events?.dispatch('link-created', newLink) } connectToRerouteInput( reroute: Reroute, - { node: inputNode, input, link: existingLink }: { node: LGraphNode, input: INodeInputSlot, link: LLink }, + { + node: inputNode, + input, + link: existingLink + }: { node: LGraphNode; input: INodeInputSlot; link: LLink }, events: CustomEventTarget, - originalReroutes: Reroute[], + originalReroutes: Reroute[] ): void { const { outputNode, outputSlot, fromReroute } = this @@ -82,12 +114,17 @@ export class MovingInputLink extends MovingLinkBase { // Set the parentId of the reroute we dropped on, to the reroute we dragged from reroute.parentId = fromReroute?.id - const newLink = outputNode.connectSlots(outputSlot, inputNode, input, existingLink.parentId) - if (newLink) events.dispatch("input-moved", this) + const newLink = outputNode.connectSlots( + outputSlot, + inputNode, + input, + existingLink.parentId + ) + if (newLink) events.dispatch('input-moved', this) } connectToRerouteOutput(): never { - throw new Error("MovingInputLink cannot connect to an output.") + throw new Error('MovingInputLink cannot connect to an output.') } disconnect(): boolean { diff --git a/src/lib/litegraph/src/canvas/MovingLinkBase.ts b/src/lib/litegraph/src/canvas/MovingLinkBase.ts index e974faf70b..b06c238b84 100644 --- a/src/lib/litegraph/src/canvas/MovingLinkBase.ts +++ b/src/lib/litegraph/src/canvas/MovingLinkBase.ts @@ -1,14 +1,19 @@ -import type { RenderLink } from "./RenderLink" -import type { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { INodeInputSlot, INodeOutputSlot, LinkNetwork, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode, NodeId } from "@/lib/litegraph/src/LGraphNode" -import type { LLink } from "@/lib/litegraph/src/LLink" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" +import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode' +import type { LLink } from '@/lib/litegraph/src/LLink' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { + INodeInputSlot, + INodeOutputSlot, + LinkNetwork, + Point +} from '@/lib/litegraph/src/interfaces' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" +import type { RenderLink } from './RenderLink' /** * Represents an existing link that is currently being dragged by the user from one slot to another. @@ -44,23 +49,29 @@ export abstract class MovingLinkBase implements RenderLink { constructor( readonly network: LinkNetwork, readonly link: LLink, - readonly toType: "input" | "output", + readonly toType: 'input' | 'output', readonly fromReroute?: Reroute, - readonly dragDirection: LinkDirection = LinkDirection.CENTER, + readonly dragDirection: LinkDirection = LinkDirection.CENTER ) { const { origin_id: outputNodeId, target_id: inputNodeId, origin_slot: outputIndex, - target_slot: inputIndex, + target_slot: inputIndex } = link // Store output info const outputNode = network.getNodeById(outputNodeId) ?? undefined - if (!outputNode) throw new Error(`Creating MovingRenderLink for link [${link.id}] failed: Output node [${outputNodeId}] not found.`) + if (!outputNode) + throw new Error( + `Creating MovingRenderLink for link [${link.id}] failed: Output node [${outputNodeId}] not found.` + ) const outputSlot = outputNode.outputs.at(outputIndex) - if (!outputSlot) throw new Error(`Creating MovingRenderLink for link [${link.id}] failed: Output slot [${outputIndex}] not found.`) + if (!outputSlot) + throw new Error( + `Creating MovingRenderLink for link [${link.id}] failed: Output slot [${outputIndex}] not found.` + ) this.outputNodeId = outputNodeId this.outputNode = outputNode @@ -70,10 +81,16 @@ export abstract class MovingLinkBase implements RenderLink { // Store input info const inputNode = network.getNodeById(inputNodeId) ?? undefined - if (!inputNode) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Input node [${inputNodeId}] not found.`) + if (!inputNode) + throw new Error( + `Creating DraggingRenderLink for link [${link.id}] failed: Input node [${inputNodeId}] not found.` + ) const inputSlot = inputNode.inputs.at(inputIndex) - if (!inputSlot) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Input slot [${inputIndex}] not found.`) + if (!inputSlot) + throw new Error( + `Creating DraggingRenderLink for link [${link.id}] failed: Input slot [${inputIndex}] not found.` + ) this.inputNodeId = inputNodeId this.inputNode = inputNode @@ -82,12 +99,40 @@ export abstract class MovingLinkBase implements RenderLink { this.inputPos = inputNode.getInputPos(inputIndex) } - abstract connectToInput(node: LGraphNode, input: INodeInputSlot, events?: CustomEventTarget): void - abstract connectToOutput(node: LGraphNode, output: INodeOutputSlot, events?: CustomEventTarget): void - abstract connectToSubgraphInput(input: SubgraphInput, events?: CustomEventTarget): void - abstract connectToSubgraphOutput(output: SubgraphOutput, events?: CustomEventTarget): void - abstract connectToRerouteInput(reroute: Reroute, { node, input, link }: { node: LGraphNode, input: INodeInputSlot, link: LLink }, events: CustomEventTarget, originalReroutes: Reroute[]): void - abstract connectToRerouteOutput(reroute: Reroute, outputNode: LGraphNode, output: INodeOutputSlot, events: CustomEventTarget): void + abstract connectToInput( + node: LGraphNode, + input: INodeInputSlot, + events?: CustomEventTarget + ): void + abstract connectToOutput( + node: LGraphNode, + output: INodeOutputSlot, + events?: CustomEventTarget + ): void + abstract connectToSubgraphInput( + input: SubgraphInput, + events?: CustomEventTarget + ): void + abstract connectToSubgraphOutput( + output: SubgraphOutput, + events?: CustomEventTarget + ): void + abstract connectToRerouteInput( + reroute: Reroute, + { + node, + input, + link + }: { node: LGraphNode; input: INodeInputSlot; link: LLink }, + events: CustomEventTarget, + originalReroutes: Reroute[] + ): void + abstract connectToRerouteOutput( + reroute: Reroute, + outputNode: LGraphNode, + output: INodeOutputSlot, + events: CustomEventTarget + ): void abstract disconnect(): boolean } diff --git a/src/lib/litegraph/src/canvas/MovingOutputLink.ts b/src/lib/litegraph/src/canvas/MovingOutputLink.ts index 5839042a9e..c8b05a8c3b 100644 --- a/src/lib/litegraph/src/canvas/MovingOutputLink.ts +++ b/src/lib/litegraph/src/canvas/MovingOutputLink.ts @@ -1,19 +1,23 @@ -import type { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { INodeInputSlot, INodeOutputSlot, LinkNetwork, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { LLink } from "@/lib/litegraph/src/LLink" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { NodeLike } from "@/lib/litegraph/src/types/NodeLike" -import type { SubgraphIO } from "@/lib/litegraph/src/types/serialisation" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LLink } from '@/lib/litegraph/src/LLink' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { + INodeInputSlot, + INodeOutputSlot, + LinkNetwork, + Point +} from '@/lib/litegraph/src/interfaces' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation' -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" - -import { MovingLinkBase } from "./MovingLinkBase" +import { MovingLinkBase } from './MovingLinkBase' export class MovingOutputLink extends MovingLinkBase { - override readonly toType = "output" + override readonly toType = 'output' readonly node: LGraphNode readonly fromSlot: INodeInputSlot @@ -21,8 +25,13 @@ export class MovingOutputLink extends MovingLinkBase { readonly fromDirection: LinkDirection readonly fromSlotIndex: number - constructor(network: LinkNetwork, link: LLink, fromReroute?: Reroute, dragDirection: LinkDirection = LinkDirection.CENTER) { - super(network, link, "output", fromReroute, dragDirection) + constructor( + network: LinkNetwork, + link: LLink, + fromReroute?: Reroute, + dragDirection: LinkDirection = LinkDirection.CENTER + ) { + super(network, link, 'output', fromReroute, dragDirection) this.node = this.inputNode this.fromSlot = this.inputSlot @@ -35,7 +44,10 @@ export class MovingOutputLink extends MovingLinkBase { return false } - canConnectToOutput(outputNode: NodeLike, output: INodeOutputSlot | SubgraphIO): boolean { + canConnectToOutput( + outputNode: NodeLike, + output: INodeOutputSlot | SubgraphIO + ): boolean { return outputNode.canConnectTo(this.node, this.inputSlot, output) } @@ -44,41 +56,57 @@ export class MovingOutputLink extends MovingLinkBase { } connectToInput(): never { - throw new Error("MovingOutputLink cannot connect to an input.") + throw new Error('MovingOutputLink cannot connect to an input.') } - connectToOutput(outputNode: LGraphNode, output: INodeOutputSlot, events: CustomEventTarget): LLink | null | undefined { + connectToOutput( + outputNode: LGraphNode, + output: INodeOutputSlot, + events: CustomEventTarget + ): LLink | null | undefined { if (output === this.outputSlot) return - const link = outputNode.connectSlots(output, this.inputNode, this.inputSlot, this.link.parentId) - if (link) events.dispatch("output-moved", this) + const link = outputNode.connectSlots( + output, + this.inputNode, + this.inputSlot, + this.link.parentId + ) + if (link) events.dispatch('output-moved', this) return link } - connectToSubgraphInput(input: SubgraphInput, events?: CustomEventTarget): void { - const newLink = input.connect(this.fromSlot, this.node, this.fromReroute?.id) - events?.dispatch("link-created", newLink) + connectToSubgraphInput( + input: SubgraphInput, + events?: CustomEventTarget + ): void { + const newLink = input.connect( + this.fromSlot, + this.node, + this.fromReroute?.id + ) + events?.dispatch('link-created', newLink) } connectToSubgraphOutput(): void { - throw new Error("MovingOutputLink cannot connect to a subgraph output.") + throw new Error('MovingOutputLink cannot connect to a subgraph output.') } connectToRerouteInput(): never { - throw new Error("MovingOutputLink cannot connect to an input.") + throw new Error('MovingOutputLink cannot connect to an input.') } connectToRerouteOutput( reroute: Reroute, outputNode: LGraphNode, output: INodeOutputSlot, - events: CustomEventTarget, + events: CustomEventTarget ): void { // Moving output side of links const { inputNode, inputSlot, fromReroute } = this // Creating a new link removes floating prop - check before connecting - const floatingTerminus = reroute?.floating?.slotType === "output" + const floatingTerminus = reroute?.floating?.slotType === 'output' // Connect the first reroute of the link being dragged to the reroute being dropped on if (fromReroute) { @@ -93,7 +121,7 @@ export class MovingOutputLink extends MovingLinkBase { // Connecting from the final reroute of a floating reroute chain if (floatingTerminus) reroute.removeAllFloatingLinks() - events.dispatch("output-moved", this) + events.dispatch('output-moved', this) } disconnect(): boolean { diff --git a/src/lib/litegraph/src/canvas/RenderLink.ts b/src/lib/litegraph/src/canvas/RenderLink.ts index 2d6d0ef357..88a8d3abf2 100644 --- a/src/lib/litegraph/src/canvas/RenderLink.ts +++ b/src/lib/litegraph/src/canvas/RenderLink.ts @@ -1,16 +1,21 @@ -import type { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { LinkNetwork, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { INodeInputSlot, INodeOutputSlot, LLink, Reroute } from "@/lib/litegraph/src/litegraph" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { SubgraphIONodeBase } from "@/lib/litegraph/src/subgraph/SubgraphIONodeBase" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" -import type { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { LinkNetwork, Point } from '@/lib/litegraph/src/interfaces' +import type { + INodeInputSlot, + INodeOutputSlot, + LLink, + Reroute +} from '@/lib/litegraph/src/litegraph' +import type { SubgraphIONodeBase } from '@/lib/litegraph/src/subgraph/SubgraphIONodeBase' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' export interface RenderLink { /** The type of link being connected. */ - readonly toType: "input" | "output" + readonly toType: 'input' | 'output' /** The source {@link Point} of the link being connected. */ readonly fromPos: Point /** The direction the link starts off as. If {@link toType} is `output`, this will be the direction the link input faces. */ @@ -23,28 +28,50 @@ export interface RenderLink { /** The node that the link is being connected from. */ readonly node: LGraphNode | SubgraphIONodeBase /** The slot that the link is being connected from. */ - readonly fromSlot: INodeOutputSlot | INodeInputSlot | SubgraphInput | SubgraphOutput + readonly fromSlot: + | INodeOutputSlot + | INodeInputSlot + | SubgraphInput + | SubgraphOutput /** The index of the slot that the link is being connected from. */ readonly fromSlotIndex: number /** The reroute that the link is being connected from. */ readonly fromReroute?: Reroute - connectToInput(node: LGraphNode, input: INodeInputSlot, events?: CustomEventTarget): void - connectToOutput(node: LGraphNode, output: INodeOutputSlot, events?: CustomEventTarget): void - connectToSubgraphInput(input: SubgraphInput, events?: CustomEventTarget): void - connectToSubgraphOutput(output: SubgraphOutput, events?: CustomEventTarget): void + connectToInput( + node: LGraphNode, + input: INodeInputSlot, + events?: CustomEventTarget + ): void + connectToOutput( + node: LGraphNode, + output: INodeOutputSlot, + events?: CustomEventTarget + ): void + connectToSubgraphInput( + input: SubgraphInput, + events?: CustomEventTarget + ): void + connectToSubgraphOutput( + output: SubgraphOutput, + events?: CustomEventTarget + ): void connectToRerouteInput( reroute: Reroute, - { node, input, link }: { node: LGraphNode, input: INodeInputSlot, link: LLink }, + { + node, + input, + link + }: { node: LGraphNode; input: INodeInputSlot; link: LLink }, events: CustomEventTarget, - originalReroutes: Reroute[], + originalReroutes: Reroute[] ): void connectToRerouteOutput( reroute: Reroute, outputNode: LGraphNode, output: INodeOutputSlot, - events: CustomEventTarget, + events: CustomEventTarget ): void } diff --git a/src/lib/litegraph/src/canvas/ToInputFromIoNodeLink.ts b/src/lib/litegraph/src/canvas/ToInputFromIoNodeLink.ts index e74f0e7ec5..14dc5b05c3 100644 --- a/src/lib/litegraph/src/canvas/ToInputFromIoNodeLink.ts +++ b/src/lib/litegraph/src/canvas/ToInputFromIoNodeLink.ts @@ -1,20 +1,24 @@ -import type { RenderLink } from "./RenderLink" -import type { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { INodeInputSlot, LinkNetwork, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { LLink } from "@/lib/litegraph/src/LLink" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { SubgraphInputNode } from "@/lib/litegraph/src/subgraph/SubgraphInputNode" -import type { NodeLike } from "@/lib/litegraph/src/types/NodeLike" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LLink } from '@/lib/litegraph/src/LLink' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { + INodeInputSlot, + LinkNetwork, + Point +} from '@/lib/litegraph/src/interfaces' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { SubgraphInputNode } from '@/lib/litegraph/src/subgraph/SubgraphInputNode' +import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" +import type { RenderLink } from './RenderLink' /** Connecting TO an input slot. */ export class ToInputFromIoNodeLink implements RenderLink { - readonly toType = "input" + readonly toType = 'input' readonly fromSlotIndex: number readonly fromPos: Point fromDirection: LinkDirection = LinkDirection.RIGHT @@ -26,17 +30,17 @@ export class ToInputFromIoNodeLink implements RenderLink { readonly fromSlot: SubgraphInput, readonly fromReroute?: Reroute, public dragDirection: LinkDirection = LinkDirection.CENTER, - existingLink?: LLink, + existingLink?: LLink ) { const outputIndex = node.slots.indexOf(fromSlot) if (outputIndex === -1 && fromSlot !== node.emptySlot) { - throw new Error(`Creating render link for node [${this.node.id}] failed: Slot index not found.`) + throw new Error( + `Creating render link for node [${this.node.id}] failed: Slot index not found.` + ) } this.fromSlotIndex = outputIndex - this.fromPos = fromReroute - ? fromReroute.pos - : fromSlot.pos + this.fromPos = fromReroute ? fromReroute.pos : fromSlot.pos this.existingLink = existingLink } @@ -48,22 +52,26 @@ export class ToInputFromIoNodeLink implements RenderLink { return false } - connectToInput(node: LGraphNode, input: INodeInputSlot, events: CustomEventTarget) { + connectToInput( + node: LGraphNode, + input: INodeInputSlot, + events: CustomEventTarget + ) { const { fromSlot, fromReroute, existingLink } = this const newLink = fromSlot.connect(input, node, fromReroute?.id) if (existingLink) { // Moving an existing link - events.dispatch("input-moved", this) + events.dispatch('input-moved', this) } else { // Creating a new link - events.dispatch("link-created", newLink) + events.dispatch('link-created', newLink) } } connectToSubgraphOutput(): void { - throw new Error("Not implemented") + throw new Error('Not implemented') } connectToRerouteInput( @@ -71,15 +79,15 @@ export class ToInputFromIoNodeLink implements RenderLink { { node: inputNode, input, - link, - }: { node: LGraphNode, input: INodeInputSlot, link: LLink }, + link + }: { node: LGraphNode; input: INodeInputSlot; link: LLink }, events: CustomEventTarget, - originalReroutes: Reroute[], + originalReroutes: Reroute[] ) { const { fromSlot, fromReroute } = this // Check before creating new link overwrites the value - const floatingTerminus = fromReroute?.floating?.slotType === "output" + const floatingTerminus = fromReroute?.floating?.slotType === 'output' // Set the parentId of the reroute we dropped on, to the reroute we dragged from reroute.parentId = fromReroute?.id @@ -100,31 +108,31 @@ export class ToInputFromIoNodeLink implements RenderLink { reroute.remove() } else { // Convert to floating - const cl = link.toFloating("output", reroute.id) + const cl = link.toFloating('output', reroute.id) this.network.addFloatingLink(cl) - reroute.floating = { slotType: "output" } + reroute.floating = { slotType: 'output' } } } } if (this.existingLink) { // Moving an existing link - events.dispatch("input-moved", this) + events.dispatch('input-moved', this) } else { // Creating a new link - events.dispatch("link-created", newLink) + events.dispatch('link-created', newLink) } } connectToOutput() { - throw new Error("ToInputRenderLink cannot connect to an output.") + throw new Error('ToInputRenderLink cannot connect to an output.') } connectToSubgraphInput(): void { - throw new Error("ToInputRenderLink cannot connect to a subgraph input.") + throw new Error('ToInputRenderLink cannot connect to a subgraph input.') } connectToRerouteOutput() { - throw new Error("ToInputRenderLink cannot connect to an output.") + throw new Error('ToInputRenderLink cannot connect to an output.') } } diff --git a/src/lib/litegraph/src/canvas/ToInputRenderLink.ts b/src/lib/litegraph/src/canvas/ToInputRenderLink.ts index e4292c7a86..61d517ae5d 100644 --- a/src/lib/litegraph/src/canvas/ToInputRenderLink.ts +++ b/src/lib/litegraph/src/canvas/ToInputRenderLink.ts @@ -1,19 +1,24 @@ -import type { RenderLink } from "./RenderLink" -import type { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { INodeInputSlot, INodeOutputSlot, LinkNetwork, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { LLink } from "@/lib/litegraph/src/LLink" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" -import type { NodeLike } from "@/lib/litegraph/src/types/NodeLike" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LLink } from '@/lib/litegraph/src/LLink' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { + INodeInputSlot, + INodeOutputSlot, + LinkNetwork, + Point +} from '@/lib/litegraph/src/interfaces' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" +import type { RenderLink } from './RenderLink' /** Connecting TO an input slot. */ export class ToInputRenderLink implements RenderLink { - readonly toType = "input" + readonly toType = 'input' readonly fromPos: Point readonly fromSlotIndex: number fromDirection: LinkDirection = LinkDirection.RIGHT @@ -23,10 +28,13 @@ export class ToInputRenderLink implements RenderLink { readonly node: LGraphNode, readonly fromSlot: INodeOutputSlot, readonly fromReroute?: Reroute, - public dragDirection: LinkDirection = LinkDirection.CENTER, + public dragDirection: LinkDirection = LinkDirection.CENTER ) { const outputIndex = node.outputs.indexOf(fromSlot) - if (outputIndex === -1) throw new Error(`Creating render link for node [${this.node.id}] failed: Slot index not found.`) + if (outputIndex === -1) + throw new Error( + `Creating render link for node [${this.node.id}] failed: Slot index not found.` + ) this.fromSlotIndex = outputIndex this.fromPos = fromReroute @@ -42,17 +50,33 @@ export class ToInputRenderLink implements RenderLink { return false } - connectToInput(node: LGraphNode, input: INodeInputSlot, events: CustomEventTarget) { + connectToInput( + node: LGraphNode, + input: INodeInputSlot, + events: CustomEventTarget + ) { const { node: outputNode, fromSlot, fromReroute } = this if (node === outputNode) return - const newLink = outputNode.connectSlots(fromSlot, node, input, fromReroute?.id) - events.dispatch("link-created", newLink) + const newLink = outputNode.connectSlots( + fromSlot, + node, + input, + fromReroute?.id + ) + events.dispatch('link-created', newLink) } - connectToSubgraphOutput(output: SubgraphOutput, events: CustomEventTarget) { - const newLink = output.connect(this.fromSlot, this.node, this.fromReroute?.id) - events.dispatch("link-created", newLink) + connectToSubgraphOutput( + output: SubgraphOutput, + events: CustomEventTarget + ) { + const newLink = output.connect( + this.fromSlot, + this.node, + this.fromReroute?.id + ) + events.dispatch('link-created', newLink) } connectToRerouteInput( @@ -60,20 +84,25 @@ export class ToInputRenderLink implements RenderLink { { node: inputNode, input, - link, - }: { node: LGraphNode, input: INodeInputSlot, link: LLink }, + link + }: { node: LGraphNode; input: INodeInputSlot; link: LLink }, events: CustomEventTarget, - originalReroutes: Reroute[], + originalReroutes: Reroute[] ) { const { node: outputNode, fromSlot, fromReroute } = this // Check before creating new link overwrites the value - const floatingTerminus = fromReroute?.floating?.slotType === "output" + const floatingTerminus = fromReroute?.floating?.slotType === 'output' // Set the parentId of the reroute we dropped on, to the reroute we dragged from reroute.parentId = fromReroute?.id - const newLink = outputNode.connectSlots(fromSlot, inputNode, input, link.parentId) + const newLink = outputNode.connectSlots( + fromSlot, + inputNode, + input, + link.parentId + ) // Connecting from the final reroute of a floating reroute chain if (floatingTerminus) fromReroute.removeAllFloatingLinks() @@ -89,24 +118,24 @@ export class ToInputRenderLink implements RenderLink { reroute.remove() } else { // Convert to floating - const cl = link.toFloating("output", reroute.id) + const cl = link.toFloating('output', reroute.id) this.network.addFloatingLink(cl) - reroute.floating = { slotType: "output" } + reroute.floating = { slotType: 'output' } } } } - events.dispatch("link-created", newLink) + events.dispatch('link-created', newLink) } connectToOutput() { - throw new Error("ToInputRenderLink cannot connect to an output.") + throw new Error('ToInputRenderLink cannot connect to an output.') } connectToSubgraphInput(): void { - throw new Error("ToInputRenderLink cannot connect to a subgraph input.") + throw new Error('ToInputRenderLink cannot connect to a subgraph input.') } connectToRerouteOutput() { - throw new Error("ToInputRenderLink cannot connect to an output.") + throw new Error('ToInputRenderLink cannot connect to an output.') } } diff --git a/src/lib/litegraph/src/canvas/ToOutputFromIoNodeLink.ts b/src/lib/litegraph/src/canvas/ToOutputFromIoNodeLink.ts index 77eadb01c6..d700801186 100644 --- a/src/lib/litegraph/src/canvas/ToOutputFromIoNodeLink.ts +++ b/src/lib/litegraph/src/canvas/ToOutputFromIoNodeLink.ts @@ -1,20 +1,24 @@ -import type { RenderLink } from "./RenderLink" -import type { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { INodeOutputSlot, LinkNetwork, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" -import type { SubgraphOutputNode } from "@/lib/litegraph/src/subgraph/SubgraphOutputNode" -import type { NodeLike } from "@/lib/litegraph/src/types/NodeLike" -import type { SubgraphIO } from "@/lib/litegraph/src/types/serialisation" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { + INodeOutputSlot, + LinkNetwork, + Point +} from '@/lib/litegraph/src/interfaces' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import type { SubgraphOutputNode } from '@/lib/litegraph/src/subgraph/SubgraphOutputNode' +import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation' -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" +import type { RenderLink } from './RenderLink' /** Connecting TO an output slot. */ export class ToOutputFromIoNodeLink implements RenderLink { - readonly toType = "output" + readonly toType = 'output' readonly fromPos: Point readonly fromSlotIndex: number fromDirection: LinkDirection = LinkDirection.LEFT @@ -24,24 +28,27 @@ export class ToOutputFromIoNodeLink implements RenderLink { readonly node: SubgraphOutputNode, readonly fromSlot: SubgraphOutput, readonly fromReroute?: Reroute, - public dragDirection: LinkDirection = LinkDirection.CENTER, + public dragDirection: LinkDirection = LinkDirection.CENTER ) { const inputIndex = node.slots.indexOf(fromSlot) if (inputIndex === -1 && fromSlot !== node.emptySlot) { - throw new Error(`Creating render link for node [${this.node.id}] failed: Slot index not found.`) + throw new Error( + `Creating render link for node [${this.node.id}] failed: Slot index not found.` + ) } this.fromSlotIndex = inputIndex - this.fromPos = fromReroute - ? fromReroute.pos - : fromSlot.pos + this.fromPos = fromReroute ? fromReroute.pos : fromSlot.pos } canConnectToInput(): false { return false } - canConnectToOutput(outputNode: NodeLike, output: INodeOutputSlot | SubgraphIO): boolean { + canConnectToOutput( + outputNode: NodeLike, + output: INodeOutputSlot | SubgraphIO + ): boolean { return this.node.canConnectTo(outputNode, this.fromSlot, output) } @@ -50,38 +57,42 @@ export class ToOutputFromIoNodeLink implements RenderLink { return true } - connectToOutput(node: LGraphNode, output: INodeOutputSlot, events: CustomEventTarget) { + connectToOutput( + node: LGraphNode, + output: INodeOutputSlot, + events: CustomEventTarget + ) { const { fromSlot, fromReroute } = this const newLink = fromSlot.connect(output, node, fromReroute?.id) - events.dispatch("link-created", newLink) + events.dispatch('link-created', newLink) } connectToSubgraphInput(): void { - throw new Error("Not implemented") + throw new Error('Not implemented') } connectToRerouteOutput( reroute: Reroute, outputNode: LGraphNode, output: INodeOutputSlot, - events: CustomEventTarget, + events: CustomEventTarget ): void { const { fromSlot } = this const newLink = fromSlot.connect(output, outputNode, reroute?.id) - events.dispatch("link-created", newLink) + events.dispatch('link-created', newLink) } connectToInput() { - throw new Error("ToOutputRenderLink cannot connect to an input.") + throw new Error('ToOutputRenderLink cannot connect to an input.') } connectToSubgraphOutput(): void { - throw new Error("ToOutputRenderLink cannot connect to a subgraph output.") + throw new Error('ToOutputRenderLink cannot connect to a subgraph output.') } connectToRerouteInput() { - throw new Error("ToOutputRenderLink cannot connect to an input.") + throw new Error('ToOutputRenderLink cannot connect to an input.') } } diff --git a/src/lib/litegraph/src/canvas/ToOutputFromRerouteLink.ts b/src/lib/litegraph/src/canvas/ToOutputFromRerouteLink.ts index 704dc2fa71..e0cbb02f29 100644 --- a/src/lib/litegraph/src/canvas/ToOutputFromRerouteLink.ts +++ b/src/lib/litegraph/src/canvas/ToOutputFromRerouteLink.ts @@ -1,10 +1,14 @@ -import type { LinkConnector } from "./LinkConnector" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { INodeInputSlot, INodeOutputSlot, LinkNetwork } from "@/lib/litegraph/src/litegraph" -import type { Reroute } from "@/lib/litegraph/src/Reroute" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { + INodeInputSlot, + INodeOutputSlot, + LinkNetwork +} from '@/lib/litegraph/src/litegraph' -import { ToInputRenderLink } from "./ToInputRenderLink" -import { ToOutputRenderLink } from "./ToOutputRenderLink" +import type { LinkConnector } from './LinkConnector' +import { ToInputRenderLink } from './ToInputRenderLink' +import { ToOutputRenderLink } from './ToOutputRenderLink' /** * @internal A workaround class to support connecting to reroutes to node outputs. @@ -15,7 +19,7 @@ export class ToOutputFromRerouteLink extends ToOutputRenderLink { node: LGraphNode, fromSlot: INodeInputSlot, override readonly fromReroute: Reroute, - readonly linkConnector: LinkConnector, + readonly linkConnector: LinkConnector ) { super(network, node, fromSlot, fromReroute) } diff --git a/src/lib/litegraph/src/canvas/ToOutputRenderLink.ts b/src/lib/litegraph/src/canvas/ToOutputRenderLink.ts index 41c806076a..aac98eda02 100644 --- a/src/lib/litegraph/src/canvas/ToOutputRenderLink.ts +++ b/src/lib/litegraph/src/canvas/ToOutputRenderLink.ts @@ -1,19 +1,24 @@ -import type { RenderLink } from "./RenderLink" -import type { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget" -import type { LinkConnectorEventMap } from "@/lib/litegraph/src/infrastructure/LinkConnectorEventMap" -import type { INodeInputSlot, INodeOutputSlot, LinkNetwork, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { NodeLike } from "@/lib/litegraph/src/types/NodeLike" -import type { SubgraphIO } from "@/lib/litegraph/src/types/serialisation" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget' +import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap' +import type { + INodeInputSlot, + INodeOutputSlot, + LinkNetwork, + Point +} from '@/lib/litegraph/src/interfaces' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation' -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" +import type { RenderLink } from './RenderLink' /** Connecting TO an output slot. */ export class ToOutputRenderLink implements RenderLink { - readonly toType = "output" + readonly toType = 'output' readonly fromPos: Point readonly fromSlotIndex: number fromDirection: LinkDirection = LinkDirection.LEFT @@ -23,10 +28,13 @@ export class ToOutputRenderLink implements RenderLink { readonly node: LGraphNode, readonly fromSlot: INodeInputSlot, readonly fromReroute?: Reroute, - public dragDirection: LinkDirection = LinkDirection.CENTER, + public dragDirection: LinkDirection = LinkDirection.CENTER ) { const inputIndex = node.inputs.indexOf(fromSlot) - if (inputIndex === -1) throw new Error(`Creating render link for node [${this.node.id}] failed: Slot index not found.`) + if (inputIndex === -1) + throw new Error( + `Creating render link for node [${this.node.id}] failed: Slot index not found.` + ) this.fromSlotIndex = inputIndex this.fromPos = fromReroute @@ -38,7 +46,10 @@ export class ToOutputRenderLink implements RenderLink { return false } - canConnectToOutput(outputNode: NodeLike, output: INodeOutputSlot | SubgraphIO): boolean { + canConnectToOutput( + outputNode: NodeLike, + output: INodeOutputSlot | SubgraphIO + ): boolean { return this.node.canConnectTo(outputNode, this.fromSlot, output) } @@ -47,39 +58,60 @@ export class ToOutputRenderLink implements RenderLink { return true } - connectToOutput(node: LGraphNode, output: INodeOutputSlot, events: CustomEventTarget) { + connectToOutput( + node: LGraphNode, + output: INodeOutputSlot, + events: CustomEventTarget + ) { const { node: inputNode, fromSlot, fromReroute } = this if (!inputNode) return - const newLink = node.connectSlots(output, inputNode, fromSlot, fromReroute?.id) - events.dispatch("link-created", newLink) + const newLink = node.connectSlots( + output, + inputNode, + fromSlot, + fromReroute?.id + ) + events.dispatch('link-created', newLink) } - connectToSubgraphInput(input: SubgraphInput, events?: CustomEventTarget): void { - const newLink = input.connect(this.fromSlot, this.node, this.fromReroute?.id) - events?.dispatch("link-created", newLink) + connectToSubgraphInput( + input: SubgraphInput, + events?: CustomEventTarget + ): void { + const newLink = input.connect( + this.fromSlot, + this.node, + this.fromReroute?.id + ) + events?.dispatch('link-created', newLink) } connectToRerouteOutput( reroute: Reroute, outputNode: LGraphNode, output: INodeOutputSlot, - events: CustomEventTarget, + events: CustomEventTarget ): void { const { node: inputNode, fromSlot } = this - const newLink = outputNode.connectSlots(output, inputNode, fromSlot, reroute?.id) - events.dispatch("link-created", newLink) + const newLink = outputNode.connectSlots( + output, + inputNode, + fromSlot, + reroute?.id + ) + events.dispatch('link-created', newLink) } connectToInput() { - throw new Error("ToOutputRenderLink cannot connect to an input.") + throw new Error('ToOutputRenderLink cannot connect to an input.') } connectToSubgraphOutput(): void { - throw new Error("ToOutputRenderLink cannot connect to a subgraph output.") + throw new Error('ToOutputRenderLink cannot connect to a subgraph output.') } connectToRerouteInput() { - throw new Error("ToOutputRenderLink cannot connect to an input.") + throw new Error('ToOutputRenderLink cannot connect to an input.') } } diff --git a/src/lib/litegraph/src/canvas/measureSlots.ts b/src/lib/litegraph/src/canvas/measureSlots.ts index f3554608e4..561ac556a6 100644 --- a/src/lib/litegraph/src/canvas/measureSlots.ts +++ b/src/lib/litegraph/src/canvas/measureSlots.ts @@ -1,9 +1,16 @@ -import type { INodeInputSlot, INodeOutputSlot, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { + INodeInputSlot, + INodeOutputSlot, + Point +} from '@/lib/litegraph/src/interfaces' +import { isInRectangle } from '@/lib/litegraph/src/measure' -import { isInRectangle } from "@/lib/litegraph/src/measure" - -export function getNodeInputOnPos(node: LGraphNode, x: number, y: number): { index: number, input: INodeInputSlot, pos: Point } | undefined { +export function getNodeInputOnPos( + node: LGraphNode, + x: number, + y: number +): { index: number; input: INodeInputSlot; pos: Point } | undefined { const { inputs } = node if (!inputs) return @@ -12,37 +19,28 @@ export function getNodeInputOnPos(node: LGraphNode, x: number, y: number): { ind // TODO: Find a cheap way to measure text, and do it on node label change instead of here // Input icon width + text approximation - const nameLength = input.label?.length ?? input.localized_name?.length ?? input.name?.length + const nameLength = + input.label?.length ?? input.localized_name?.length ?? input.name?.length const width = 20 + (nameLength || 3) * 7 - if (isInRectangle( - x, - y, - pos[0] - 10, - pos[1] - 10, - width, - 20, - )) { + if (isInRectangle(x, y, pos[0] - 10, pos[1] - 10, width, 20)) { return { index, input, pos } } } } -export function getNodeOutputOnPos(node: LGraphNode, x: number, y: number): { index: number, output: INodeOutputSlot, pos: Point } | undefined { +export function getNodeOutputOnPos( + node: LGraphNode, + x: number, + y: number +): { index: number; output: INodeOutputSlot; pos: Point } | undefined { const { outputs } = node if (!outputs) return for (const [index, output] of outputs.entries()) { const pos = node.getOutputPos(index) - if (isInRectangle( - x, - y, - pos[0] - 10, - pos[1] - 10, - 40, - 20, - )) { + if (isInRectangle(x, y, pos[0] - 10, pos[1] - 10, 40, 20)) { return { index, output, pos } } } @@ -56,7 +54,7 @@ export function isOverNodeInput( node: LGraphNode, canvasx: number, canvasy: number, - slot_pos?: Point, + slot_pos?: Point ): number { const result = getNodeInputOnPos(node, canvasx, canvasy) if (!result) return -1 @@ -76,7 +74,7 @@ export function isOverNodeOutput( node: LGraphNode, canvasx: number, canvasy: number, - slot_pos?: Point, + slot_pos?: Point ): number { const result = getNodeOutputOnPos(node, canvasx, canvasy) if (!result) return -1 diff --git a/src/lib/litegraph/src/draw.ts b/src/lib/litegraph/src/draw.ts index f798100918..756c60db1b 100644 --- a/src/lib/litegraph/src/draw.ts +++ b/src/lib/litegraph/src/draw.ts @@ -1,16 +1,15 @@ -import type { Rectangle } from "./infrastructure/Rectangle" -import type { CanvasColour, Rect } from "./interfaces" +import type { Rectangle } from './infrastructure/Rectangle' +import type { CanvasColour, Rect } from './interfaces' +import { LiteGraph } from './litegraph' +import { LinkDirection, RenderShape, TitleMode } from './types/globalEnums' -import { LiteGraph } from "./litegraph" -import { LinkDirection, RenderShape, TitleMode } from "./types/globalEnums" - -const ELLIPSIS = "\u2026" -const TWO_DOT_LEADER = "\u2025" -const ONE_DOT_LEADER = "\u2024" +const ELLIPSIS = '\u2026' +const TWO_DOT_LEADER = '\u2025' +const ONE_DOT_LEADER = '\u2024' export enum SlotType { - Array = "array", - Event = -1, + Array = 'array', + Event = -1 } /** @see RenderShape */ @@ -19,7 +18,7 @@ export enum SlotShape { Arrow = RenderShape.ARROW, Grid = RenderShape.GRID, Circle = RenderShape.CIRCLE, - HollowCircle = RenderShape.HollowCircle, + HollowCircle = RenderShape.HollowCircle } /** @see LinkDirection */ @@ -27,12 +26,12 @@ export enum SlotDirection { Up = LinkDirection.UP, Right = LinkDirection.RIGHT, Down = LinkDirection.DOWN, - Left = LinkDirection.LEFT, + Left = LinkDirection.LEFT } export enum LabelPosition { - Left = "left", - Right = "right", + Left = 'left', + Right = 'right' } export interface IDrawBoundingOptions { @@ -62,7 +61,7 @@ export interface IDrawTextInAreaOptions { /** The area the text will be drawn in. */ area: Rectangle /** The alignment of the text. */ - align?: "left" | "right" | "center" + align?: 'left' | 'right' | 'center' } /** @@ -82,8 +81,8 @@ export function strokeShape( color, padding = 6, collapsed = false, - lineWidth: thickness = 1, - }: IDrawBoundingOptions = {}, + lineWidth: thickness = 1 + }: IDrawBoundingOptions = {} ): void { // These param defaults are not compile-time static, and must be re-evaluated at runtime round_radius ??= LiteGraph.ROUND_RADIUS @@ -106,39 +105,39 @@ export function strokeShape( // Draw shape based on type const [x, y, width, height] = area switch (shape) { - case RenderShape.BOX: { - ctx.rect( - x - padding, - y - padding, - width + 2 * padding, - height + 2 * padding, - ) - break - } - case RenderShape.ROUND: - case RenderShape.CARD: { - const radius = round_radius + padding - const isCollapsed = shape === RenderShape.CARD && collapsed - const cornerRadii = + case RenderShape.BOX: { + ctx.rect( + x - padding, + y - padding, + width + 2 * padding, + height + 2 * padding + ) + break + } + case RenderShape.ROUND: + case RenderShape.CARD: { + const radius = round_radius + padding + const isCollapsed = shape === RenderShape.CARD && collapsed + const cornerRadii = isCollapsed || shape === RenderShape.ROUND ? [radius] : [radius, 2, radius, 2] - ctx.roundRect( - x - padding, - y - padding, - width + 2 * padding, - height + 2 * padding, - cornerRadii, - ) - break - } - case RenderShape.CIRCLE: { - const centerX = x + width / 2 - const centerY = y + height / 2 - const radius = Math.max(width, height) / 2 + padding - ctx.arc(centerX, centerY, radius, 0, Math.PI * 2) - break - } + ctx.roundRect( + x - padding, + y - padding, + width + 2 * padding, + height + 2 * padding, + cornerRadii + ) + break + } + case RenderShape.CIRCLE: { + const centerX = x + width / 2 + const centerY = y + height / 2 + const radius = Math.max(width, height) / 2 + padding + ctx.arc(centerX, centerY, radius, 0, Math.PI * 2) + break + } } // Stroke the shape @@ -159,8 +158,12 @@ export function strokeShape( * @param maxWidth The maximum width the text (plus ellipsis) can occupy. * @returns The truncated text, or the original text if it fits. */ -function truncateTextToWidth(ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string { - if (!(maxWidth > 0)) return "" +function truncateTextToWidth( + ctx: CanvasRenderingContext2D, + text: string, + maxWidth: number +): string { + if (!(maxWidth > 0)) return '' // Text fits const fullWidth = ctx.measureText(text).width @@ -174,7 +177,7 @@ function truncateTextToWidth(ctx: CanvasRenderingContext2D, text: string, maxWid if (twoDotsWidth < maxWidth) return TWO_DOT_LEADER const oneDotWidth = ctx.measureText(ONE_DOT_LEADER).width * 0.75 - return oneDotWidth < maxWidth ? ONE_DOT_LEADER : "" + return oneDotWidth < maxWidth ? ONE_DOT_LEADER : '' } let min = 0 @@ -204,22 +207,25 @@ function truncateTextToWidth(ctx: CanvasRenderingContext2D, text: string, maxWid } } - return bestLen === 0 - ? ELLIPSIS - : text.substring(0, bestLen) + ELLIPSIS + return bestLen === 0 ? ELLIPSIS : text.substring(0, bestLen) + ELLIPSIS } /** * Draws text within an area, truncating it and adding an ellipsis if necessary. */ -export function drawTextInArea({ ctx, text, area, align = "left" }: IDrawTextInAreaOptions) { +export function drawTextInArea({ + ctx, + text, + area, + align = 'left' +}: IDrawTextInAreaOptions) { const { left, right, bottom, width, centreX } = area // Text already fits const fullWidth = ctx.measureText(text).width if (fullWidth <= width) { ctx.textAlign = align - const x = align === "left" ? left : (align === "right" ? right : centreX) + const x = align === 'left' ? left : align === 'right' ? right : centreX ctx.fillText(text, x, bottom) return } @@ -229,12 +235,12 @@ export function drawTextInArea({ ctx, text, area, align = "left" }: IDrawTextInA if (truncated.length === 0) return // Draw text - left-aligned to prevent bouncing during resize - ctx.textAlign = "left" + ctx.textAlign = 'left' ctx.fillText(truncated.slice(0, -1), left, bottom) ctx.rect(left, bottom, width, 1) // Draw the ellipsis, right-aligned to the button - ctx.textAlign = "right" + ctx.textAlign = 'right' const ellipsis = truncated.at(-1)! ctx.fillText(ellipsis, right, bottom, ctx.measureText(ellipsis).width * 0.75) } diff --git a/src/lib/litegraph/src/infrastructure/ConstrainedSize.ts b/src/lib/litegraph/src/infrastructure/ConstrainedSize.ts index 0a6a8bb199..006c318e5f 100644 --- a/src/lib/litegraph/src/infrastructure/ConstrainedSize.ts +++ b/src/lib/litegraph/src/infrastructure/ConstrainedSize.ts @@ -1,6 +1,9 @@ -import type { ReadOnlyRect, ReadOnlySize, Size } from "@/lib/litegraph/src/interfaces" - -import { clamp } from "@/lib/litegraph/src/litegraph" +import type { + ReadOnlyRect, + ReadOnlySize, + Size +} from '@/lib/litegraph/src/interfaces' +import { clamp } from '@/lib/litegraph/src/litegraph' /** * Basic width and height, with min/max constraints. diff --git a/src/lib/litegraph/src/infrastructure/CustomEventTarget.ts b/src/lib/litegraph/src/infrastructure/CustomEventTarget.ts index de3131fdeb..ced6ad7aa6 100644 --- a/src/lib/litegraph/src/infrastructure/CustomEventTarget.ts +++ b/src/lib/litegraph/src/infrastructure/CustomEventTarget.ts @@ -1,7 +1,10 @@ -import type { NeverNever, PickNevers } from "@/lib/litegraph/src/types/utility" +import type { NeverNever, PickNevers } from '@/lib/litegraph/src/types/utility' type EventListeners = { - readonly [K in keyof T]: ((this: EventTarget, ev: CustomEvent) => any) | EventListenerObject | null + readonly [K in keyof T]: + | ((this: EventTarget, ev: CustomEvent) => any) + | EventListenerObject + | null } /** @@ -9,18 +12,18 @@ type EventListeners = { */ export interface ICustomEventTarget< EventMap extends Record, - Keys extends keyof EventMap & string = keyof EventMap & string, + Keys extends keyof EventMap & string = keyof EventMap & string > { addEventListener( type: K, listener: EventListeners[K], - options?: boolean | AddEventListenerOptions, + options?: boolean | AddEventListenerOptions ): void removeEventListener( type: K, listener: EventListeners[K], - options?: boolean | EventListenerOptions, + options?: boolean | EventListenerOptions ): void /** @deprecated Use {@link dispatch}. */ @@ -33,9 +36,12 @@ export interface ICustomEventTarget< */ export interface CustomEventDispatcher< EventMap extends Record, - Keys extends keyof EventMap & string = keyof EventMap & string, + Keys extends keyof EventMap & string = keyof EventMap & string > { - dispatch>(type: T, detail: EventMap[T]): boolean + dispatch>( + type: T, + detail: EventMap[T] + ): boolean dispatch>(type: T): boolean } @@ -75,10 +81,12 @@ export interface CustomEventDispatcher< * ``` */ export class CustomEventTarget< - EventMap extends Record, - Keys extends keyof EventMap & string = keyof EventMap & string, -> - extends EventTarget implements ICustomEventTarget { + EventMap extends Record, + Keys extends keyof EventMap & string = keyof EventMap & string + > + extends EventTarget + implements ICustomEventTarget +{ /** * Type-safe event dispatching. * @see {@link EventTarget.dispatchEvent} @@ -86,7 +94,10 @@ export class CustomEventTarget< * @param detail A custom object to send with the event * @returns `true` if the event was dispatched successfully, otherwise `false`. */ - dispatch>(type: T, detail: EventMap[T]): boolean + dispatch>( + type: T, + detail: EventMap[T] + ): boolean dispatch>(type: T): boolean dispatch(type: T, detail?: EventMap[T]) { const event = new CustomEvent(type as string, { detail, cancelable: true }) @@ -96,7 +107,7 @@ export class CustomEventTarget< override addEventListener( type: K, listener: EventListeners[K], - options?: boolean | AddEventListenerOptions, + options?: boolean | AddEventListenerOptions ): void { // Assertion: Contravariance on CustomEvent => Event super.addEventListener(type as string, listener as EventListener, options) @@ -105,10 +116,14 @@ export class CustomEventTarget< override removeEventListener( type: K, listener: EventListeners[K], - options?: boolean | EventListenerOptions, + options?: boolean | EventListenerOptions ): void { // Assertion: Contravariance on CustomEvent => Event - super.removeEventListener(type as string, listener as EventListener, options) + super.removeEventListener( + type as string, + listener as EventListener, + options + ) } /** @deprecated Use {@link dispatch}. */ diff --git a/src/lib/litegraph/src/infrastructure/InvalidLinkError.ts b/src/lib/litegraph/src/infrastructure/InvalidLinkError.ts index d5ffa0f857..f410f5767a 100644 --- a/src/lib/litegraph/src/infrastructure/InvalidLinkError.ts +++ b/src/lib/litegraph/src/infrastructure/InvalidLinkError.ts @@ -1,6 +1,9 @@ export class InvalidLinkError extends Error { - constructor(message: string = "Attempted to access a link that was invalid.", cause?: Error) { + constructor( + message: string = 'Attempted to access a link that was invalid.', + cause?: Error + ) { super(message, { cause }) - this.name = "InvalidLinkError" + this.name = 'InvalidLinkError' } } diff --git a/src/lib/litegraph/src/infrastructure/LGraphCanvasEventMap.ts b/src/lib/litegraph/src/infrastructure/LGraphCanvasEventMap.ts index ccbf3dfceb..6b51765f44 100644 --- a/src/lib/litegraph/src/infrastructure/LGraphCanvasEventMap.ts +++ b/src/lib/litegraph/src/infrastructure/LGraphCanvasEventMap.ts @@ -1,44 +1,44 @@ -import type { ConnectingLink } from "@/lib/litegraph/src/interfaces" -import type { LGraph } from "@/lib/litegraph/src/LGraph" -import type { LGraphButton } from "@/lib/litegraph/src/LGraphButton" -import type { LGraphGroup } from "@/lib/litegraph/src/LGraphGroup" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { Subgraph } from "@/lib/litegraph/src/subgraph/Subgraph" -import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events" +import type { LGraph } from '@/lib/litegraph/src/LGraph' +import type { LGraphButton } from '@/lib/litegraph/src/LGraphButton' +import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { ConnectingLink } from '@/lib/litegraph/src/interfaces' +import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' export interface LGraphCanvasEventMap { /** The active graph has changed. */ - "litegraph:set-graph": { + 'litegraph:set-graph': { /** The new active graph. */ newGraph: LGraph | Subgraph /** The old active graph, or `null` if there was no active graph. */ oldGraph: LGraph | Subgraph | null | undefined } - "litegraph:canvas": - | { subType: "before-change" | "after-change" } + 'litegraph:canvas': + | { subType: 'before-change' | 'after-change' } | { - subType: "empty-release" - originalEvent?: CanvasPointerEvent - linkReleaseContext?: { links: ConnectingLink[] } - } + subType: 'empty-release' + originalEvent?: CanvasPointerEvent + linkReleaseContext?: { links: ConnectingLink[] } + } | { - subType: "group-double-click" - originalEvent?: CanvasPointerEvent - group: LGraphGroup - } + subType: 'group-double-click' + originalEvent?: CanvasPointerEvent + group: LGraphGroup + } | { - subType: "empty-double-click" - originalEvent?: CanvasPointerEvent - } + subType: 'empty-double-click' + originalEvent?: CanvasPointerEvent + } | { - subType: "node-double-click" - originalEvent?: CanvasPointerEvent - node: LGraphNode - } + subType: 'node-double-click' + originalEvent?: CanvasPointerEvent + node: LGraphNode + } /** A title button on a node was clicked. */ - "litegraph:node-title-button-clicked": { + 'litegraph:node-title-button-clicked': { node: LGraphNode button: LGraphButton } diff --git a/src/lib/litegraph/src/infrastructure/LGraphEventMap.ts b/src/lib/litegraph/src/infrastructure/LGraphEventMap.ts index 792fd3ff9b..dcf836a3e0 100644 --- a/src/lib/litegraph/src/infrastructure/LGraphEventMap.ts +++ b/src/lib/litegraph/src/infrastructure/LGraphEventMap.ts @@ -1,19 +1,23 @@ -import type { ReadOnlyRect } from "@/lib/litegraph/src/interfaces" -import type { LGraph } from "@/lib/litegraph/src/LGraph" -import type { LLink, ResolvedConnection } from "@/lib/litegraph/src/LLink" -import type { Subgraph } from "@/lib/litegraph/src/subgraph/Subgraph" -import type { ExportedSubgraph, ISerialisedGraph, SerialisableGraph } from "@/lib/litegraph/src/types/serialisation" +import type { LGraph } from '@/lib/litegraph/src/LGraph' +import type { LLink, ResolvedConnection } from '@/lib/litegraph/src/LLink' +import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces' +import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph' +import type { + ExportedSubgraph, + ISerialisedGraph, + SerialisableGraph +} from '@/lib/litegraph/src/types/serialisation' export interface LGraphEventMap { - "configuring": { + configuring: { /** The data that was used to configure the graph. */ data: ISerialisedGraph | SerialisableGraph /** If `true`, the graph will be cleared prior to adding the configuration. */ clearGraph: boolean } - "configured": never + configured: never - "subgraph-created": { + 'subgraph-created': { /** The subgraph that was created. */ subgraph: Subgraph /** The raw data that was used to create the subgraph. */ @@ -21,7 +25,7 @@ export interface LGraphEventMap { } /** Dispatched when a group of items are converted to a subgraph. */ - "convert-to-subgraph": { + 'convert-to-subgraph': { /** The type of subgraph to create. */ subgraph: Subgraph /** The boundary around every item that was moved into the subgraph. */ @@ -40,7 +44,7 @@ export interface LGraphEventMap { internalLinks: LLink[] } - "open-subgraph": { + 'open-subgraph': { subgraph: Subgraph closingGraph: LGraph | Subgraph } diff --git a/src/lib/litegraph/src/infrastructure/LinkConnectorEventMap.ts b/src/lib/litegraph/src/infrastructure/LinkConnectorEventMap.ts index 086c7b7a69..837c131ded 100644 --- a/src/lib/litegraph/src/infrastructure/LinkConnectorEventMap.ts +++ b/src/lib/litegraph/src/infrastructure/LinkConnectorEventMap.ts @@ -1,52 +1,52 @@ -import type { FloatingRenderLink } from "@/lib/litegraph/src/canvas/FloatingRenderLink" -import type { MovingInputLink } from "@/lib/litegraph/src/canvas/MovingInputLink" -import type { MovingOutputLink } from "@/lib/litegraph/src/canvas/MovingOutputLink" -import type { RenderLink } from "@/lib/litegraph/src/canvas/RenderLink" -import type { ToInputFromIoNodeLink } from "@/lib/litegraph/src/canvas/ToInputFromIoNodeLink" -import type { ToInputRenderLink } from "@/lib/litegraph/src/canvas/ToInputRenderLink" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { LLink } from "@/lib/litegraph/src/LLink" -import type { Reroute } from "@/lib/litegraph/src/Reroute" -import type { SubgraphInputNode } from "@/lib/litegraph/src/subgraph/SubgraphInputNode" -import type { SubgraphOutputNode } from "@/lib/litegraph/src/subgraph/SubgraphOutputNode" -import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events" -import type { IWidget } from "@/lib/litegraph/src/types/widgets" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LLink } from '@/lib/litegraph/src/LLink' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { FloatingRenderLink } from '@/lib/litegraph/src/canvas/FloatingRenderLink' +import type { MovingInputLink } from '@/lib/litegraph/src/canvas/MovingInputLink' +import type { MovingOutputLink } from '@/lib/litegraph/src/canvas/MovingOutputLink' +import type { RenderLink } from '@/lib/litegraph/src/canvas/RenderLink' +import type { ToInputFromIoNodeLink } from '@/lib/litegraph/src/canvas/ToInputFromIoNodeLink' +import type { ToInputRenderLink } from '@/lib/litegraph/src/canvas/ToInputRenderLink' +import type { SubgraphInputNode } from '@/lib/litegraph/src/subgraph/SubgraphInputNode' +import type { SubgraphOutputNode } from '@/lib/litegraph/src/subgraph/SubgraphOutputNode' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' +import type { IWidget } from '@/lib/litegraph/src/types/widgets' export interface LinkConnectorEventMap { - "reset": boolean + reset: boolean - "before-drop-links": { + 'before-drop-links': { renderLinks: RenderLink[] event: CanvasPointerEvent } - "after-drop-links": { + 'after-drop-links': { renderLinks: RenderLink[] event: CanvasPointerEvent } - "before-move-input": MovingInputLink | FloatingRenderLink - "before-move-output": MovingOutputLink | FloatingRenderLink + 'before-move-input': MovingInputLink | FloatingRenderLink + 'before-move-output': MovingOutputLink | FloatingRenderLink - "input-moved": MovingInputLink | FloatingRenderLink | ToInputFromIoNodeLink - "output-moved": MovingOutputLink | FloatingRenderLink + 'input-moved': MovingInputLink | FloatingRenderLink | ToInputFromIoNodeLink + 'output-moved': MovingOutputLink | FloatingRenderLink - "link-created": LLink | null | undefined + 'link-created': LLink | null | undefined - "dropped-on-reroute": { + 'dropped-on-reroute': { reroute: Reroute event: CanvasPointerEvent } - "dropped-on-node": { + 'dropped-on-node': { node: LGraphNode event: CanvasPointerEvent } - "dropped-on-io-node": { + 'dropped-on-io-node': { node: SubgraphInputNode | SubgraphOutputNode event: CanvasPointerEvent } - "dropped-on-canvas": CanvasPointerEvent + 'dropped-on-canvas': CanvasPointerEvent - "dropped-on-widget": { + 'dropped-on-widget': { link: ToInputRenderLink node: LGraphNode widget: IWidget diff --git a/src/lib/litegraph/src/infrastructure/NullGraphError.ts b/src/lib/litegraph/src/infrastructure/NullGraphError.ts index a83984ef31..8c064c1f73 100644 --- a/src/lib/litegraph/src/infrastructure/NullGraphError.ts +++ b/src/lib/litegraph/src/infrastructure/NullGraphError.ts @@ -1,6 +1,9 @@ export class NullGraphError extends Error { - constructor(message: string = "Attempted to access LGraph reference that was null or undefined.", cause?: Error) { + constructor( + message: string = 'Attempted to access LGraph reference that was null or undefined.', + cause?: Error + ) { super(message, { cause }) - this.name = "NullGraphError" + this.name = 'NullGraphError' } } diff --git a/src/lib/litegraph/src/infrastructure/Rectangle.ts b/src/lib/litegraph/src/infrastructure/Rectangle.ts index ab5aa5d53f..1bdc26fb0f 100644 --- a/src/lib/litegraph/src/infrastructure/Rectangle.ts +++ b/src/lib/litegraph/src/infrastructure/Rectangle.ts @@ -1,6 +1,13 @@ -import type { CompassCorners, Point, ReadOnlyPoint, ReadOnlyRect, ReadOnlySize, ReadOnlyTypedArray, Size } from "@/lib/litegraph/src/interfaces" - -import { isInRectangle } from "@/lib/litegraph/src/measure" +import type { + CompassCorners, + Point, + ReadOnlyPoint, + ReadOnlyRect, + ReadOnlySize, + ReadOnlyTypedArray, + Size +} from '@/lib/litegraph/src/interfaces' +import { isInRectangle } from '@/lib/litegraph/src/measure' /** * A rectangle, represented as a float64 array of 4 numbers: [x, y, width, height]. @@ -17,7 +24,12 @@ export class Rectangle extends Float64Array { #pos: Point | undefined #size: Size | undefined - constructor(x: number = 0, y: number = 0, width: number = 0, height: number = 0) { + constructor( + x: number = 0, + y: number = 0, + width: number = 0, + height: number = 0 + ) { super(4) this[0] = x @@ -37,7 +49,11 @@ export class Rectangle extends Float64Array { * @param height The height of the rectangle. Default: {@link width} * @returns A new rectangle whose centre is at {@link x} */ - static fromCentre([x, y]: ReadOnlyPoint, width: number, height = width): Rectangle { + static fromCentre( + [x, y]: ReadOnlyPoint, + width: number, + height = width + ): Rectangle { const left = x - width * 0.5 const top = y - height * 0.5 return new Rectangle(left, top, width, height) @@ -161,12 +177,12 @@ export class Rectangle extends Float64Array { /** The x co-ordinate of the centre of this rectangle. */ get centreX() { - return this[0] + (this[2] * 0.5) + return this[0] + this[2] * 0.5 } /** The y co-ordinate of the centre of this rectangle. */ get centreY() { - return this[1] + (this[3] * 0.5) + return this[1] + this[3] * 0.5 } // #endregion Property accessors @@ -189,10 +205,7 @@ export class Rectangle extends Float64Array { */ containsXy(x: number, y: number): boolean { const [left, top, width, height] = this - return x >= left && - x < left + width && - y >= top && - y < top + height + return x >= left && x < left + width && y >= top && y < top + height } /** @@ -202,10 +215,7 @@ export class Rectangle extends Float64Array { */ containsPoint([x, y]: ReadOnlyPoint): boolean { const [left, top, width, height] = this - return x >= left && - x < left + width && - y >= top && - y < top + height + return x >= left && x < left + width && y >= top && y < top + height } /** @@ -219,16 +229,19 @@ export class Rectangle extends Float64Array { const otherRight = other[0] + other[2] const otherBottom = other[1] + other[3] - const identical = this.x === other[0] && + const identical = + this.x === other[0] && this.y === other[1] && right === otherRight && bottom === otherBottom - return !identical && + return ( + !identical && this.x <= other[0] && this.y <= other[1] && right >= otherRight && bottom >= otherBottom + ) } /** @@ -237,10 +250,12 @@ export class Rectangle extends Float64Array { * @returns `true` if {@link rect} overlaps with this rectangle, otherwise `false`. */ overlaps(rect: ReadOnlyRect): boolean { - return this.x < rect[0] + rect[2] && + return ( + this.x < rect[0] + rect[2] && this.y < rect[1] + rect[3] && this.x + this.width > rect[0] && this.y + this.height > rect[1] + ) } /** @@ -250,11 +265,15 @@ export class Rectangle extends Float64Array { * @param cornerSize Each corner is treated as an inset square with this width and height. * @returns The compass direction of the corner that contains the point, or `undefined` if the point is not in any corner. */ - findContainingCorner(x: number, y: number, cornerSize: number): CompassCorners | undefined { - if (this.isInTopLeftCorner(x, y, cornerSize)) return "NW" - if (this.isInTopRightCorner(x, y, cornerSize)) return "NE" - if (this.isInBottomLeftCorner(x, y, cornerSize)) return "SW" - if (this.isInBottomRightCorner(x, y, cornerSize)) return "SE" + findContainingCorner( + x: number, + y: number, + cornerSize: number + ): CompassCorners | undefined { + if (this.isInTopLeftCorner(x, y, cornerSize)) return 'NW' + if (this.isInTopRightCorner(x, y, cornerSize)) return 'NE' + if (this.isInBottomLeftCorner(x, y, cornerSize)) return 'SW' + if (this.isInBottomRightCorner(x, y, cornerSize)) return 'SE' } /** @returns `true` if the point [{@link x}, {@link y}] is in the top-left corner of this rectangle, otherwise `false`. */ @@ -264,17 +283,38 @@ export class Rectangle extends Float64Array { /** @returns `true` if the point [{@link x}, {@link y}] is in the top-right corner of this rectangle, otherwise `false`. */ isInTopRightCorner(x: number, y: number, cornerSize: number): boolean { - return isInRectangle(x, y, this.right - cornerSize, this.y, cornerSize, cornerSize) + return isInRectangle( + x, + y, + this.right - cornerSize, + this.y, + cornerSize, + cornerSize + ) } /** @returns `true` if the point [{@link x}, {@link y}] is in the bottom-left corner of this rectangle, otherwise `false`. */ isInBottomLeftCorner(x: number, y: number, cornerSize: number): boolean { - return isInRectangle(x, y, this.x, this.bottom - cornerSize, cornerSize, cornerSize) + return isInRectangle( + x, + y, + this.x, + this.bottom - cornerSize, + cornerSize, + cornerSize + ) } /** @returns `true` if the point [{@link x}, {@link y}] is in the bottom-right corner of this rectangle, otherwise `false`. */ isInBottomRightCorner(x: number, y: number, cornerSize: number): boolean { - return isInRectangle(x, y, this.right - cornerSize, this.bottom - cornerSize, cornerSize, cornerSize) + return isInRectangle( + x, + y, + this.right - cornerSize, + this.bottom - cornerSize, + cornerSize, + cornerSize + ) } /** @returns `true` if the point [{@link x}, {@link y}] is in the top edge of this rectangle, otherwise `false`. */ @@ -284,7 +324,14 @@ export class Rectangle extends Float64Array { /** @returns `true` if the point [{@link x}, {@link y}] is in the bottom edge of this rectangle, otherwise `false`. */ isInBottomEdge(x: number, y: number, edgeSize: number): boolean { - return isInRectangle(x, y, this.x, this.bottom - edgeSize, this.width, edgeSize) + return isInRectangle( + x, + y, + this.x, + this.bottom - edgeSize, + this.width, + edgeSize + ) } /** @returns `true` if the point [{@link x}, {@link y}] is in the left edge of this rectangle, otherwise `false`. */ @@ -294,7 +341,14 @@ export class Rectangle extends Float64Array { /** @returns `true` if the point [{@link x}, {@link y}] is in the right edge of this rectangle, otherwise `false`. */ isInRightEdge(x: number, y: number, edgeSize: number): boolean { - return isInRectangle(x, y, this.right - edgeSize, this.y, edgeSize, this.height) + return isInRectangle( + x, + y, + this.right - edgeSize, + this.y, + edgeSize, + this.height + ) } /** @returns The centre point of this rectangle, as a new {@link Point}. */ @@ -387,7 +441,9 @@ export class Rectangle extends Float64Array { } /** Alias of {@link export}. */ - toArray() { return this.export() } + toArray() { + return this.export() + } /** @returns A new, untyped array (serializable) containing the values of this rectangle. */ export(): [number, number, number, number] { @@ -398,7 +454,7 @@ export class Rectangle extends Float64Array { * Draws a debug outline of this rectangle. * @internal Convenience debug/development interface; not for production use. */ - _drawDebug(ctx: CanvasRenderingContext2D, colour = "red") { + _drawDebug(ctx: CanvasRenderingContext2D, colour = 'red') { const { strokeStyle, lineWidth } = ctx try { ctx.strokeStyle = colour @@ -414,12 +470,12 @@ export class Rectangle extends Float64Array { export type ReadOnlyRectangle = Omit< ReadOnlyTypedArray, - | "setHeightBottomAnchored" - | "setWidthRightAnchored" - | "resizeTopLeft" - | "resizeBottomLeft" - | "resizeTopRight" - | "resizeBottomRight" - | "resizeBottomRight" - | "updateTo" + | 'setHeightBottomAnchored' + | 'setWidthRightAnchored' + | 'resizeTopLeft' + | 'resizeBottomLeft' + | 'resizeTopRight' + | 'resizeBottomRight' + | 'resizeBottomRight' + | 'updateTo' > diff --git a/src/lib/litegraph/src/infrastructure/RecursionError.ts b/src/lib/litegraph/src/infrastructure/RecursionError.ts index bba29e9c8d..233f001777 100644 --- a/src/lib/litegraph/src/infrastructure/RecursionError.ts +++ b/src/lib/litegraph/src/infrastructure/RecursionError.ts @@ -4,6 +4,6 @@ export class RecursionError extends Error { constructor(subject: string) { super(subject) - this.name = "RecursionError" + this.name = 'RecursionError' } } diff --git a/src/lib/litegraph/src/infrastructure/SlotIndexError.ts b/src/lib/litegraph/src/infrastructure/SlotIndexError.ts index 4ab963679f..8114df9b17 100644 --- a/src/lib/litegraph/src/infrastructure/SlotIndexError.ts +++ b/src/lib/litegraph/src/infrastructure/SlotIndexError.ts @@ -1,6 +1,9 @@ export class SlotIndexError extends Error { - constructor(message: string = "Attempted to access a slot that was out of bounds.", cause?: Error) { + constructor( + message: string = 'Attempted to access a slot that was out of bounds.', + cause?: Error + ) { super(message, { cause }) - this.name = "SlotIndexError" + this.name = 'SlotIndexError' } } diff --git a/src/lib/litegraph/src/infrastructure/SubgraphEventMap.ts b/src/lib/litegraph/src/infrastructure/SubgraphEventMap.ts index c7850659b2..e323b6d544 100644 --- a/src/lib/litegraph/src/infrastructure/SubgraphEventMap.ts +++ b/src/lib/litegraph/src/infrastructure/SubgraphEventMap.ts @@ -1,53 +1,54 @@ -import type { LGraphEventMap } from "./LGraphEventMap" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { SubgraphNode } from "@/lib/litegraph/src/subgraph/SubgraphNode" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" -import type { IBaseWidget } from "@/lib/litegraph/src/types/widgets" +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' + +import type { LGraphEventMap } from './LGraphEventMap' export interface SubgraphEventMap extends LGraphEventMap { - "adding-input": { + 'adding-input': { name: string type: string } - "adding-output": { + 'adding-output': { name: string type: string } - "input-added": { + 'input-added': { input: SubgraphInput } - "output-added": { + 'output-added': { output: SubgraphOutput } - "removing-input": { + 'removing-input': { input: SubgraphInput index: number } - "removing-output": { + 'removing-output': { output: SubgraphOutput index: number } - "renaming-input": { + 'renaming-input': { input: SubgraphInput index: number oldName: string newName: string } - "renaming-output": { + 'renaming-output': { output: SubgraphOutput index: number oldName: string newName: string } - "widget-promoted": { + 'widget-promoted': { widget: IBaseWidget subgraphNode: SubgraphNode } - "widget-demoted": { + 'widget-demoted': { widget: IBaseWidget subgraphNode: SubgraphNode } diff --git a/src/lib/litegraph/src/infrastructure/SubgraphInputEventMap.ts b/src/lib/litegraph/src/infrastructure/SubgraphInputEventMap.ts index 2bb3ed97e1..2a5927558e 100644 --- a/src/lib/litegraph/src/infrastructure/SubgraphInputEventMap.ts +++ b/src/lib/litegraph/src/infrastructure/SubgraphInputEventMap.ts @@ -1,15 +1,16 @@ -import type { LGraphEventMap } from "./LGraphEventMap" -import type { INodeInputSlot } from "@/lib/litegraph/src/litegraph" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { IBaseWidget } from "@/lib/litegraph/src/types/widgets" +import type { INodeInputSlot } from '@/lib/litegraph/src/litegraph' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' + +import type { LGraphEventMap } from './LGraphEventMap' export interface SubgraphInputEventMap extends LGraphEventMap { - "input-connected": { + 'input-connected': { input: INodeInputSlot widget: IBaseWidget } - "input-disconnected": { + 'input-disconnected': { input: SubgraphInput } } diff --git a/src/lib/litegraph/src/interfaces.ts b/src/lib/litegraph/src/interfaces.ts index 97c38f2c1d..67870b390e 100644 --- a/src/lib/litegraph/src/interfaces.ts +++ b/src/lib/litegraph/src/interfaces.ts @@ -1,13 +1,14 @@ -import type { ContextMenu } from "./ContextMenu" -import type { LGraphNode, NodeId } from "./LGraphNode" -import type { LinkId, LLink } from "./LLink" -import type { Reroute, RerouteId } from "./Reroute" -import type { SubgraphInputNode } from "./subgraph/SubgraphInputNode" -import type { SubgraphOutputNode } from "./subgraph/SubgraphOutputNode" -import type { LinkDirection, RenderShape } from "./types/globalEnums" -import type { IBaseWidget } from "./types/widgets" -import type { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" -import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events" +import type { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' + +import type { ContextMenu } from './ContextMenu' +import type { LGraphNode, NodeId } from './LGraphNode' +import type { LLink, LinkId } from './LLink' +import type { Reroute, RerouteId } from './Reroute' +import type { SubgraphInputNode } from './subgraph/SubgraphInputNode' +import type { SubgraphOutputNode } from './subgraph/SubgraphOutputNode' +import type { LinkDirection, RenderShape } from './types/globalEnums' +import type { IBaseWidget } from './types/widgets' export type Dictionary = { [key: string]: T } @@ -20,13 +21,21 @@ export type NullableProperties = { * If {@link T} is `null` or `undefined`, evaluates to {@link Result}. Otherwise, evaluates to {@link T}. * Useful for functions that return e.g. `undefined` when a param is nullish. */ -export type WhenNullish = T & {} | (T extends null ? Result : T extends undefined ? Result : T & {}) +export type WhenNullish = + | (T & {}) + | (T extends null ? Result : T extends undefined ? Result : T & {}) /** A type with each of the {@link Properties} made optional. */ -export type OptionalProps = Omit & { [K in Properties]?: T[K] } +export type OptionalProps = Omit< + T, + Properties +> & { [K in Properties]?: T[K] } /** A type with each of the {@link Properties} marked as required. */ -export type RequiredProps = Omit & { [K in Properties]-?: T[K] } +export type RequiredProps = Omit< + T, + Properties +> & { [K in Properties]-?: T[K] } /** Bitwise AND intersection of two types; returns a new, non-union type that includes only properties that exist on both types. */ export type SharedIntersection = { @@ -167,7 +176,10 @@ export interface LinkNetwork extends ReadonlyLinkNetwork { export interface ItemLocator { getNodeOnPos(x: number, y: number, nodeList?: LGraphNode[]): LGraphNode | null getRerouteOnPos(x: number, y: number): Reroute | undefined - getIoNodeOnPos?(x: number, y: number): SubgraphInputNode | SubgraphOutputNode | undefined + getIoNodeOnPos?( + x: number, + y: number + ): SubgraphInputNode | SubgraphOutputNode | undefined } /** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */ @@ -254,10 +266,16 @@ type TypedArrays = type TypedBigIntArrays = BigInt64Array | BigUint64Array export type ReadOnlyTypedArray = - Omit, "fill" | "copyWithin" | "reverse" | "set" | "sort" | "subarray"> + Omit< + Readonly, + 'fill' | 'copyWithin' | 'reverse' | 'set' | 'sort' | 'subarray' + > /** Union of property names that are of type Match */ -export type KeysOfType = Exclude<{ [P in keyof T]: T[P] extends Match ? P : never }[keyof T], undefined> +export type KeysOfType = Exclude< + { [P in keyof T]: T[P] extends Match ? P : never }[keyof T], + undefined +> /** A new type that contains only the properties of T that are of type Match */ export type PickByType = { [P in keyof T]: Extract } @@ -272,10 +290,10 @@ export interface IBoundaryNodes { left: LGraphNode } -export type Direction = "top" | "bottom" | "left" | "right" +export type Direction = 'top' | 'bottom' | 'left' | 'right' /** Resize handle positions (compass points) */ -export type CompassCorners = "NE" | "SE" | "SW" | "NW" +export type CompassCorners = 'NE' | 'SE' | 'SW' | 'NW' /** * A string that represents a specific data / slot type, e.g. `STRING`. @@ -383,7 +401,8 @@ interface IContextMenuBase { } /** ContextMenu */ -export interface IContextMenuOptions extends IContextMenuBase { +export interface IContextMenuOptions + extends IContextMenuBase { ignore_item_callbacks?: boolean parentMenu?: ContextMenu event?: MouseEvent @@ -401,11 +420,15 @@ export interface IContextMenuOptions extends options?: unknown, event?: MouseEvent, previous_menu?: ContextMenu, - extra?: unknown, + extra?: unknown ): void | boolean } -export interface IContextMenuValue extends IContextMenuBase { +export interface IContextMenuValue< + TValue = unknown, + TExtra = unknown, + TCallbackValue = unknown +> extends IContextMenuBase { value?: TValue content: string | undefined has_submenu?: boolean @@ -420,20 +443,26 @@ export interface IContextMenuValue, - extra?: TExtra, + extra?: TExtra ): void | boolean } -export interface IContextMenuSubmenu extends IContextMenuOptions { +export interface IContextMenuSubmenu + extends IContextMenuOptions { options: ConstructorParameters>[0] } -export interface ContextMenuDivElement extends HTMLDivElement { +export interface ContextMenuDivElement + extends HTMLDivElement { value?: string | IContextMenuValue onclick_callback?: never } -export type INodeSlotContextItem = [string, ISlotType, Partial] +export type INodeSlotContextItem = [ + string, + ISlotType, + Partial +] export interface DefaultConnectionColors { getConnectedColor(type: ISlotType): CanvasColour @@ -463,7 +492,8 @@ export type CallbackParams any) | undefined> = * Shorthand for {@link ReturnType} of optional callbacks. * @see {@link CallbackParams} */ -export type CallbackReturn any) | undefined> = ReturnType> +export type CallbackReturn any) | undefined> = + ReturnType> /** * An object that can be hovered over. diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts index c2b4ba9a4b..222ca43dee 100644 --- a/src/lib/litegraph/src/litegraph.ts +++ b/src/lib/litegraph/src/litegraph.ts @@ -1,19 +1,14 @@ -import type { ContextMenu } from "./ContextMenu" -import type { ConnectingLink, Point } from "./interfaces" -import type { - IContextMenuOptions, - INodeSlot, - Size, -} from "./interfaces" -import type { LGraphNode } from "./LGraphNode" -import type { CanvasEventDetail } from "./types/events" -import type { RenderShape, TitleMode } from "./types/globalEnums" +import type { ContextMenu } from './ContextMenu' +import type { LGraphNode } from './LGraphNode' +import { LiteGraphGlobal } from './LiteGraphGlobal' +import type { ConnectingLink, Point } from './interfaces' +import type { IContextMenuOptions, INodeSlot, Size } from './interfaces' +import { loadPolyfills } from './polyfills' +import type { CanvasEventDetail } from './types/events' +import type { RenderShape, TitleMode } from './types/globalEnums' // Must remain above LiteGraphGlobal (circular dependency due to abstract factory behaviour in `configure`) -export { Subgraph } from "./subgraph/Subgraph" - -import { LiteGraphGlobal } from "./LiteGraphGlobal" -import { loadPolyfills } from "./polyfills" +export { Subgraph } from './subgraph/Subgraph' export const LiteGraph = new LiteGraphGlobal() @@ -48,7 +43,7 @@ export type ContextMenuEventListener = ( options: IContextMenuOptions, event: MouseEvent, parentMenu: ContextMenu | undefined, - node: LGraphNode, + node: LGraphNode ) => boolean | void export interface LinkReleaseContext { @@ -88,17 +83,17 @@ export interface LGraphNodeConstructor { // End backwards compat -export { InputIndicators } from "./canvas/InputIndicators" -export { LinkConnector } from "./canvas/LinkConnector" -export { isOverNodeInput, isOverNodeOutput } from "./canvas/measureSlots" -export { CanvasPointer } from "./CanvasPointer" -export * as Constants from "./constants" -export { ContextMenu } from "./ContextMenu" -export { CurveEditor } from "./CurveEditor" -export { DragAndScale } from "./DragAndScale" -export { LabelPosition, SlotDirection, SlotShape, SlotType } from "./draw" -export { strokeShape } from "./draw" -export { Rectangle } from "./infrastructure/Rectangle" +export { InputIndicators } from './canvas/InputIndicators' +export { LinkConnector } from './canvas/LinkConnector' +export { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots' +export { CanvasPointer } from './CanvasPointer' +export * as Constants from './constants' +export { ContextMenu } from './ContextMenu' +export { CurveEditor } from './CurveEditor' +export { DragAndScale } from './DragAndScale' +export { LabelPosition, SlotDirection, SlotShape, SlotType } from './draw' +export { strokeShape } from './draw' +export { Rectangle } from './infrastructure/Rectangle' export type { CanvasColour, ColorOption, @@ -126,27 +121,35 @@ export type { ReadOnlyPoint, ReadOnlyRect, Rect, - Size, -} from "./interfaces" -export { LGraph } from "./LGraph" -export { BadgePosition, LGraphBadge, type LGraphBadgeOptions } from "./LGraphBadge" -export { LGraphCanvas, type LGraphCanvasState } from "./LGraphCanvas" -export { LGraphGroup } from "./LGraphGroup" -export { LGraphNode, type NodeId } from "./LGraphNode" -export { type LinkId, LLink } from "./LLink" -export { clamp, createBounds } from "./measure" -export { Reroute, type RerouteId } from "./Reroute" -export { type ExecutableLGraphNode, ExecutableNodeDTO, type ExecutionId } from "./subgraph/ExecutableNodeDTO" -export { SubgraphNode } from "./subgraph/SubgraphNode" -export type { CanvasPointerEvent } from "./types/events" + Size +} from './interfaces' +export { LGraph } from './LGraph' +export { + BadgePosition, + LGraphBadge, + type LGraphBadgeOptions +} from './LGraphBadge' +export { LGraphCanvas, type LGraphCanvasState } from './LGraphCanvas' +export { LGraphGroup } from './LGraphGroup' +export { LGraphNode, type NodeId } from './LGraphNode' +export { type LinkId, LLink } from './LLink' +export { clamp, createBounds } from './measure' +export { Reroute, type RerouteId } from './Reroute' +export { + type ExecutableLGraphNode, + ExecutableNodeDTO, + type ExecutionId +} from './subgraph/ExecutableNodeDTO' +export { SubgraphNode } from './subgraph/SubgraphNode' +export type { CanvasPointerEvent } from './types/events' export { CanvasItem, EaseFunction, LGraphEventMode, LinkMarkerShape, RenderShape, - TitleMode, -} from "./types/globalEnums" + TitleMode +} from './types/globalEnums' export type { ExportedSubgraph, ExportedSubgraphInstance, @@ -154,19 +157,19 @@ export type { ISerialisedGraph, SerialisableGraph, SerialisableLLink, - SubgraphIO, -} from "./types/serialisation" -export type { IWidget } from "./types/widgets" -export { isColorable } from "./utils/type" -export { createUuidv4 } from "./utils/uuid" -export { BaseSteppedWidget } from "./widgets/BaseSteppedWidget" -export { BaseWidget } from "./widgets/BaseWidget" -export { BooleanWidget } from "./widgets/BooleanWidget" -export { ButtonWidget } from "./widgets/ButtonWidget" -export { ComboWidget } from "./widgets/ComboWidget" -export { KnobWidget } from "./widgets/KnobWidget" -export { LegacyWidget } from "./widgets/LegacyWidget" -export { NumberWidget } from "./widgets/NumberWidget" -export { SliderWidget } from "./widgets/SliderWidget" -export { TextWidget } from "./widgets/TextWidget" -export { isComboWidget } from "./widgets/widgetMap" + SubgraphIO +} from './types/serialisation' +export type { IWidget } from './types/widgets' +export { isColorable } from './utils/type' +export { createUuidv4 } from './utils/uuid' +export { BaseSteppedWidget } from './widgets/BaseSteppedWidget' +export { BaseWidget } from './widgets/BaseWidget' +export { BooleanWidget } from './widgets/BooleanWidget' +export { ButtonWidget } from './widgets/ButtonWidget' +export { ComboWidget } from './widgets/ComboWidget' +export { KnobWidget } from './widgets/KnobWidget' +export { LegacyWidget } from './widgets/LegacyWidget' +export { NumberWidget } from './widgets/NumberWidget' +export { SliderWidget } from './widgets/SliderWidget' +export { TextWidget } from './widgets/TextWidget' +export { isComboWidget } from './widgets/widgetMap' diff --git a/src/lib/litegraph/src/measure.ts b/src/lib/litegraph/src/measure.ts index 4c2417d3fc..638c987fba 100644 --- a/src/lib/litegraph/src/measure.ts +++ b/src/lib/litegraph/src/measure.ts @@ -3,10 +3,9 @@ import type { Point, ReadOnlyPoint, ReadOnlyRect, - Rect, -} from "./interfaces" - -import { Alignment, hasFlag, LinkDirection } from "./types/globalEnums" + Rect +} from './interfaces' +import { Alignment, LinkDirection, hasFlag } from './types/globalEnums' /** * Calculates the distance between two points (2D vector) @@ -16,7 +15,7 @@ import { Alignment, hasFlag, LinkDirection } from "./types/globalEnums" */ export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number { return Math.sqrt( - (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]), + (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) ) } @@ -30,7 +29,7 @@ export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number { * @returns Distance2 (squared) between point [{@link x1}, {@link y1}] & [{@link x2}, {@link y2}] */ export function dist2(x1: number, y1: number, x2: number, y2: number): number { - return ((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)) + return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) } /** @@ -51,12 +50,9 @@ export function isInRectangle( left: number, top: number, width: number, - height: number, + height: number ): boolean { - return x >= left && - x < left + width && - y >= top && - y < top + height + return x >= left && x < left + width && y >= top && y < top + height } /** @@ -65,11 +61,16 @@ export function isInRectangle( * @param rect The rectangle, as `x, y, width, height` * @returns `true` if the point is inside the rect, otherwise `false` */ -export function isPointInRect(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean { - return point[0] >= rect[0] && +export function isPointInRect( + point: ReadOnlyPoint, + rect: ReadOnlyRect +): boolean { + return ( + point[0] >= rect[0] && point[0] < rect[0] + rect[2] && point[1] >= rect[1] && point[1] < rect[1] + rect[3] + ) } /** @@ -80,10 +81,12 @@ export function isPointInRect(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean * @returns `true` if the point is inside the rect, otherwise `false` */ export function isInRect(x: number, y: number, rect: ReadOnlyRect): boolean { - return x >= rect[0] && + return ( + x >= rect[0] && x < rect[0] + rect[2] && y >= rect[1] && y < rect[1] + rect[3] + ) } /** @@ -107,12 +110,9 @@ export function isInsideRectangle( left: number, top: number, width: number, - height: number, + height: number ): boolean { - return left < x && - left + width > x && - top < y && - top + height > y + return left < x && left + width > x && top < y && top + height > y } /** @@ -127,10 +127,7 @@ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean { const bRight = b[0] + b[2] const bBottom = b[1] + b[3] - return a[0] > bRight || - a[1] > bBottom || - aRight < b[0] || - aBottom < b[1] + return a[0] > bRight || a[1] > bBottom || aRight < b[0] || aBottom < b[1] ? false : true } @@ -141,10 +138,7 @@ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean { * @returns The centre of the rectangle, as `x, y` */ export function getCentre(rect: ReadOnlyRect): Point { - return [ - rect[0] + (rect[2] * 0.5), - rect[1] + (rect[3] * 0.5), - ] + return [rect[0] + rect[2] * 0.5, rect[1] + rect[3] * 0.5] } /** @@ -154,8 +148,8 @@ export function getCentre(rect: ReadOnlyRect): Point { * @returns `true` if {@link a} contains most of {@link b}, otherwise `false` */ export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean { - const centreX = b[0] + (b[2] * 0.5) - const centreY = b[1] + (b[3] * 0.5) + const centreX = b[0] + b[2] * 0.5 + const centreY = b[1] + b[3] * 0.5 return isInRect(centreX, centreY, a) } @@ -171,16 +165,16 @@ export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean { const bRight = b[0] + b[2] const bBottom = b[1] + b[3] - const identical = a[0] === b[0] && - a[1] === b[1] && - aRight === bRight && - aBottom === bBottom + const identical = + a[0] === b[0] && a[1] === b[1] && aRight === bRight && aBottom === bBottom - return !identical && + return ( + !identical && a[0] <= b[0] && a[1] <= b[1] && aRight >= bRight && aBottom >= bBottom + ) } /** @@ -192,21 +186,21 @@ export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean { export function addDirectionalOffset( amount: number, direction: LinkDirection, - out: Point, + out: Point ): void { switch (direction) { - case LinkDirection.LEFT: - out[0] -= amount - return - case LinkDirection.RIGHT: - out[0] += amount - return - case LinkDirection.UP: - out[1] -= amount - return - case LinkDirection.DOWN: - out[1] += amount - return + case LinkDirection.LEFT: + out[0] -= amount + return + case LinkDirection.RIGHT: + out[0] += amount + return + case LinkDirection.UP: + out[1] -= amount + return + case LinkDirection.DOWN: + out[1] += amount + return // LinkDirection.CENTER: Nothing to do. } } @@ -223,61 +217,61 @@ export function addDirectionalOffset( export function rotateLink( offset: Point, from: LinkDirection, - to: LinkDirection, + to: LinkDirection ): void { let x: number let y: number // Normalise to left switch (from) { - case to: - case LinkDirection.CENTER: - case LinkDirection.NONE: - default: - // Nothing to do - return + case to: + case LinkDirection.CENTER: + case LinkDirection.NONE: + default: + // Nothing to do + return - case LinkDirection.LEFT: - x = offset[0] - y = offset[1] - break - case LinkDirection.RIGHT: - x = -offset[0] - y = -offset[1] - break - case LinkDirection.UP: - x = -offset[1] - y = offset[0] - break - case LinkDirection.DOWN: - x = offset[1] - y = -offset[0] - break + case LinkDirection.LEFT: + x = offset[0] + y = offset[1] + break + case LinkDirection.RIGHT: + x = -offset[0] + y = -offset[1] + break + case LinkDirection.UP: + x = -offset[1] + y = offset[0] + break + case LinkDirection.DOWN: + x = offset[1] + y = -offset[0] + break } // Apply new direction switch (to) { - case LinkDirection.CENTER: - case LinkDirection.NONE: - // Nothing to do - return + case LinkDirection.CENTER: + case LinkDirection.NONE: + // Nothing to do + return - case LinkDirection.LEFT: - offset[0] = x - offset[1] = y - break - case LinkDirection.RIGHT: - offset[0] = -x - offset[1] = -y - break - case LinkDirection.UP: - offset[0] = y - offset[1] = -x - break - case LinkDirection.DOWN: - offset[0] = -y - offset[1] = x - break + case LinkDirection.LEFT: + offset[0] = x + offset[1] = y + break + case LinkDirection.RIGHT: + offset[0] = -x + offset[1] = -y + break + case LinkDirection.UP: + offset[0] = y + offset[1] = -x + break + case LinkDirection.DOWN: + offset[0] = -y + offset[1] = x + break } } @@ -298,10 +292,12 @@ export function getOrientation( lineStart: ReadOnlyPoint, lineEnd: ReadOnlyPoint, x: number, - y: number, + y: number ): number { - return ((lineEnd[1] - lineStart[1]) * (x - lineEnd[0])) - - ((lineEnd[0] - lineStart[0]) * (y - lineEnd[1])) + return ( + (lineEnd[1] - lineStart[1]) * (x - lineEnd[0]) - + (lineEnd[0] - lineStart[0]) * (y - lineEnd[1]) + ) } /** @@ -318,7 +314,7 @@ export function findPointOnCurve( b: ReadOnlyPoint, controlA: ReadOnlyPoint, controlB: ReadOnlyPoint, - t: number = 0.5, + t: number = 0.5 ): void { const iT = 1 - t @@ -327,13 +323,13 @@ export function findPointOnCurve( const c3 = 3 * iT * (t * t) const c4 = t * t * t - out[0] = (c1 * a[0]) + (c2 * controlA[0]) + (c3 * controlB[0]) + (c4 * b[0]) - out[1] = (c1 * a[1]) + (c2 * controlA[1]) + (c3 * controlB[1]) + (c4 * b[1]) + out[0] = c1 * a[0] + c2 * controlA[0] + c3 * controlB[0] + c4 * b[0] + out[1] = c1 * a[1] + c2 * controlA[1] + c3 * controlB[1] + c4 * b[1] } export function createBounds( objects: Iterable, - padding: number = 10, + padding: number = 10 ): ReadOnlyRect | null { const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity]) @@ -344,13 +340,13 @@ export function createBounds( bounds[2] = Math.max(bounds[2], rect[0] + rect[2]) bounds[3] = Math.max(bounds[3], rect[1] + rect[3]) } - if (!bounds.every(x => isFinite(x))) return null + if (!bounds.every((x) => isFinite(x))) return null return [ bounds[0] - padding, bounds[1] - padding, - bounds[2] - bounds[0] + (2 * padding), - bounds[3] - bounds[1] + (2 * padding), + bounds[2] - bounds[0] + 2 * padding, + bounds[3] - bounds[1] + 2 * padding ] } @@ -386,7 +382,7 @@ export function alignToContainer( rect: Rect, anchors: Alignment, [containerX, containerY, containerWidth, containerHeight]: ReadOnlyRect, - [insetX, insetY]: ReadOnlyPoint = [0, 0], + [insetX, insetY]: ReadOnlyPoint = [0, 0] ): Rect { if (hasFlag(anchors, Alignment.Left)) { // Left @@ -396,7 +392,7 @@ export function alignToContainer( rect[0] = containerX + containerWidth - insetX - rect[2] } else if (hasFlag(anchors, Alignment.Centre)) { // Horizontal centre - rect[0] = containerX + (containerWidth * 0.5) - (rect[2] * 0.5) + rect[0] = containerX + containerWidth * 0.5 - rect[2] * 0.5 } if (hasFlag(anchors, Alignment.Top)) { @@ -407,7 +403,7 @@ export function alignToContainer( rect[1] = containerY + containerHeight - insetY - rect[3] } else if (hasFlag(anchors, Alignment.Middle)) { // Vertical middle - rect[1] = containerY + (containerHeight * 0.5) - (rect[3] * 0.5) + rect[1] = containerY + containerHeight * 0.5 - rect[3] * 0.5 } return rect } @@ -429,7 +425,7 @@ export function alignOutsideContainer( rect: Rect, anchors: Alignment, [otherX, otherY, otherWidth, otherHeight]: ReadOnlyRect, - [outsetX, outsetY]: ReadOnlyPoint = [0, 0], + [outsetX, outsetY]: ReadOnlyPoint = [0, 0] ): Rect { if (hasFlag(anchors, Alignment.Left)) { // Left @@ -439,7 +435,7 @@ export function alignOutsideContainer( rect[0] = otherX + otherWidth + outsetX } else if (hasFlag(anchors, Alignment.Centre)) { // Horizontal centre - rect[0] = otherX + (otherWidth * 0.5) - (rect[2] * 0.5) + rect[0] = otherX + otherWidth * 0.5 - rect[2] * 0.5 } if (hasFlag(anchors, Alignment.Top)) { @@ -450,11 +446,11 @@ export function alignOutsideContainer( rect[1] = otherY + otherHeight + outsetY } else if (hasFlag(anchors, Alignment.Middle)) { // Vertical middle - rect[1] = otherY + (otherHeight * 0.5) - (rect[3] * 0.5) + rect[1] = otherY + otherHeight * 0.5 - rect[3] * 0.5 } return rect } export function clamp(value: number, min: number, max: number): number { - return value < min ? min : (value > max ? max : value) + return value < min ? min : value > max ? max : value } diff --git a/src/lib/litegraph/src/node/NodeInputSlot.ts b/src/lib/litegraph/src/node/NodeInputSlot.ts index 5ecfa0a61e..8a6af55e9d 100644 --- a/src/lib/litegraph/src/node/NodeInputSlot.ts +++ b/src/lib/litegraph/src/node/NodeInputSlot.ts @@ -1,14 +1,18 @@ -import type { INodeInputSlot, INodeOutputSlot, OptionalProps, ReadOnlyPoint } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { LinkId } from "@/lib/litegraph/src/LLink" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" -import type { IBaseWidget } from "@/lib/litegraph/src/types/widgets" - -import { LabelPosition } from "@/lib/litegraph/src/draw" -import { LiteGraph } from "@/lib/litegraph/src/litegraph" -import { type IDrawOptions, NodeSlot } from "@/lib/litegraph/src/node/NodeSlot" -import { isSubgraphInput } from "@/lib/litegraph/src/subgraph/subgraphUtils" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LinkId } from '@/lib/litegraph/src/LLink' +import { LabelPosition } from '@/lib/litegraph/src/draw' +import type { + INodeInputSlot, + INodeOutputSlot, + OptionalProps, + ReadOnlyPoint +} from '@/lib/litegraph/src/interfaces' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import { isSubgraphInput } from '@/lib/litegraph/src/subgraph/subgraphUtils' +import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' export class NodeInputSlot extends NodeSlot implements INodeInputSlot { link: LinkId | null @@ -32,7 +36,10 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot { return [0, LiteGraph.NODE_TITLE_HEIGHT * -0.5] } - constructor(slot: OptionalProps, node: LGraphNode) { + constructor( + slot: OptionalProps, + node: LGraphNode + ) { super(slot, node) this.link = slot.link } @@ -41,8 +48,10 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot { return this.link != null } - override isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput): boolean { - if ("links" in fromSlot) { + override isValidTarget( + fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput + ): boolean { + if ('links' in fromSlot) { return LiteGraph.isValidConnection(fromSlot.type, this.type) } @@ -53,14 +62,17 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot { return false } - override draw(ctx: CanvasRenderingContext2D, options: Omit) { + override draw( + ctx: CanvasRenderingContext2D, + options: Omit + ) { const { textAlign } = ctx - ctx.textAlign = "left" + ctx.textAlign = 'left' super.draw(ctx, { ...options, labelPosition: LabelPosition.Right, - doStroke: false, + doStroke: false }) ctx.textAlign = textAlign diff --git a/src/lib/litegraph/src/node/NodeOutputSlot.ts b/src/lib/litegraph/src/node/NodeOutputSlot.ts index a018f64572..a1120dd8ec 100644 --- a/src/lib/litegraph/src/node/NodeOutputSlot.ts +++ b/src/lib/litegraph/src/node/NodeOutputSlot.ts @@ -1,13 +1,17 @@ -import type { INodeInputSlot, INodeOutputSlot, OptionalProps, ReadOnlyPoint } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { LinkId } from "@/lib/litegraph/src/LLink" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" - -import { LabelPosition } from "@/lib/litegraph/src/draw" -import { LiteGraph } from "@/lib/litegraph/src/litegraph" -import { type IDrawOptions, NodeSlot } from "@/lib/litegraph/src/node/NodeSlot" -import { isSubgraphOutput } from "@/lib/litegraph/src/subgraph/subgraphUtils" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LinkId } from '@/lib/litegraph/src/LLink' +import { LabelPosition } from '@/lib/litegraph/src/draw' +import type { + INodeInputSlot, + INodeOutputSlot, + OptionalProps, + ReadOnlyPoint +} from '@/lib/litegraph/src/interfaces' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +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 @@ -23,11 +27,14 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot { get collapsedPos(): ReadOnlyPoint { return [ this.#node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH, - LiteGraph.NODE_TITLE_HEIGHT * -0.5, + LiteGraph.NODE_TITLE_HEIGHT * -0.5 ] } - constructor(slot: OptionalProps, node: LGraphNode) { + constructor( + slot: OptionalProps, + node: LGraphNode + ) { super(slot, node) this.links = slot.links this._data = slot._data @@ -35,8 +42,10 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot { this.#node = node } - override isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput): boolean { - if ("link" in fromSlot) { + override isValidTarget( + fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput + ): boolean { + if ('link' in fromSlot) { return LiteGraph.isValidConnection(this.type, fromSlot.type) } @@ -51,15 +60,18 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot { return this.links != null && this.links.length > 0 } - override draw(ctx: CanvasRenderingContext2D, options: Omit) { + override draw( + ctx: CanvasRenderingContext2D, + options: Omit + ) { const { textAlign, strokeStyle } = ctx - ctx.textAlign = "right" - ctx.strokeStyle = "black" + ctx.textAlign = 'right' + ctx.strokeStyle = 'black' super.draw(ctx, { ...options, labelPosition: LabelPosition.Left, - doStroke: true, + doStroke: true }) ctx.textAlign = textAlign diff --git a/src/lib/litegraph/src/node/NodeSlot.ts b/src/lib/litegraph/src/node/NodeSlot.ts index 0a2d9f6b2e..527c1dfac2 100644 --- a/src/lib/litegraph/src/node/NodeSlot.ts +++ b/src/lib/litegraph/src/node/NodeSlot.ts @@ -1,15 +1,27 @@ -import type { CanvasColour, DefaultConnectionColors, INodeInputSlot, INodeOutputSlot, INodeSlot, ISubgraphInput, OptionalProps, Point, ReadOnlyPoint } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { SubgraphInput } from "@/lib/litegraph/src/subgraph/SubgraphInput" -import type { SubgraphOutput } from "@/lib/litegraph/src/subgraph/SubgraphOutput" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LabelPosition, SlotShape, SlotType } from '@/lib/litegraph/src/draw' +import type { + CanvasColour, + DefaultConnectionColors, + INodeInputSlot, + INodeOutputSlot, + INodeSlot, + ISubgraphInput, + OptionalProps, + Point, + ReadOnlyPoint +} from '@/lib/litegraph/src/interfaces' +import { LiteGraph, Rectangle } from '@/lib/litegraph/src/litegraph' +import { getCentre } from '@/lib/litegraph/src/measure' +import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput' +import { + LinkDirection, + RenderShape +} from '@/lib/litegraph/src/types/globalEnums' -import { LabelPosition, SlotShape, SlotType } from "@/lib/litegraph/src/draw" -import { LiteGraph, Rectangle } from "@/lib/litegraph/src/litegraph" -import { getCentre } from "@/lib/litegraph/src/measure" -import { LinkDirection, RenderShape } from "@/lib/litegraph/src/types/globalEnums" - -import { NodeInputSlot } from "./NodeInputSlot" -import { SlotBase } from "./SlotBase" +import { NodeInputSlot } from './NodeInputSlot' +import { SlotBase } from './SlotBase' export interface IDrawOptions { colorContext: DefaultConnectionColors @@ -35,7 +47,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot { boundingRect[0] - nodePos[0], boundingRect[1] - nodePos[1], diameter, - diameter, + diameter ]) } @@ -48,17 +60,30 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot { } get highlightColor(): CanvasColour { - return LiteGraph.NODE_TEXT_HIGHLIGHT_COLOR ?? LiteGraph.NODE_SELECTED_TITLE_COLOR ?? LiteGraph.NODE_TEXT_COLOR + return ( + LiteGraph.NODE_TEXT_HIGHLIGHT_COLOR ?? + LiteGraph.NODE_SELECTED_TITLE_COLOR ?? + LiteGraph.NODE_TEXT_COLOR + ) } abstract get isWidgetInputSlot(): boolean - constructor(slot: OptionalProps, node: LGraphNode) { + constructor( + slot: OptionalProps, + node: LGraphNode + ) { // Workaround: Ensure internal properties are not copied to the slot (_listenerController // https://github.com/Comfy-Org/litegraph.js/issues/1138 - const maybeSubgraphSlot: OptionalProps = slot - const { boundingRect, name, type, _listenerController, ...rest } = maybeSubgraphSlot - const rectangle = boundingRect ? Rectangle.ensureRect(boundingRect) : new Rectangle() + const maybeSubgraphSlot: OptionalProps< + ISubgraphInput, + 'link' | 'boundingRect' + > = slot + const { boundingRect, name, type, _listenerController, ...rest } = + maybeSubgraphSlot + const rectangle = boundingRect + ? Rectangle.ensureRect(boundingRect) + : new Rectangle() super(name, type, rectangle) @@ -70,13 +95,15 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot { * Whether this slot is a valid target for a dragging link. * @param fromSlot The slot that the link is being connected from. */ - abstract isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput): boolean + abstract isValidTarget( + fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput + ): boolean /** * The label to display in the UI. */ get renderingLabel(): string { - return this.label || this.localized_name || this.name || "" + return this.label || this.localized_name || this.name || '' } draw( @@ -86,8 +113,8 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot { labelPosition = LabelPosition.Right, lowQuality = false, highlight = false, - doStroke = false, - }: IDrawOptions, + doStroke = false + }: IDrawOptions ) { // Save the current fillStyle and strokeStyle const originalFillStyle = ctx.fillStyle @@ -127,7 +154,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot { pos[0] - 4 + x * spacing, pos[1] - 4 + y * spacing, cellSize, - cellSize, + cellSize ) } } @@ -182,7 +209,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot { // Draw a red circle if the slot has errors. if (this.hasErrors) { ctx.lineWidth = 2 - ctx.strokeStyle = "red" + ctx.strokeStyle = 'red' ctx.beginPath() ctx.arc(pos[0], pos[1], 12, 0, Math.PI * 2) ctx.stroke() @@ -200,7 +227,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot { // Save original styles const { fillStyle } = ctx - ctx.fillStyle = "#686" + ctx.fillStyle = '#686' ctx.beginPath() if (this.type === SlotType.Event || this.shape === RenderShape.BOX) { diff --git a/src/lib/litegraph/src/node/SlotBase.ts b/src/lib/litegraph/src/node/SlotBase.ts index df24090f99..c6facdc168 100644 --- a/src/lib/litegraph/src/node/SlotBase.ts +++ b/src/lib/litegraph/src/node/SlotBase.ts @@ -1,9 +1,15 @@ -import type { CanvasColour, DefaultConnectionColors, INodeSlot, ISlotType, IWidgetLocator, Point } from "@/lib/litegraph/src/interfaces" -import type { LLink } from "@/lib/litegraph/src/LLink" -import type { RenderShape } from "@/lib/litegraph/src/types/globalEnums" -import type { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" - -import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" +import type { LLink } from '@/lib/litegraph/src/LLink' +import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' +import type { + CanvasColour, + DefaultConnectionColors, + INodeSlot, + ISlotType, + IWidgetLocator, + Point +} from '@/lib/litegraph/src/interfaces' +import type { RenderShape } from '@/lib/litegraph/src/types/globalEnums' +import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' /** Base class for all input & output slots. */ diff --git a/src/lib/litegraph/src/node/slotUtils.ts b/src/lib/litegraph/src/node/slotUtils.ts index a4b805a006..3d97ca1427 100644 --- a/src/lib/litegraph/src/node/slotUtils.ts +++ b/src/lib/litegraph/src/node/slotUtils.ts @@ -1,15 +1,57 @@ -import type { IWidgetInputSlot, SharedIntersection } from "@/lib/litegraph/src/interfaces" -import type { INodeInputSlot, INodeOutputSlot, INodeSlot, IWidget } from "@/lib/litegraph/src/litegraph" -import type { ISerialisableNodeInput, ISerialisableNodeOutput } from "@/lib/litegraph/src/types/serialisation" +import type { + IWidgetInputSlot, + SharedIntersection +} from '@/lib/litegraph/src/interfaces' +import type { + INodeInputSlot, + INodeOutputSlot, + INodeSlot, + IWidget +} from '@/lib/litegraph/src/litegraph' +import type { + ISerialisableNodeInput, + ISerialisableNodeOutput +} from '@/lib/litegraph/src/types/serialisation' -type CommonIoSlotProps = SharedIntersection +type CommonIoSlotProps = SharedIntersection< + ISerialisableNodeInput, + ISerialisableNodeOutput +> -export function shallowCloneCommonProps(slot: CommonIoSlotProps): CommonIoSlotProps { - const { color_off, color_on, dir, label, localized_name, locked, name, nameLocked, removable, shape, type } = slot - return { color_off, color_on, dir, label, localized_name, locked, name, nameLocked, removable, shape, type } +export function shallowCloneCommonProps( + slot: CommonIoSlotProps +): CommonIoSlotProps { + const { + color_off, + color_on, + dir, + label, + localized_name, + locked, + name, + nameLocked, + removable, + shape, + type + } = slot + return { + color_off, + color_on, + dir, + label, + localized_name, + locked, + name, + nameLocked, + removable, + shape, + type + } } -export function inputAsSerialisable(slot: INodeInputSlot): ISerialisableNodeInput { +export function inputAsSerialisable( + slot: INodeInputSlot +): ISerialisableNodeInput { const { link } = slot const widgetOrPos = slot.widget ? { widget: { name: slot.widget.name } } @@ -18,32 +60,32 @@ export function inputAsSerialisable(slot: INodeInputSlot): ISerialisableNodeInpu return { ...shallowCloneCommonProps(slot), ...widgetOrPos, - link, + link } } -export function outputAsSerialisable(slot: INodeOutputSlot & { widget?: IWidget }): ISerialisableNodeOutput { +export function outputAsSerialisable( + slot: INodeOutputSlot & { widget?: IWidget } +): ISerialisableNodeOutput { const { pos, slot_index, links, widget } = slot // Output widgets do not exist in Litegraph; this is a temporary downstream workaround. - const outputWidget = widget - ? { widget: { name: widget.name } } - : null + const outputWidget = widget ? { widget: { name: widget.name } } : null return { ...shallowCloneCommonProps(slot), ...outputWidget, pos, slot_index, - links, + links } } export function isINodeInputSlot(slot: INodeSlot): slot is INodeInputSlot { - return "link" in slot + return 'link' in slot } export function isINodeOutputSlot(slot: INodeSlot): slot is INodeOutputSlot { - return "links" in slot + return 'links' in slot } /** @@ -51,6 +93,8 @@ export function isINodeOutputSlot(slot: INodeSlot): slot is INodeOutputSlot { * @param slot The slot to check. */ -export function isWidgetInputSlot(slot: INodeInputSlot): slot is IWidgetInputSlot { +export function isWidgetInputSlot( + slot: INodeInputSlot +): slot is IWidgetInputSlot { return !!slot.widget } diff --git a/src/lib/litegraph/src/polyfills.ts b/src/lib/litegraph/src/polyfills.ts index 73a1c67106..488e247883 100644 --- a/src/lib/litegraph/src/polyfills.ts +++ b/src/lib/litegraph/src/polyfills.ts @@ -1,13 +1,13 @@ // @ts-expect-error Polyfill -Symbol.dispose ??= Symbol("Symbol.dispose") +Symbol.dispose ??= Symbol('Symbol.dispose') // @ts-expect-error Polyfill -Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose") +Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose') // API ************************************************* // like rect but rounded corners export function loadPolyfills() { if ( - typeof window != "undefined" && + typeof window != 'undefined' && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect ) { @@ -18,7 +18,7 @@ export function loadPolyfills() { w: number, h: number, radius: number | number[], - radius_low: number | number[], + radius_low: number | number[] ) { let top_left_radius = 0 let top_right_radius = 0 @@ -35,7 +35,11 @@ export function loadPolyfills() { // make it compatible with official one if (Array.isArray(radius)) { if (radius.length == 1) { - top_left_radius = top_right_radius = bottom_left_radius = bottom_right_radius = radius[0] + top_left_radius = + top_right_radius = + bottom_left_radius = + bottom_right_radius = + radius[0] } else if (radius.length == 2) { top_left_radius = bottom_right_radius = radius[0] top_right_radius = bottom_left_radius = radius[1] @@ -64,12 +68,7 @@ export function loadPolyfills() { // bottom right this.lineTo(x + w, y + h - bottom_right_radius) - this.quadraticCurveTo( - x + w, - y + h, - x + w - bottom_right_radius, - y + h, - ) + this.quadraticCurveTo(x + w, y + h, x + w - bottom_right_radius, y + h) // bottom left this.lineTo(x + bottom_right_radius, y + h) @@ -81,10 +80,12 @@ export function loadPolyfills() { } } - if (typeof window != "undefined" && !window["requestAnimationFrame"]) { + if (typeof window != 'undefined' && !window['requestAnimationFrame']) { window.requestAnimationFrame = // @ts-expect-error Legacy code - window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + // @ts-expect-error Legacy code + window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60) } diff --git a/src/lib/litegraph/src/strings.ts b/src/lib/litegraph/src/strings.ts index e5481e604a..d643cfb21d 100644 --- a/src/lib/litegraph/src/strings.ts +++ b/src/lib/litegraph/src/strings.ts @@ -1,4 +1,4 @@ -import type { ISlotType } from "./litegraph" +import type { ISlotType } from './litegraph' /** * Uses the standard String() function to coerce to string, unless the value is null or undefined - then null. @@ -15,11 +15,13 @@ export function stringOrNull(value: unknown): string | null { * @returns String(value) or "" */ export function stringOrEmpty(value: unknown): string { - return value == null ? "" : String(value) + return value == null ? '' : String(value) } export function parseSlotTypes(type: ISlotType): string[] { - return type == "" || type == "0" ? ["*"] : String(type).toLowerCase().split(",") + return type == '' || type == '0' + ? ['*'] + : String(type).toLowerCase().split(',') } /** @@ -29,7 +31,10 @@ export function parseSlotTypes(type: ISlotType): string[] { * @param existingNames The names that already exist. Default: an empty array * @returns The name, or a unique name if it already exists. */ -export function nextUniqueName(name: string, existingNames: string[] = []): string { +export function nextUniqueName( + name: string, + existingNames: string[] = [] +): string { let i = 1 const baseName = name while (existingNames.includes(name)) { diff --git a/src/lib/litegraph/src/subgraph/EmptySubgraphInput.ts b/src/lib/litegraph/src/subgraph/EmptySubgraphInput.ts index a84e173233..0419b18d3a 100644 --- a/src/lib/litegraph/src/subgraph/EmptySubgraphInput.ts +++ b/src/lib/litegraph/src/subgraph/EmptySubgraphInput.ts @@ -1,13 +1,12 @@ -import type { SubgraphInputNode } from "./SubgraphInputNode" -import type { INodeInputSlot, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { RerouteId } from "@/lib/litegraph/src/Reroute" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LLink } from '@/lib/litegraph/src/LLink' +import type { RerouteId } from '@/lib/litegraph/src/Reroute' +import type { INodeInputSlot, Point } from '@/lib/litegraph/src/interfaces' +import { nextUniqueName } from '@/lib/litegraph/src/strings' +import { zeroUuid } from '@/lib/litegraph/src/utils/uuid' -import { LLink } from "@/lib/litegraph/src/LLink" -import { nextUniqueName } from "@/lib/litegraph/src/strings" -import { zeroUuid } from "@/lib/litegraph/src/utils/uuid" - -import { SubgraphInput } from "./SubgraphInput" +import { SubgraphInput } from './SubgraphInput' +import type { SubgraphInputNode } from './SubgraphInputNode' /** * A virtual slot that simply creates a new input slot when connected to. @@ -16,16 +15,23 @@ export class EmptySubgraphInput extends SubgraphInput { declare parent: SubgraphInputNode constructor(parent: SubgraphInputNode) { - super({ - id: zeroUuid, - name: "", - type: "", - }, parent) + super( + { + id: zeroUuid, + name: '', + type: '' + }, + parent + ) } - override connect(slot: INodeInputSlot, node: LGraphNode, afterRerouteId?: RerouteId): LLink | undefined { + override connect( + slot: INodeInputSlot, + node: LGraphNode, + afterRerouteId?: RerouteId + ): LLink | undefined { const { subgraph } = this.parent - const existingNames = subgraph.inputs.map(x => x.name) + const existingNames = subgraph.inputs.map((x) => x.name) const name = nextUniqueName(slot.name, existingNames) const input = subgraph.addInput(name, String(slot.type)) diff --git a/src/lib/litegraph/src/subgraph/EmptySubgraphOutput.ts b/src/lib/litegraph/src/subgraph/EmptySubgraphOutput.ts index baafed0ffb..afb52b1209 100644 --- a/src/lib/litegraph/src/subgraph/EmptySubgraphOutput.ts +++ b/src/lib/litegraph/src/subgraph/EmptySubgraphOutput.ts @@ -1,13 +1,12 @@ -import type { SubgraphOutputNode } from "./SubgraphOutputNode" -import type { INodeOutputSlot, Point } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { RerouteId } from "@/lib/litegraph/src/Reroute" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LLink } from '@/lib/litegraph/src/LLink' +import type { RerouteId } from '@/lib/litegraph/src/Reroute' +import type { INodeOutputSlot, Point } from '@/lib/litegraph/src/interfaces' +import { nextUniqueName } from '@/lib/litegraph/src/strings' +import { zeroUuid } from '@/lib/litegraph/src/utils/uuid' -import { LLink } from "@/lib/litegraph/src/LLink" -import { nextUniqueName } from "@/lib/litegraph/src/strings" -import { zeroUuid } from "@/lib/litegraph/src/utils/uuid" - -import { SubgraphOutput } from "./SubgraphOutput" +import { SubgraphOutput } from './SubgraphOutput' +import type { SubgraphOutputNode } from './SubgraphOutputNode' /** * A virtual slot that simply creates a new output slot when connected to. @@ -16,16 +15,23 @@ export class EmptySubgraphOutput extends SubgraphOutput { declare parent: SubgraphOutputNode constructor(parent: SubgraphOutputNode) { - super({ - id: zeroUuid, - name: "", - type: "", - }, parent) + super( + { + id: zeroUuid, + name: '', + type: '' + }, + parent + ) } - override connect(slot: INodeOutputSlot, node: LGraphNode, afterRerouteId?: RerouteId): LLink | undefined { + override connect( + slot: INodeOutputSlot, + node: LGraphNode, + afterRerouteId?: RerouteId + ): LLink | undefined { const { subgraph } = this.parent - const existingNames = subgraph.outputs.map(x => x.name) + const existingNames = subgraph.outputs.map((x) => x.name) const name = nextUniqueName(slot.name, existingNames) const output = subgraph.addOutput(name, String(slot.type)) diff --git a/src/lib/litegraph/src/subgraph/ExecutableNodeDTO.ts b/src/lib/litegraph/src/subgraph/ExecutableNodeDTO.ts index 685859ba37..a5c5b7087a 100644 --- a/src/lib/litegraph/src/subgraph/ExecutableNodeDTO.ts +++ b/src/lib/litegraph/src/subgraph/ExecutableNodeDTO.ts @@ -1,22 +1,28 @@ -import type { SubgraphNode } from "./SubgraphNode" -import type { CallbackParams, CallbackReturn, ISlotType } from "@/lib/litegraph/src/interfaces" -import type { LGraph } from "@/lib/litegraph/src/LGraph" -import type { LGraphNode, NodeId } from "@/lib/litegraph/src/LGraphNode" +import type { LGraph } from '@/lib/litegraph/src/LGraph' +import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode' +import { InvalidLinkError } from '@/lib/litegraph/src/infrastructure/InvalidLinkError' +import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError' +import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError' +import { SlotIndexError } from '@/lib/litegraph/src/infrastructure/SlotIndexError' +import type { + CallbackParams, + CallbackReturn, + ISlotType +} from '@/lib/litegraph/src/interfaces' +import { LGraphEventMode } from '@/lib/litegraph/src/litegraph' -import { InvalidLinkError } from "@/lib/litegraph/src/infrastructure/InvalidLinkError" -import { NullGraphError } from "@/lib/litegraph/src/infrastructure/NullGraphError" -import { RecursionError } from "@/lib/litegraph/src/infrastructure/RecursionError" -import { SlotIndexError } from "@/lib/litegraph/src/infrastructure/SlotIndexError" -import { LGraphEventMode } from "@/lib/litegraph/src/litegraph" - -import { Subgraph } from "./Subgraph" +import { Subgraph } from './Subgraph' +import type { SubgraphNode } from './SubgraphNode' export type ExecutionId = string /** * Interface describing the data transfer objects used when compiling a graph for execution. */ -export type ExecutableLGraphNode = Omit +export type ExecutableLGraphNode = Omit< + ExecutableNodeDTO, + 'graph' | 'node' | 'subgraphNode' +> /** * The end result of resolving a DTO input. @@ -38,12 +44,14 @@ type ResolvedInput = { * @remarks This is the class that is used to create the data transfer objects for executable nodes. */ export class ExecutableNodeDTO implements ExecutableLGraphNode { - applyToGraph?(...args: CallbackParams): CallbackReturn + applyToGraph?( + ...args: CallbackParams + ): CallbackReturn /** The graph that this node is a part of. */ readonly graph: LGraph | Subgraph - inputs: { linkId: number | null, name: string, type: ISlotType }[] + inputs: { linkId: number | null; name: string; type: ISlotType }[] /** Backing field for {@link id}. */ #id: ExecutionId @@ -97,17 +105,17 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { /** A flattened map of all DTOs in this node network. Subgraph instances have been expanded into their inner nodes. */ readonly nodesByExecutionId: Map, /** The actual subgraph instance that contains this node, otherise undefined. */ - readonly subgraphNode?: SubgraphNode, + readonly subgraphNode?: SubgraphNode ) { 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 => ({ + this.inputs = this.node.inputs.map((x) => ({ linkId: x.link, name: x.name, - type: x.type, + type: x.type })) // Only create a wrapper if the node has an applyToGraph method @@ -118,7 +126,12 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { /** Returns either the DTO itself, or the DTOs of the inner nodes of the subgraph. */ getInnerNodes(): ExecutableLGraphNode[] { - return this.subgraphNode ? this.subgraphNode.getInnerNodes(this.nodesByExecutionId, this.subgraphNodePath) : [this] + return this.subgraphNode + ? this.subgraphNode.getInnerNodes( + this.nodesByExecutionId, + this.subgraphNodePath + ) + : [this] } /** @@ -129,33 +142,48 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { * If overriding, ensure that the set is passed on all recursive calls. * @returns The node and the origin ID / slot index of the output. */ - resolveInput(slot: number, visited = new Set()): ResolvedInput | undefined { + resolveInput( + slot: number, + visited = new Set() + ): ResolvedInput | undefined { const uniqueId = `${this.subgraphNode?.subgraph.id}:${this.node.id}[I]${slot}` if (visited.has(uniqueId)) { - const nodeInfo = `${this.node.id}${this.node.title ? ` (${this.node.title})` : ""}` - const pathInfo = this.subgraphNodePath.length > 0 ? ` at path ${this.subgraphNodePath.join(":")}` : "" + const nodeInfo = `${this.node.id}${this.node.title ? ` (${this.node.title})` : ''}` + const pathInfo = + this.subgraphNodePath.length > 0 + ? ` at path ${this.subgraphNodePath.join(':')}` + : '' throw new RecursionError( `Circular reference detected while resolving input ${slot} of node ${nodeInfo}${pathInfo}. ` + - `This creates an infinite loop in link resolution. UniqueID: [${uniqueId}]`, + `This creates an infinite loop in link resolution. UniqueID: [${uniqueId}]` ) } visited.add(uniqueId) const input = this.inputs.at(slot) - if (!input) throw new SlotIndexError(`No input found for flattened id [${this.id}] slot [${slot}]`) + if (!input) + throw new SlotIndexError( + `No input found for flattened id [${this.id}] slot [${slot}]` + ) // Nothing connected if (input.linkId == null) return const link = this.graph.getLink(input.linkId) - if (!link) throw new InvalidLinkError(`No link found in parent graph for id [${this.id}] slot [${slot}] ${input.name}`) + if (!link) + throw new InvalidLinkError( + `No link found in parent graph for id [${this.id}] slot [${slot}] ${input.name}` + ) const { subgraphNode } = this // Link goes up and out of this subgraph if (subgraphNode && link.originIsIoNode) { const subgraphNodeInput = subgraphNode.inputs.at(link.origin_slot) - if (!subgraphNodeInput) throw new SlotIndexError(`No input found for slot [${link.origin_slot}] ${input.name}`) + if (!subgraphNodeInput) + throw new SlotIndexError( + `No input found for slot [${link.origin_slot}] ${input.name}` + ) // Nothing connected const linkId = subgraphNodeInput.link @@ -168,27 +196,44 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { node: this, origin_id: this.id, origin_slot: -1, - widgetInfo: { value: widget.value }, + widgetInfo: { value: widget.value } } } const outerLink = subgraphNode.graph.getLink(linkId) - if (!outerLink) throw new InvalidLinkError(`No outer link found for slot [${link.origin_slot}] ${input.name}`) + if (!outerLink) + throw new InvalidLinkError( + `No outer link found for slot [${link.origin_slot}] ${input.name}` + ) - const subgraphNodeExecutionId = this.subgraphNodePath.join(":") - const subgraphNodeDto = this.nodesByExecutionId.get(subgraphNodeExecutionId) - if (!subgraphNodeDto) throw new Error(`No subgraph node DTO found for id [${subgraphNodeExecutionId}]`) + const subgraphNodeExecutionId = this.subgraphNodePath.join(':') + const subgraphNodeDto = this.nodesByExecutionId.get( + subgraphNodeExecutionId + ) + if (!subgraphNodeDto) + throw new Error( + `No subgraph node DTO found for id [${subgraphNodeExecutionId}]` + ) return subgraphNodeDto.resolveInput(outerLink.target_slot, visited) } // Not part of a subgraph; use the original link const outputNode = this.graph.getNodeById(link.origin_id) - if (!outputNode) throw new InvalidLinkError(`No input node found for id [${this.id}] slot [${slot}] ${input.name}`) + if (!outputNode) + throw new InvalidLinkError( + `No input node found for id [${this.id}] slot [${slot}] ${input.name}` + ) - const outputNodeExecutionId = [...this.subgraphNodePath, outputNode.id].join(":") + const outputNodeExecutionId = [ + ...this.subgraphNodePath, + outputNode.id + ].join(':') const outputNodeDto = this.nodesByExecutionId.get(outputNodeExecutionId) - if (!outputNodeDto) throw new Error(`No output node DTO found for id [${outputNodeExecutionId}]`) + if (!outputNodeDto) + throw new Error( + `No output node DTO found for id [${outputNodeExecutionId}]` + ) return outputNodeDto.resolveOutput(link.origin_slot, input.type, visited) } @@ -200,14 +245,21 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { * @param visited A set of unique IDs to guard against infinite recursion. See {@link resolveInput}. * @returns The node and the origin ID / slot index of the output. */ - resolveOutput(slot: number, type: ISlotType, visited: Set): ResolvedInput | undefined { + resolveOutput( + slot: number, + type: ISlotType, + visited: Set + ): ResolvedInput | undefined { const uniqueId = `${this.subgraphNode?.subgraph.id}:${this.node.id}[O]${slot}` if (visited.has(uniqueId)) { - const nodeInfo = `${this.node.id}${this.node.title ? ` (${this.node.title})` : ""}` - const pathInfo = this.subgraphNodePath.length > 0 ? ` at path ${this.subgraphNodePath.join(":")}` : "" + const nodeInfo = `${this.node.id}${this.node.title ? ` (${this.node.title})` : ''}` + const pathInfo = + this.subgraphNodePath.length > 0 + ? ` at path ${this.subgraphNodePath.join(':')}` + : '' throw new RecursionError( `Circular reference detected while resolving output ${slot} of node ${nodeInfo}${pathInfo}. ` + - `This creates an infinite loop in link resolution. UniqueID: [${uniqueId}]`, + `This creates an infinite loop in link resolution. UniqueID: [${uniqueId}]` ) } visited.add(uniqueId) @@ -220,11 +272,14 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { const parentInputIndexes = Object.keys(inputs).map(Number) // Prioritise exact slot index const indexes = [slot, ...parentInputIndexes] - const matchingIndex = indexes.find(i => inputs[i]?.type === type) + const matchingIndex = indexes.find((i) => inputs[i]?.type === type) // No input types match if (matchingIndex === undefined) { - console.debug(`[ExecutableNodeDTO.resolveOutput] No input types match type [${type}] for id [${this.id}] slot [${slot}]`, this) + console.debug( + `[ExecutableNodeDTO.resolveOutput] No input types match type [${type}] for id [${this.id}] slot [${slot}]`, + this + ) return } @@ -232,7 +287,8 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { } const { node } = this - if (node.isSubgraphNode()) return this.#resolveSubgraphOutput(slot, type, visited) + if (node.isSubgraphNode()) + return this.#resolveSubgraphOutput(slot, type, visited) // Upstreamed: Other virtual nodes are bypassed using the same input/output index (slots must match) if (node.isVirtualNode) { @@ -242,13 +298,24 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { const virtualLink = this.node.getInputLink(slot) if (virtualLink) { const outputNode = this.graph.getNodeById(virtualLink.origin_id) - if (!outputNode) throw new InvalidLinkError(`Virtual node failed to resolve parent [${this.id}] slot [${slot}]`) + if (!outputNode) + throw new InvalidLinkError( + `Virtual node failed to resolve parent [${this.id}] slot [${slot}]` + ) - const outputNodeExecutionId = [...this.subgraphNodePath, outputNode.id].join(":") + const outputNodeExecutionId = [ + ...this.subgraphNodePath, + outputNode.id + ].join(':') const outputNodeDto = this.nodesByExecutionId.get(outputNodeExecutionId) - if (!outputNodeDto) throw new Error(`No output node DTO found for id [${outputNode.id}]`) + if (!outputNodeDto) + throw new Error(`No output node DTO found for id [${outputNode.id}]`) - return outputNodeDto.resolveOutput(virtualLink.origin_slot, type, visited) + return outputNodeDto.resolveOutput( + virtualLink.origin_slot, + type, + visited + ) } // Virtual nodes without a matching input should be discarded. @@ -258,7 +325,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode { return { node: this, origin_id: this.id, - origin_slot: slot, + origin_slot: slot } } @@ -268,25 +335,47 @@ 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(slot: number, type: ISlotType, visited: Set): ResolvedInput | undefined { + #resolveSubgraphOutput( + slot: number, + type: ISlotType, + visited: Set + ): ResolvedInput | undefined { const { node } = this const output = node.outputs.at(slot) - if (!output) throw new SlotIndexError(`No output found for flattened id [${this.id}] slot [${slot}]`) - if (!node.isSubgraphNode()) throw new TypeError(`Node is not a subgraph node: ${node.id}`) + if (!output) + throw new SlotIndexError( + `No output found for flattened id [${this.id}] slot [${slot}]` + ) + if (!node.isSubgraphNode()) + throw new TypeError(`Node is not a subgraph node: ${node.id}`) // Link inside the subgraph const innerResolved = node.resolveSubgraphOutputLink(slot) if (!innerResolved) return const innerNode = innerResolved.outputNode - if (!innerNode) throw new Error(`No output node found for id [${this.id}] slot [${slot}] ${output.name}`) + if (!innerNode) + throw new Error( + `No output node found for id [${this.id}] slot [${slot}] ${output.name}` + ) // Recurse into the subgraph - const innerNodeExecutionId = [...this.subgraphNodePath, node.id, innerNode.id].join(":") + const innerNodeExecutionId = [ + ...this.subgraphNodePath, + node.id, + innerNode.id + ].join(':') const innerNodeDto = this.nodesByExecutionId.get(innerNodeExecutionId) - if (!innerNodeDto) throw new Error(`No inner node DTO found for id [${innerNodeExecutionId}]`) + if (!innerNodeDto) + throw new Error( + `No inner node DTO found for id [${innerNodeExecutionId}]` + ) - return innerNodeDto.resolveOutput(innerResolved.link.origin_slot, type, visited) + return innerNodeDto.resolveOutput( + innerResolved.link.origin_slot, + type, + visited + ) } } diff --git a/src/lib/litegraph/src/subgraph/Subgraph.ts b/src/lib/litegraph/src/subgraph/Subgraph.ts index ba57aea6b4..e1f2f0b255 100644 --- a/src/lib/litegraph/src/subgraph/Subgraph.ts +++ b/src/lib/litegraph/src/subgraph/Subgraph.ts @@ -1,3 +1,3 @@ // Re-export Subgraph and GraphOrSubgraph from LGraph.ts to maintain compatibility // This is a temporary fix to resolve circular dependency issues -export { Subgraph, type GraphOrSubgraph } from "@/lib/litegraph/src/LGraph" \ No newline at end of file +export { Subgraph, type GraphOrSubgraph } from '@/lib/litegraph/src/LGraph' diff --git a/src/lib/litegraph/src/subgraph/SubgraphIONodeBase.ts b/src/lib/litegraph/src/subgraph/SubgraphIONodeBase.ts index 3dec40d101..856136b0f6 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphIONodeBase.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphIONodeBase.ts @@ -1,19 +1,39 @@ -import type { EmptySubgraphInput } from "./EmptySubgraphInput" -import type { EmptySubgraphOutput } from "./EmptySubgraphOutput" -import type { Subgraph } from "./Subgraph" -import type { SubgraphInput } from "./SubgraphInput" -import type { SubgraphOutput } from "./SubgraphOutput" -import type { LinkConnector } from "@/lib/litegraph/src/canvas/LinkConnector" -import type { DefaultConnectionColors, Hoverable, INodeInputSlot, INodeOutputSlot, Point, Positionable } from "@/lib/litegraph/src/interfaces" -import type { NodeId } from "@/lib/litegraph/src/LGraphNode" -import type { ExportedSubgraphIONode, Serialisable } from "@/lib/litegraph/src/types/serialisation" +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' +import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector' +import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' +import type { + DefaultConnectionColors, + Hoverable, + INodeInputSlot, + INodeOutputSlot, + Point, + Positionable +} from '@/lib/litegraph/src/interfaces' +import { + type CanvasColour, + type CanvasPointer, + type CanvasPointerEvent, + type IContextMenuValue, + LiteGraph +} from '@/lib/litegraph/src/litegraph' +import { snapPoint } from '@/lib/litegraph/src/measure' +import { CanvasItem } from '@/lib/litegraph/src/types/globalEnums' +import type { + ExportedSubgraphIONode, + Serialisable +} from '@/lib/litegraph/src/types/serialisation' -import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" -import { type CanvasColour, type CanvasPointer, type CanvasPointerEvent, type IContextMenuValue, LiteGraph } from "@/lib/litegraph/src/litegraph" -import { snapPoint } from "@/lib/litegraph/src/measure" -import { CanvasItem } from "@/lib/litegraph/src/types/globalEnums" +import type { EmptySubgraphInput } from './EmptySubgraphInput' +import type { EmptySubgraphOutput } from './EmptySubgraphOutput' +import type { Subgraph } from './Subgraph' +import type { SubgraphInput } from './SubgraphInput' +import type { SubgraphOutput } from './SubgraphOutput' -export abstract class SubgraphIONodeBase implements Positionable, Hoverable, Serialisable { +export abstract class SubgraphIONodeBase< + TSlot extends SubgraphInput | SubgraphOutput + > + implements Positionable, Hoverable, Serialisable +{ static margin = 10 static minWidth = 100 static roundedRadius = 10 @@ -55,7 +75,7 @@ export abstract class SubgraphIONodeBase 0)) return - new LiteGraph.ContextMenu( - options, - { - event: event as any, - title: slot.name || "Subgraph Output", - callback: (item: IContextMenuValue) => { - this.#onSlotMenuAction(item, slot, event) - }, - }, - ) + new LiteGraph.ContextMenu(options, { + event: event as any, + title: slot.name || 'Subgraph Output', + callback: (item: IContextMenuValue) => { + this.#onSlotMenuAction(item, slot, event) + } + }) } /** @@ -175,14 +198,14 @@ export abstract class SubgraphIONodeBase 0) { - options.push({ content: "Disconnect Links", value: "disconnect" }) + options.push({ content: 'Disconnect Links', value: 'disconnect' }) } // Remove / rename slot option (except for the empty slot) if (slot !== this.emptySlot) { options.push( - { content: "Remove Slot", value: "remove" }, - { content: "Rename Slot", value: "rename" }, + { content: 'Remove Slot', value: 'remove' }, + { content: 'Rename Slot', value: 'rename' } ) } @@ -195,33 +218,39 @@ export abstract class SubgraphIONodeBase c.prompt( - "Slot name", - slot.name, - (newName: string) => { - if (newName) this.renameSlot(slot, newName) - }, - event, - )) - } - break + // Rename the slot + case 'rename': + if (slot !== this.emptySlot) { + this.subgraph.canvasAction((c) => + c.prompt( + 'Slot name', + slot.name, + (newName: string) => { + if (newName) this.renameSlot(slot, newName) + }, + event + ) + ) + } + break } this.subgraph.setDirtyCanvas(true) @@ -249,20 +278,53 @@ export abstract class SubgraphIONodeBase w.name === widgetNamePojo.name) + const widget = resolved.inputNode.widgets.find( + (w) => w.name === widgetNamePojo.name + ) if (!widget) { - console.warn("Widget not found", widgetNamePojo) + console.warn('Widget not found', widgetNamePojo) continue } widgets.push(widget) } else { - console.debug("No input found on link id", linkId, link) + console.debug('No input found on link id', linkId, link) } } return widgets @@ -198,7 +209,7 @@ export class SubgraphInput extends SubgraphSlot { override disconnect(): void { super.disconnect() - this.events.dispatch("input-disconnected", { input: this }) + this.events.dispatch('input-disconnected', { input: this }) } /** For inputs, x is the right edge of the input node. */ @@ -220,9 +231,14 @@ export class SubgraphInput extends SubgraphSlot { * For SubgraphInput (which acts as an output inside the subgraph), * the fromSlot should be an input slot. */ - override isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput): boolean { + override isValidTarget( + fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput + ): boolean { if (isNodeSlot(fromSlot)) { - return "link" in fromSlot && LiteGraph.isValidConnection(this.type, fromSlot.type) + return ( + 'link' in fromSlot && + LiteGraph.isValidConnection(this.type, fromSlot.type) + ) } if (isSubgraphOutput(fromSlot)) { diff --git a/src/lib/litegraph/src/subgraph/SubgraphInputNode.ts b/src/lib/litegraph/src/subgraph/SubgraphInputNode.ts index 30bbdea5f4..5bdfd6fa35 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphInputNode.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphInputNode.ts @@ -1,23 +1,31 @@ -import type { SubgraphInput } from "./SubgraphInput" -import type { SubgraphOutput } from "./SubgraphOutput" -import type { LinkConnector } from "@/lib/litegraph/src/canvas/LinkConnector" -import type { CanvasPointer } from "@/lib/litegraph/src/CanvasPointer" -import type { DefaultConnectionColors, INodeInputSlot, INodeOutputSlot, ISlotType, Positionable } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode, NodeId } from "@/lib/litegraph/src/LGraphNode" -import type { RerouteId } from "@/lib/litegraph/src/Reroute" -import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events" -import type { NodeLike } from "@/lib/litegraph/src/types/NodeLike" +import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer' +import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode' +import { LLink } from '@/lib/litegraph/src/LLink' +import type { RerouteId } from '@/lib/litegraph/src/Reroute' +import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector' +import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/constants' +import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' +import type { + DefaultConnectionColors, + INodeInputSlot, + INodeOutputSlot, + ISlotType, + Positionable +} from '@/lib/litegraph/src/interfaces' +import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' +import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums' +import { findFreeSlotOfType } from '@/lib/litegraph/src/utils/collections' -import { SUBGRAPH_INPUT_ID } from "@/lib/litegraph/src/constants" -import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" -import { LLink } from "@/lib/litegraph/src/LLink" -import { NodeSlotType } from "@/lib/litegraph/src/types/globalEnums" -import { findFreeSlotOfType } from "@/lib/litegraph/src/utils/collections" +import { EmptySubgraphInput } from './EmptySubgraphInput' +import { SubgraphIONodeBase } from './SubgraphIONodeBase' +import type { SubgraphInput } from './SubgraphInput' +import type { SubgraphOutput } from './SubgraphOutput' -import { EmptySubgraphInput } from "./EmptySubgraphInput" -import { SubgraphIONodeBase } from "./SubgraphIONodeBase" - -export class SubgraphInputNode extends SubgraphIONodeBase implements Positionable { +export class SubgraphInputNode + extends SubgraphIONodeBase + implements Positionable +{ readonly id: NodeId = SUBGRAPH_INPUT_ID readonly emptySlot: EmptySubgraphInput = new EmptySubgraphInput(this) @@ -35,11 +43,18 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme return x + width - SubgraphIONodeBase.roundedRadius } - override onPointerDown(e: CanvasPointerEvent, pointer: CanvasPointer, linkConnector: LinkConnector): void { + override onPointerDown( + e: CanvasPointerEvent, + pointer: CanvasPointer, + linkConnector: LinkConnector + ): void { // Left-click handling for dragging connections if (e.button === 0) { for (const slot of this.allSlots) { - const slotBounds = Rectangle.fromCentre(slot.pos, slot.boundingRect.height) + const slotBounds = Rectangle.fromCentre( + slot.pos, + slot.boundingRect.height + ) if (slotBounds.containsXy(e.canvasX, e.canvasY)) { pointer.onDragStart = () => { @@ -53,7 +68,7 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme } } } - // Check for right-click + // Check for right-click } else if (e.button === 2) { const slot = this.getSlotInPosition(e.canvasX, e.canvasY) if (slot) this.showSlotContextMenu(slot, e) @@ -70,17 +85,27 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme this.subgraph.removeInput(slot) } - canConnectTo(inputNode: NodeLike, input: INodeInputSlot, fromSlot: SubgraphInput): boolean { + canConnectTo( + inputNode: NodeLike, + input: INodeInputSlot, + fromSlot: SubgraphInput + ): boolean { return inputNode.canConnectTo(this, input, fromSlot) } - connectSlots(fromSlot: SubgraphInput, inputNode: LGraphNode, input: INodeInputSlot, afterRerouteId: RerouteId | undefined): LLink { + connectSlots( + fromSlot: SubgraphInput, + inputNode: LGraphNode, + input: INodeInputSlot, + afterRerouteId: RerouteId | undefined + ): LLink { const { subgraph } = this const outputIndex = this.slots.indexOf(fromSlot) const inputIndex = inputNode.inputs.indexOf(input) - if (outputIndex === -1 || inputIndex === -1) throw new Error("Invalid slot indices.") + if (outputIndex === -1 || inputIndex === -1) + throw new Error('Invalid slot indices.') return new LLink( ++subgraph.state.lastLinkId, @@ -89,7 +114,7 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme outputIndex, inputNode.id, inputIndex, - afterRerouteId, + afterRerouteId ) } @@ -99,7 +124,7 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme slot: number, target_node: LGraphNode, target_slotType: ISlotType, - optsIn?: { afterRerouteId?: RerouteId }, + optsIn?: { afterRerouteId?: RerouteId } ): LLink | undefined { const inputSlot = target_node.findInputByType(target_slotType) if (!inputSlot) return @@ -107,29 +132,44 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme if (slot === -1) { // This indicates a connection is being made from the "Empty" slot. // We need to create a new, concrete input on the subgraph that matches the target. - const newSubgraphInput = this.subgraph.addInput(inputSlot.slot.name, String(inputSlot.slot.type ?? "")) + const newSubgraphInput = this.subgraph.addInput( + inputSlot.slot.name, + String(inputSlot.slot.type ?? '') + ) const newSlotIndex = this.slots.indexOf(newSubgraphInput) if (newSlotIndex === -1) { - console.error("Could not find newly created subgraph input slot.") + console.error('Could not find newly created subgraph input slot.') return } slot = newSlotIndex } - return this.slots[slot].connect(inputSlot.slot, target_node, optsIn?.afterRerouteId) + return this.slots[slot].connect( + inputSlot.slot, + target_node, + optsIn?.afterRerouteId + ) } findOutputSlot(name: string): SubgraphInput | undefined { - return this.slots.find(output => output.name === name) + return this.slots.find((output) => output.name === name) } findOutputByType(type: ISlotType): SubgraphInput | undefined { - return findFreeSlotOfType(this.slots, type, slot => slot.linkIds.length > 0)?.slot + return findFreeSlotOfType( + this.slots, + type, + (slot) => slot.linkIds.length > 0 + )?.slot } // #endregion Legacy LGraphNode compatibility - _disconnectNodeInput(node: LGraphNode, input: INodeInputSlot, link: LLink | undefined): void { + _disconnectNodeInput( + node: LGraphNode, + input: INodeInputSlot, + link: LLink | undefined + ): void { const { subgraph } = this // Break floating links @@ -145,12 +185,16 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme if (!link) return const subgraphInputIndex = link.origin_slot - link.disconnect(subgraph, "output") + link.disconnect(subgraph, 'output') subgraph._version++ const subgraphInput = this.slots.at(subgraphInputIndex) if (!subgraphInput) { - console.debug("disconnectNodeInput: subgraphInput not found", this, subgraphInputIndex) + console.debug( + 'disconnectNodeInput: subgraphInput not found', + this, + subgraphInputIndex + ) return } @@ -159,7 +203,10 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme if (index !== -1) { subgraphInput.linkIds.splice(index, 1) } else { - console.debug("disconnectNodeInput: link ID not found in subgraphInput linkIds", link.id) + console.debug( + 'disconnectNodeInput: link ID not found in subgraphInput linkIds', + link.id + ) } node.onConnectionsChange?.( @@ -167,11 +214,20 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme index, false, link, - subgraphInput, + subgraphInput ) } - override drawProtected(ctx: CanvasRenderingContext2D, colorContext: DefaultConnectionColors, fromSlot?: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput, editorAlpha?: number): void { + override drawProtected( + ctx: CanvasRenderingContext2D, + colorContext: DefaultConnectionColors, + fromSlot?: + | INodeInputSlot + | INodeOutputSlot + | SubgraphInput + | SubgraphOutput, + editorAlpha?: number + ): void { const { roundedRadius } = SubgraphIONodeBase const transform = ctx.getTransform() @@ -182,14 +238,26 @@ export class SubgraphInputNode extends SubgraphIONodeBase impleme ctx.strokeStyle = this.sideStrokeStyle ctx.lineWidth = this.sideLineWidth ctx.beginPath() - ctx.arc(width - roundedRadius, roundedRadius, roundedRadius, Math.PI * 1.5, 0) + ctx.arc( + width - roundedRadius, + roundedRadius, + roundedRadius, + Math.PI * 1.5, + 0 + ) // Straight line to bottom ctx.moveTo(width, roundedRadius) ctx.lineTo(width, height - roundedRadius) // Bottom rounded part - ctx.arc(width - roundedRadius, height - roundedRadius, roundedRadius, 0, Math.PI * 0.5) + ctx.arc( + width - roundedRadius, + height - roundedRadius, + roundedRadius, + 0, + Math.PI * 0.5 + ) ctx.stroke() // Restore context diff --git a/src/lib/litegraph/src/subgraph/SubgraphNode.ts b/src/lib/litegraph/src/subgraph/SubgraphNode.ts index 89b2bc6323..846d40a679 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphNode.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphNode.ts @@ -1,22 +1,32 @@ -import type { SubgraphInput } from "./SubgraphInput" -import type { ISubgraphInput } from "@/lib/litegraph/src/interfaces" -import type { BaseLGraph, LGraph } from "@/lib/litegraph/src/LGraph" -import type { GraphOrSubgraph, Subgraph } from "@/lib/litegraph/src/subgraph/Subgraph" -import type { ExportedSubgraphInstance } from "@/lib/litegraph/src/types/serialisation" -import type { IBaseWidget } from "@/lib/litegraph/src/types/widgets" -import type { UUID } from "@/lib/litegraph/src/utils/uuid" +import type { BaseLGraph, LGraph } from '@/lib/litegraph/src/LGraph' +import { LGraphButton } from '@/lib/litegraph/src/LGraphButton' +import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' +import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LLink, type ResolvedConnection } from '@/lib/litegraph/src/LLink' +import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError' +import type { ISubgraphInput } from '@/lib/litegraph/src/interfaces' +import { + type INodeInputSlot, + type ISlotType, + type NodeId +} from '@/lib/litegraph/src/litegraph' +import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot' +import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot' +import type { + GraphOrSubgraph, + Subgraph +} from '@/lib/litegraph/src/subgraph/Subgraph' +import type { ExportedSubgraphInstance } from '@/lib/litegraph/src/types/serialisation' +import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' +import type { UUID } from '@/lib/litegraph/src/utils/uuid' +import { toConcreteWidget } from '@/lib/litegraph/src/widgets/widgetMap' -import { RecursionError } from "@/lib/litegraph/src/infrastructure/RecursionError" -import { LGraphButton } from "@/lib/litegraph/src/LGraphButton" -import { LGraphCanvas } from "@/lib/litegraph/src/LGraphCanvas" -import { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import { type INodeInputSlot, type ISlotType, type NodeId } from "@/lib/litegraph/src/litegraph" -import { LLink, type ResolvedConnection } from "@/lib/litegraph/src/LLink" -import { NodeInputSlot } from "@/lib/litegraph/src/node/NodeInputSlot" -import { NodeOutputSlot } from "@/lib/litegraph/src/node/NodeOutputSlot" -import { toConcreteWidget } from "@/lib/litegraph/src/widgets/widgetMap" - -import { type ExecutableLGraphNode, ExecutableNodeDTO, type ExecutionId } from "./ExecutableNodeDTO" +import { + type ExecutableLGraphNode, + ExecutableNodeDTO, + type ExecutionId +} from './ExecutableNodeDTO' +import type { SubgraphInput } from './SubgraphInput' /** * An instance of a {@link Subgraph}, displayed as a node on the containing (parent) graph. @@ -32,7 +42,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { } override get displayType(): string { - return "Subgraph node" + return 'Subgraph node' } override isSubgraphNode(): this is SubgraphNode { @@ -49,7 +59,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { override readonly graph: GraphOrSubgraph, /** The definition of this subgraph; how its nodes are configured, etc. */ readonly subgraph: Subgraph, - instanceData: ExportedSubgraphInstance, + instanceData: ExportedSubgraphInstance ) { super(subgraph.name, subgraph.id) @@ -57,75 +67,105 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { const subgraphEvents = this.subgraph.events const { signal } = this.#eventAbortController - subgraphEvents.addEventListener("input-added", (e) => { - const subgraphInput = e.detail.input - const { name, type } = subgraphInput - const input = this.addInput(name, type) + subgraphEvents.addEventListener( + 'input-added', + (e) => { + const subgraphInput = e.detail.input + const { name, type } = subgraphInput + const input = this.addInput(name, type) - this.#addSubgraphInputListeners(subgraphInput, input) - }, { signal }) + this.#addSubgraphInputListeners(subgraphInput, input) + }, + { signal } + ) - subgraphEvents.addEventListener("removing-input", (e) => { - const widget = e.detail.input._widget - if (widget) this.ensureWidgetRemoved(widget) + subgraphEvents.addEventListener( + 'removing-input', + (e) => { + const widget = e.detail.input._widget + if (widget) this.ensureWidgetRemoved(widget) - this.removeInput(e.detail.index) - this.setDirtyCanvas(true, true) - }, { signal }) + this.removeInput(e.detail.index) + this.setDirtyCanvas(true, true) + }, + { signal } + ) - subgraphEvents.addEventListener("output-added", (e) => { - const { name, type } = e.detail.output - this.addOutput(name, type) - }, { signal }) + subgraphEvents.addEventListener( + 'output-added', + (e) => { + const { name, type } = e.detail.output + this.addOutput(name, type) + }, + { signal } + ) - subgraphEvents.addEventListener("removing-output", (e) => { - this.removeOutput(e.detail.index) - this.setDirtyCanvas(true, true) - }, { signal }) + subgraphEvents.addEventListener( + 'removing-output', + (e) => { + this.removeOutput(e.detail.index) + this.setDirtyCanvas(true, true) + }, + { signal } + ) - subgraphEvents.addEventListener("renaming-input", (e) => { - const { index, newName } = e.detail - const input = this.inputs.at(index) - if (!input) throw new Error("Subgraph input not found") + subgraphEvents.addEventListener( + 'renaming-input', + (e) => { + const { index, newName } = e.detail + const input = this.inputs.at(index) + if (!input) throw new Error('Subgraph input not found') - input.label = newName - }, { signal }) + input.label = newName + }, + { signal } + ) - subgraphEvents.addEventListener("renaming-output", (e) => { - const { index, newName } = e.detail - const output = this.outputs.at(index) - if (!output) throw new Error("Subgraph output not found") + subgraphEvents.addEventListener( + 'renaming-output', + (e) => { + const { index, newName } = e.detail + const output = this.outputs.at(index) + if (!output) throw new Error('Subgraph output not found') - output.label = newName - }, { signal }) + output.label = newName + }, + { signal } + ) this.type = subgraph.id this.configure(instanceData) this.addTitleButton({ - name: "enter_subgraph", - text: "\uE93B", // Unicode for pi-window-maximize + name: 'enter_subgraph', + text: '\uE93B', // Unicode for pi-window-maximize yOffset: 0, // No vertical offset needed, button is centered xOffset: -10, - fontSize: 16, + fontSize: 16 }) } - override onTitleButtonClick(button: LGraphButton, canvas: LGraphCanvas): void { - if (button.name === "enter_subgraph") { + override onTitleButtonClick( + button: LGraphButton, + canvas: LGraphCanvas + ): void { + if (button.name === 'enter_subgraph') { canvas.openSubgraph(this.subgraph) } else { super.onTitleButtonClick(button, canvas) } } - #addSubgraphInputListeners(subgraphInput: SubgraphInput, input: INodeInputSlot & Partial) { + #addSubgraphInputListeners( + subgraphInput: SubgraphInput, + input: INodeInputSlot & Partial + ) { input._listenerController?.abort() input._listenerController = new AbortController() const { signal } = input._listenerController subgraphInput.events.addEventListener( - "input-connected", + 'input-connected', () => { if (input._widget) return @@ -134,11 +174,11 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { this.#setWidget(subgraphInput, input, widget) }, - { signal }, + { signal } ) subgraphInput.events.addEventListener( - "input-disconnected", + 'input-disconnected', () => { // If the input is connected to more than one widget, don't remove the widget const connectedWidgets = subgraphInput.getConnectedWidgets() @@ -150,7 +190,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { delete input.widget input._widget = undefined }, - { signal }, + { signal } ) } @@ -162,15 +202,35 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { this.inputs.length = 0 this.inputs.push( ...this.subgraph.inputNode.slots.map( - slot => new NodeInputSlot({ name: slot.name, localized_name: slot.localized_name, label: slot.label, type: slot.type, link: null }, this), - ), + (slot) => + new NodeInputSlot( + { + name: slot.name, + localized_name: slot.localized_name, + label: slot.label, + type: slot.type, + link: null + }, + this + ) + ) ) this.outputs.length = 0 this.outputs.push( ...this.subgraph.outputNode.slots.map( - slot => new NodeOutputSlot({ name: slot.name, localized_name: slot.localized_name, label: slot.label, type: slot.type, links: null }, this), - ), + (slot) => + new NodeOutputSlot( + { + name: slot.name, + localized_name: slot.localized_name, + label: slot.label, + type: slot.type, + links: null + }, + this + ) + ) ) super.configure(info) @@ -182,8 +242,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { // Check all inputs for connected widgets for (const input of this.inputs) { - const subgraphInput = this.subgraph.inputNode.slots.find(slot => slot.name === input.name) - if (!subgraphInput) throw new Error(`[SubgraphNode.configure] No subgraph input found for input ${input.name}`) + const subgraphInput = this.subgraph.inputNode.slots.find( + (slot) => slot.name === input.name + ) + if (!subgraphInput) + throw new Error( + `[SubgraphNode.configure] No subgraph input found for input ${input.name}` + ) this.#addSubgraphInputListeners(subgraphInput, input) @@ -191,13 +256,16 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { for (const linkId of subgraphInput.linkIds) { const link = this.subgraph.getLink(linkId) if (!link) { - console.warn(`[SubgraphNode.configure] No link found for link ID ${linkId}`, this) + console.warn( + `[SubgraphNode.configure] No link found for link ID ${linkId}`, + this + ) continue } const resolved = link.resolve(this.subgraph) if (!resolved.input || !resolved.inputNode) { - console.warn("Invalid resolved link", resolved, this) + console.warn('Invalid resolved link', resolved, this) continue } @@ -211,42 +279,67 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { } } - #setWidget(subgraphInput: Readonly, input: INodeInputSlot, widget: Readonly) { + #setWidget( + subgraphInput: Readonly, + input: INodeInputSlot, + widget: Readonly + ) { // Use the first matching widget - const promotedWidget = toConcreteWidget(widget, this).createCopyForNode(this) + const promotedWidget = toConcreteWidget(widget, this).createCopyForNode( + this + ) Object.assign(promotedWidget, { get name() { return subgraphInput.name }, set name(value) { - console.warn("Promoted widget: setting name is not allowed", this, value) + console.warn( + 'Promoted widget: setting name is not allowed', + this, + value + ) }, get localized_name() { return subgraphInput.localized_name }, set localized_name(value) { - console.warn("Promoted widget: setting localized_name is not allowed", this, value) + console.warn( + 'Promoted widget: setting localized_name is not allowed', + this, + value + ) }, get label() { return subgraphInput.label }, set label(value) { - console.warn("Promoted widget: setting label is not allowed", this, value) + console.warn( + 'Promoted widget: setting label is not allowed', + this, + value + ) }, get tooltip() { // Preserve the original widget's tooltip for promoted widgets return widget.tooltip }, set tooltip(value) { - console.warn("Promoted widget: setting tooltip is not allowed", this, value) - }, + console.warn( + 'Promoted widget: setting tooltip is not allowed', + this, + value + ) + } }) this.widgets.push(promotedWidget) // Dispatch widget-promoted event - this.subgraph.events.dispatch("widget-promoted", { widget: promotedWidget, subgraphNode: this }) + this.subgraph.events.dispatch('widget-promoted', { + widget: promotedWidget, + subgraphNode: this + }) input.widget = { name: subgraphInput.name } input._widget = promotedWidget @@ -260,7 +353,11 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { * @returns The new input slot. * @remarks Assertion is required to instantiate empty generic POJO. */ - override addInput>(name: string, type: ISlotType, inputProperties: TInput = {} as TInput): INodeInputSlot & TInput { + override addInput>( + name: string, + type: ISlotType, + inputProperties: TInput = {} as TInput + ): INodeInputSlot & TInput { // Bypasses type narrowing on this.inputs return super.addInput(name, type, inputProperties) } @@ -269,7 +366,9 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { // Output side: the link from inside the subgraph const innerLink = this.subgraph.outputNode.slots[slot].getLinks().at(0) if (!innerLink) { - console.warn(`SubgraphNode.getInputLink: no inner link found for slot ${slot}`) + console.warn( + `SubgraphNode.getInputLink: no inner link found for slot ${slot}` + ) return null } @@ -290,10 +389,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { const inputSlot = this.subgraph.inputNode.slots[slot] const innerLinks = inputSlot.getLinks() if (innerLinks.length === 0) { - console.debug(`[SubgraphNode.resolveSubgraphInputLinks] No inner links found for input slot [${slot}] ${inputSlot.name}`, this) + console.debug( + `[SubgraphNode.resolveSubgraphInputLinks] No inner links found for input slot [${slot}] ${inputSlot.name}`, + this + ) return [] } - return innerLinks.map(link => link.resolve(this.subgraph)) + return innerLinks.map((link) => link.resolve(this.subgraph)) } /** @@ -306,7 +408,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { const innerLink = outputSlot.getLinks().at(0) if (innerLink) return innerLink.resolve(this.subgraph) - console.debug(`[SubgraphNode.resolveSubgraphOutputLink] No inner link found for output slot [${slot}] ${outputSlot.name}`, this) + console.debug( + `[SubgraphNode.resolveSubgraphOutputLink] No inner link found for output slot [${slot}] ${outputSlot.name}`, + this + ) } /** @internal Used to flatten the subgraph before execution. */ @@ -318,15 +423,15 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { /** Internal recursion param. The list of nodes to add to. */ nodes: ExecutableLGraphNode[] = [], /** Internal recursion param. The set of visited nodes. */ - visited = new Set(), + visited = new Set() ): ExecutableLGraphNode[] { if (visited.has(this)) { - const nodeInfo = `${this.id}${this.title ? ` (${this.title})` : ""}` - const subgraphInfo = `'${this.subgraph.name || "Unnamed Subgraph"}'` + const nodeInfo = `${this.id}${this.title ? ` (${this.title})` : ''}` + const subgraphInfo = `'${this.subgraph.name || 'Unnamed Subgraph'}'` const depth = subgraphNodePath.length throw new RecursionError( `Circular reference detected at depth ${depth} in node ${nodeInfo} of subgraph ${subgraphInfo}. ` + - `This creates an infinite loop in the subgraph hierarchy.`, + `This creates an infinite loop in the subgraph hierarchy.` ) } visited.add(this) @@ -334,16 +439,33 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { const subgraphInstanceIdPath = [...subgraphNodePath, this.id] // Store the subgraph node DTO - const parentSubgraphNode = this.graph.rootGraph.resolveSubgraphIdPath(subgraphNodePath).at(-1) - const subgraphNodeDto = new ExecutableNodeDTO(this, subgraphNodePath, executableNodes, parentSubgraphNode) + const parentSubgraphNode = this.graph.rootGraph + .resolveSubgraphIdPath(subgraphNodePath) + .at(-1) + const subgraphNodeDto = new ExecutableNodeDTO( + this, + subgraphNodePath, + executableNodes, + parentSubgraphNode + ) executableNodes.set(subgraphNodeDto.id, subgraphNodeDto) for (const node of this.subgraph.nodes) { - if ("getInnerNodes" in node && node.getInnerNodes) { - node.getInnerNodes(executableNodes, subgraphInstanceIdPath, nodes, new Set(visited)) + if ('getInnerNodes' in node && node.getInnerNodes) { + node.getInnerNodes( + executableNodes, + subgraphInstanceIdPath, + nodes, + new Set(visited) + ) } else { // Create minimal DTOs rather than cloning the node - const aVeryRealNode = new ExecutableNodeDTO(node, subgraphInstanceIdPath, executableNodes, this) + const aVeryRealNode = new ExecutableNodeDTO( + node, + subgraphInstanceIdPath, + executableNodes, + this + ) executableNodes.set(aVeryRealNode.id, aVeryRealNode) nodes.push(aVeryRealNode) } @@ -352,16 +474,22 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { } override removeWidgetByName(name: string): void { - const widget = this.widgets.find(w => w.name === name) + const widget = this.widgets.find((w) => w.name === name) if (widget) { - this.subgraph.events.dispatch("widget-demoted", { widget, subgraphNode: this }) + this.subgraph.events.dispatch('widget-demoted', { + widget, + subgraphNode: this + }) } super.removeWidgetByName(name) } override ensureWidgetRemoved(widget: IBaseWidget): void { if (this.widgets.includes(widget)) { - this.subgraph.events.dispatch("widget-demoted", { widget, subgraphNode: this }) + this.subgraph.events.dispatch('widget-demoted', { + widget, + subgraphNode: this + }) } super.ensureWidgetRemoved(widget) } @@ -372,7 +500,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph { // Clean up all promoted widgets for (const widget of this.widgets) { - this.subgraph.events.dispatch("widget-demoted", { widget, subgraphNode: this }) + this.subgraph.events.dispatch('widget-demoted', { + widget, + subgraphNode: this + }) } for (const input of this.inputs) { diff --git a/src/lib/litegraph/src/subgraph/SubgraphOutput.ts b/src/lib/litegraph/src/subgraph/SubgraphOutput.ts index c2f7c297de..f816286f33 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphOutput.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphOutput.ts @@ -1,16 +1,20 @@ -import type { SubgraphInput } from "./SubgraphInput" -import type { SubgraphOutputNode } from "./SubgraphOutputNode" -import type { INodeInputSlot, INodeOutputSlot, Point, ReadOnlyRect } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { RerouteId } from "@/lib/litegraph/src/Reroute" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LLink } from '@/lib/litegraph/src/LLink' +import type { RerouteId } from '@/lib/litegraph/src/Reroute' +import type { + INodeInputSlot, + INodeOutputSlot, + Point, + ReadOnlyRect +} from '@/lib/litegraph/src/interfaces' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums' +import { removeFromArray } from '@/lib/litegraph/src/utils/collections' -import { LiteGraph } from "@/lib/litegraph/src/litegraph" -import { LLink } from "@/lib/litegraph/src/LLink" -import { NodeSlotType } from "@/lib/litegraph/src/types/globalEnums" -import { removeFromArray } from "@/lib/litegraph/src/utils/collections" - -import { SubgraphSlot } from "./SubgraphSlotBase" -import { isNodeSlot, isSubgraphInput } from "./subgraphUtils" +import type { SubgraphInput } from './SubgraphInput' +import type { SubgraphOutputNode } from './SubgraphOutputNode' +import { SubgraphSlot } from './SubgraphSlotBase' +import { isNodeSlot, isSubgraphInput } from './subgraphUtils' /** * An output "slot" from a subgraph to a parent graph. @@ -26,7 +30,11 @@ import { isNodeSlot, isSubgraphInput } from "./subgraphUtils" export class SubgraphOutput extends SubgraphSlot { declare parent: SubgraphOutputNode - override connect(slot: INodeOutputSlot, node: LGraphNode, afterRerouteId?: RerouteId): LLink | undefined { + override connect( + slot: INodeOutputSlot, + node: LGraphNode, + afterRerouteId?: RerouteId + ): LLink | undefined { const { subgraph } = this.parent // Validate type compatibility @@ -34,16 +42,21 @@ export class SubgraphOutput extends SubgraphSlot { // Allow nodes to block connection const outputIndex = node.outputs.indexOf(slot) - if (outputIndex === -1) throw new Error("Slot is not an output of the given node") + if (outputIndex === -1) + throw new Error('Slot is not an output of the given node') - if (node.onConnectOutput?.(outputIndex, this.type, this, this.parent, -1) === false) return + if ( + node.onConnectOutput?.(outputIndex, this.type, this, this.parent, -1) === + false + ) + return // Link should not be present, but just in case, disconnect it const existingLink = this.getLinks().at(0) if (existingLink != null) { subgraph.beforeChange() - existingLink.disconnect(subgraph, "input") + existingLink.disconnect(subgraph, 'input') const resolved = existingLink.resolve(subgraph) const links = resolved.output?.links if (links) removeFromArray(links, existingLink.id) @@ -56,7 +69,7 @@ export class SubgraphOutput extends SubgraphSlot { outputIndex, this.parent.id, this.parent.slots.indexOf(this), - afterRerouteId, + afterRerouteId ) // Add to graph links list @@ -92,7 +105,7 @@ export class SubgraphOutput extends SubgraphSlot { outputIndex, true, link, - slot, + slot ) subgraph.afterChange() @@ -123,9 +136,14 @@ export class SubgraphOutput extends SubgraphSlot { * For SubgraphOutput (which acts as an input inside the subgraph), * the fromSlot should be an output slot. */ - override isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput): boolean { + override isValidTarget( + fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput + ): boolean { if (isNodeSlot(fromSlot)) { - return "links" in fromSlot && LiteGraph.isValidConnection(fromSlot.type, this.type) + return ( + 'links' in fromSlot && + LiteGraph.isValidConnection(fromSlot.type, this.type) + ) } if (isSubgraphInput(fromSlot)) { diff --git a/src/lib/litegraph/src/subgraph/SubgraphOutputNode.ts b/src/lib/litegraph/src/subgraph/SubgraphOutputNode.ts index c5ff9caa73..6cce8e60d1 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphOutputNode.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphOutputNode.ts @@ -1,23 +1,31 @@ -import type { SubgraphInput } from "./SubgraphInput" -import type { SubgraphOutput } from "./SubgraphOutput" -import type { LinkConnector } from "@/lib/litegraph/src/canvas/LinkConnector" -import type { CanvasPointer } from "@/lib/litegraph/src/CanvasPointer" -import type { DefaultConnectionColors, INodeInputSlot, INodeOutputSlot, ISlotType, Positionable } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode, NodeId } from "@/lib/litegraph/src/LGraphNode" -import type { LLink } from "@/lib/litegraph/src/LLink" -import type { RerouteId } from "@/lib/litegraph/src/Reroute" -import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events" -import type { NodeLike } from "@/lib/litegraph/src/types/NodeLike" -import type { SubgraphIO } from "@/lib/litegraph/src/types/serialisation" +import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer' +import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode' +import type { LLink } from '@/lib/litegraph/src/LLink' +import type { RerouteId } from '@/lib/litegraph/src/Reroute' +import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector' +import { SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants' +import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' +import type { + DefaultConnectionColors, + INodeInputSlot, + INodeOutputSlot, + ISlotType, + Positionable +} from '@/lib/litegraph/src/interfaces' +import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' +import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation' +import { findFreeSlotOfType } from '@/lib/litegraph/src/utils/collections' -import { SUBGRAPH_OUTPUT_ID } from "@/lib/litegraph/src/constants" -import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" -import { findFreeSlotOfType } from "@/lib/litegraph/src/utils/collections" +import { EmptySubgraphOutput } from './EmptySubgraphOutput' +import { SubgraphIONodeBase } from './SubgraphIONodeBase' +import type { SubgraphInput } from './SubgraphInput' +import type { SubgraphOutput } from './SubgraphOutput' -import { EmptySubgraphOutput } from "./EmptySubgraphOutput" -import { SubgraphIONodeBase } from "./SubgraphIONodeBase" - -export class SubgraphOutputNode extends SubgraphIONodeBase implements Positionable { +export class SubgraphOutputNode + extends SubgraphIONodeBase + implements Positionable +{ readonly id: NodeId = SUBGRAPH_OUTPUT_ID readonly emptySlot: EmptySubgraphOutput = new EmptySubgraphOutput(this) @@ -35,11 +43,18 @@ export class SubgraphOutputNode extends SubgraphIONodeBase imple return x + SubgraphIONodeBase.roundedRadius } - override onPointerDown(e: CanvasPointerEvent, pointer: CanvasPointer, linkConnector: LinkConnector): void { + override onPointerDown( + e: CanvasPointerEvent, + pointer: CanvasPointer, + linkConnector: LinkConnector + ): void { // Left-click handling for dragging connections if (e.button === 0) { for (const slot of this.allSlots) { - const slotBounds = Rectangle.fromCentre(slot.pos, slot.boundingRect.height) + const slotBounds = Rectangle.fromCentre( + slot.pos, + slot.boundingRect.height + ) if (slotBounds.containsXy(e.canvasX, e.canvasY)) { pointer.onDragStart = () => { @@ -53,7 +68,7 @@ export class SubgraphOutputNode extends SubgraphIONodeBase imple } } } - // Check for right-click + // Check for right-click } else if (e.button === 2) { const slot = this.getSlotInPosition(e.canvasX, e.canvasY) if (slot) this.showSlotContextMenu(slot, e) @@ -70,7 +85,11 @@ export class SubgraphOutputNode extends SubgraphIONodeBase imple this.subgraph.removeOutput(slot) } - canConnectTo(outputNode: NodeLike, fromSlot: SubgraphOutput, output: INodeOutputSlot | SubgraphIO): boolean { + canConnectTo( + outputNode: NodeLike, + fromSlot: SubgraphOutput, + output: INodeOutputSlot | SubgraphIO + ): boolean { return outputNode.canConnectTo(this, fromSlot, output) } @@ -78,19 +97,36 @@ export class SubgraphOutputNode extends SubgraphIONodeBase imple slot: number, target_node: LGraphNode, target_slotType: ISlotType, - optsIn?: { afterRerouteId?: RerouteId }, + optsIn?: { afterRerouteId?: RerouteId } ): LLink | undefined { const outputSlot = target_node.findOutputByType(target_slotType) if (!outputSlot) return - return this.slots[slot].connect(outputSlot.slot, target_node, optsIn?.afterRerouteId) + return this.slots[slot].connect( + outputSlot.slot, + target_node, + optsIn?.afterRerouteId + ) } findInputByType(type: ISlotType): SubgraphOutput | undefined { - return findFreeSlotOfType(this.slots, type, slot => slot.linkIds.length > 0)?.slot + return findFreeSlotOfType( + this.slots, + type, + (slot) => slot.linkIds.length > 0 + )?.slot } - override drawProtected(ctx: CanvasRenderingContext2D, colorContext: DefaultConnectionColors, fromSlot?: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput, editorAlpha?: number): void { + override drawProtected( + ctx: CanvasRenderingContext2D, + colorContext: DefaultConnectionColors, + fromSlot?: + | INodeInputSlot + | INodeOutputSlot + | SubgraphInput + | SubgraphOutput, + editorAlpha?: number + ): void { const { roundedRadius } = SubgraphIONodeBase const transform = ctx.getTransform() @@ -108,7 +144,14 @@ export class SubgraphOutputNode extends SubgraphIONodeBase imple ctx.lineTo(0, height - roundedRadius) // Bottom rounded part - ctx.arc(roundedRadius, height - roundedRadius, roundedRadius, Math.PI, Math.PI * 0.5, true) + ctx.arc( + roundedRadius, + height - roundedRadius, + roundedRadius, + Math.PI, + Math.PI * 0.5, + true + ) ctx.stroke() // Restore context diff --git a/src/lib/litegraph/src/subgraph/SubgraphSlotBase.ts b/src/lib/litegraph/src/subgraph/SubgraphSlotBase.ts index 20426f0e89..ed973733a2 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphSlotBase.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphSlotBase.ts @@ -1,21 +1,32 @@ -import type { SubgraphInput } from "./SubgraphInput" -import type { SubgraphInputNode } from "./SubgraphInputNode" -import type { SubgraphOutput } from "./SubgraphOutput" -import type { SubgraphOutputNode } from "./SubgraphOutputNode" -import type { DefaultConnectionColors, Hoverable, INodeInputSlot, INodeOutputSlot, Point, ReadOnlyRect, ReadOnlySize } from "@/lib/litegraph/src/interfaces" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { LinkId, LLink } from "@/lib/litegraph/src/LLink" -import type { RerouteId } from "@/lib/litegraph/src/Reroute" -import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events" -import type { Serialisable, SubgraphIO } from "@/lib/litegraph/src/types/serialisation" +import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LLink, LinkId } from '@/lib/litegraph/src/LLink' +import type { RerouteId } from '@/lib/litegraph/src/Reroute' +import { SlotShape } from '@/lib/litegraph/src/draw' +import { ConstrainedSize } from '@/lib/litegraph/src/infrastructure/ConstrainedSize' +import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' +import type { + DefaultConnectionColors, + Hoverable, + INodeInputSlot, + INodeOutputSlot, + Point, + ReadOnlyRect, + ReadOnlySize +} from '@/lib/litegraph/src/interfaces' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { SlotBase } from '@/lib/litegraph/src/node/SlotBase' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' +import type { + Serialisable, + SubgraphIO +} from '@/lib/litegraph/src/types/serialisation' +import { type UUID, createUuidv4 } from '@/lib/litegraph/src/utils/uuid' -import { SlotShape } from "@/lib/litegraph/src/draw" -import { ConstrainedSize } from "@/lib/litegraph/src/infrastructure/ConstrainedSize" -import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" -import { LGraphCanvas } from "@/lib/litegraph/src/LGraphCanvas" -import { LiteGraph } from "@/lib/litegraph/src/litegraph" -import { SlotBase } from "@/lib/litegraph/src/node/SlotBase" -import { createUuidv4, type UUID } from "@/lib/litegraph/src/utils/uuid" +import type { SubgraphInput } from './SubgraphInput' +import type { SubgraphInputNode } from './SubgraphInputNode' +import type { SubgraphOutput } from './SubgraphOutput' +import type { SubgraphOutputNode } from './SubgraphOutputNode' export interface SubgraphSlotDrawOptions { ctx: CanvasRenderingContext2D @@ -26,14 +37,20 @@ export interface SubgraphSlotDrawOptions { } /** Shared base class for the slots used on Subgraph . */ -export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Hoverable, Serialisable { +export abstract class SubgraphSlot + extends SlotBase + implements SubgraphIO, Hoverable, Serialisable +{ static get defaultHeight() { return LiteGraph.NODE_SLOT_HEIGHT } readonly #pos: Point = new Float32Array(2) - readonly measurement: ConstrainedSize = new ConstrainedSize(SubgraphSlot.defaultHeight, SubgraphSlot.defaultHeight) + readonly measurement: ConstrainedSize = new ConstrainedSize( + SubgraphSlot.defaultHeight, + SubgraphSlot.defaultHeight + ) readonly id: UUID readonly parent: SubgraphInputNode | SubgraphOutputNode @@ -41,7 +58,12 @@ export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Hover readonly linkIds: LinkId[] = [] - override readonly boundingRect: Rectangle = new Rectangle(0, 0, 0, SubgraphSlot.defaultHeight) + override readonly boundingRect: Rectangle = new Rectangle( + 0, + 0, + 0, + SubgraphSlot.defaultHeight + ) override get pos() { return this.#pos @@ -66,7 +88,10 @@ export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Hover abstract get labelPos(): Point - constructor(slot: SubgraphIO, parent: SubgraphInputNode | SubgraphOutputNode) { + constructor( + slot: SubgraphIO, + parent: SubgraphInputNode | SubgraphOutputNode + ) { super(slot.name, slot.type) Object.assign(this, slot) @@ -96,14 +121,15 @@ export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Hover return links } - decrementSlots(inputsOrOutputs: "inputs" | "outputs"): void { + decrementSlots(inputsOrOutputs: 'inputs' | 'outputs'): void { const { links } = this.parent.subgraph - const linkProperty = inputsOrOutputs === "inputs" ? "origin_slot" : "target_slot" + const linkProperty = + inputsOrOutputs === 'inputs' ? 'origin_slot' : 'target_slot' for (const linkId of this.linkIds) { const link = links.get(linkId) if (link) link[linkProperty]-- - else console.warn("decrementSlots: link ID not found", linkId) + else console.warn('decrementSlots: link ID not found', linkId) } } @@ -120,7 +146,7 @@ export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Hover abstract connect( slot: INodeInputSlot | INodeOutputSlot, node: LGraphNode, - afterRerouteId?: RerouteId, + afterRerouteId?: RerouteId ): LLink | undefined /** @@ -141,13 +167,24 @@ export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Hover * @param fromSlot The slot that is being dragged to connect to this slot. * @returns true if the connection is valid, false otherwise. */ - abstract isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput): boolean + abstract isValidTarget( + fromSlot: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput + ): boolean /** @remarks Leaves the context dirty. */ - draw({ ctx, colorContext, lowQuality, fromSlot, editorAlpha = 1 }: SubgraphSlotDrawOptions): void { + draw({ + ctx, + colorContext, + lowQuality, + fromSlot, + editorAlpha = 1 + }: SubgraphSlotDrawOptions): void { // Assertion: SlotShape is a subset of RenderShape const shape = this.shape as unknown as SlotShape - const { isPointerOver, pos: [x, y] } = this + const { + isPointerOver, + pos: [x, y] + } = this // Check if this slot is a valid target for the current dragging connection const isValidTarget = fromSlot ? this.isValidTarget(fromSlot) : true @@ -191,7 +228,7 @@ export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Hover if (this.displayName) { const [labelX, labelY] = this.labelPos // Also apply highlight logic to text color - ctx.fillStyle = highlight ? "white" : (LiteGraph.NODE_TEXT_COLOR || "#AAA") + ctx.fillStyle = highlight ? 'white' : LiteGraph.NODE_TEXT_COLOR || '#AAA' ctx.fillText(this.displayName, labelX, labelY) } @@ -200,7 +237,31 @@ export abstract class SubgraphSlot extends SlotBase implements SubgraphIO, Hover } asSerialisable(): SubgraphIO { - const { id, name, type, linkIds, localized_name, label, dir, shape, color_off, color_on, pos } = this - return { id, name, type, linkIds, localized_name, label, dir, shape, color_off, color_on, pos } + const { + id, + name, + type, + linkIds, + localized_name, + label, + dir, + shape, + color_off, + color_on, + pos + } = this + return { + id, + name, + type, + linkIds, + localized_name, + label, + dir, + shape, + color_off, + color_on, + pos + } } } diff --git a/src/lib/litegraph/src/subgraph/subgraphUtils.ts b/src/lib/litegraph/src/subgraph/subgraphUtils.ts index 9a7cba1bb8..2053d49995 100644 --- a/src/lib/litegraph/src/subgraph/subgraphUtils.ts +++ b/src/lib/litegraph/src/subgraph/subgraphUtils.ts @@ -1,21 +1,31 @@ -import type { GraphOrSubgraph } from "./Subgraph" -import type { SubgraphInput } from "./SubgraphInput" -import type { SubgraphOutput } from "./SubgraphOutput" -import type { INodeInputSlot, INodeOutputSlot, Positionable } from "@/lib/litegraph/src/interfaces" -import type { LGraph } from "@/lib/litegraph/src/LGraph" -import type { ISerialisedNode, SerialisableLLink, SubgraphIO } from "@/lib/litegraph/src/types/serialisation" -import type { UUID } from "@/lib/litegraph/src/utils/uuid" +import type { LGraph } from '@/lib/litegraph/src/LGraph' +import { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup' +import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LLink, type ResolvedConnection } from '@/lib/litegraph/src/LLink' +import { Reroute } from '@/lib/litegraph/src/Reroute' +import { + SUBGRAPH_INPUT_ID, + SUBGRAPH_OUTPUT_ID +} from '@/lib/litegraph/src/constants' +import type { + INodeInputSlot, + INodeOutputSlot, + Positionable +} from '@/lib/litegraph/src/interfaces' +import { LiteGraph, createUuidv4 } from '@/lib/litegraph/src/litegraph' +import { nextUniqueName } from '@/lib/litegraph/src/strings' +import type { + ISerialisedNode, + SerialisableLLink, + SubgraphIO +} from '@/lib/litegraph/src/types/serialisation' +import type { UUID } from '@/lib/litegraph/src/utils/uuid' -import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from "@/lib/litegraph/src/constants" -import { LGraphGroup } from "@/lib/litegraph/src/LGraphGroup" -import { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import { createUuidv4, LiteGraph } from "@/lib/litegraph/src/litegraph" -import { LLink, type ResolvedConnection } from "@/lib/litegraph/src/LLink" -import { Reroute } from "@/lib/litegraph/src/Reroute" -import { nextUniqueName } from "@/lib/litegraph/src/strings" - -import { SubgraphInputNode } from "./SubgraphInputNode" -import { SubgraphOutputNode } from "./SubgraphOutputNode" +import type { GraphOrSubgraph } from './Subgraph' +import type { SubgraphInput } from './SubgraphInput' +import { SubgraphInputNode } from './SubgraphInputNode' +import type { SubgraphOutput } from './SubgraphOutput' +import { SubgraphOutputNode } from './SubgraphOutputNode' export interface FilteredItems { nodes: Set @@ -26,7 +36,9 @@ export interface FilteredItems { unknown: Set } -export function splitPositionables(items: Iterable): FilteredItems { +export function splitPositionables( + items: Iterable +): FilteredItems { const nodes = new Set() const reroutes = new Set() const groups = new Set() @@ -37,24 +49,24 @@ export function splitPositionables(items: Iterable): FilteredItems for (const item of items) { switch (true) { - case item instanceof LGraphNode: - nodes.add(item) - break - case item instanceof LGraphGroup: - groups.add(item) - break - case item instanceof Reroute: - reroutes.add(item) - break - case item instanceof SubgraphInputNode: - subgraphInputNodes.add(item) - break - case item instanceof SubgraphOutputNode: - subgraphOutputNodes.add(item) - break - default: - unknown.add(item) - break + case item instanceof LGraphNode: + nodes.add(item) + break + case item instanceof LGraphGroup: + groups.add(item) + break + case item instanceof Reroute: + reroutes.add(item) + break + case item instanceof SubgraphInputNode: + subgraphInputNodes.add(item) + break + case item instanceof SubgraphOutputNode: + subgraphOutputNodes.add(item) + break + default: + unknown.add(item) + break } } @@ -64,7 +76,7 @@ export function splitPositionables(items: Iterable): FilteredItems groups, subgraphInputNodes, subgraphOutputNodes, - unknown, + unknown } } @@ -76,7 +88,10 @@ interface BoundaryLinks { boundaryOutputLinks: LLink[] } -export function getBoundaryLinks(graph: LGraph, items: Set): BoundaryLinks { +export function getBoundaryLinks( + graph: LGraph, + items: Set +): BoundaryLinks { const internalLinks: LLink[] = [] const boundaryLinks: LLink[] = [] const boundaryInputLinks: LLink[] = [] @@ -151,7 +166,9 @@ export function getBoundaryLinks(graph: LGraph, items: Set): Bound const results = LLink.resolveMany(reroute.linkIds, graph) for (const { link } of results) { const reroutes = LLink.getReroutes(graph, link) - const reroutesOutside = reroutes.filter(reroute => !items.has(reroute)) + const reroutesOutside = reroutes.filter( + (reroute) => !items.has(reroute) + ) // for (const reroute of reroutes) { // // TODO: Do the checks here. @@ -170,7 +187,13 @@ export function getBoundaryLinks(graph: LGraph, items: Set): Bound } } - return { boundaryLinks, boundaryFloatingLinks, internalLinks, boundaryInputLinks, boundaryOutputLinks } + return { + boundaryLinks, + boundaryFloatingLinks, + internalLinks, + boundaryInputLinks, + boundaryOutputLinks + } /** * Adds any floating links that cross the boundary. @@ -180,9 +203,9 @@ export function getBoundaryLinks(graph: LGraph, items: Set): Bound if (!floatingLinks) return for (const link of floatingLinks) { - const crossesBoundary = LLink - .getReroutes(graph, link) - .some(reroute => !items.has(reroute)) + const crossesBoundary = LLink.getReroutes(graph, link).some( + (reroute) => !items.has(reroute) + ) if (crossesBoundary) boundaryFloatingLinks.push(link) } @@ -196,7 +219,7 @@ export function multiClone(nodes: Iterable): ISerialisedNode[] { for (const node of nodes) { const newNode = LiteGraph.createNode(node.type) if (!newNode) { - console.warn("Failed to create node", node.type) + console.warn('Failed to create node', node.type) continue } @@ -216,7 +239,7 @@ export function multiClone(nodes: Iterable): ISerialisedNode[] { * @returns A map of grouped connections. */ export function groupResolvedByOutput( - resolvedConnections: ResolvedConnection[], + resolvedConnections: ResolvedConnection[] ): Map { const groupedByOutput: ReturnType = new Map() @@ -234,7 +257,10 @@ export function groupResolvedByOutput( return groupedByOutput } -export function mapSubgraphInputsAndLinks(resolvedInputLinks: ResolvedConnection[], links: SerialisableLLink[]): SubgraphIO[] { +export function mapSubgraphInputsAndLinks( + resolvedInputLinks: ResolvedConnection[], + links: SerialisableLLink[] +): SubgraphIO[] { // Group matching links const groupedByOutput = groupResolvedByOutput(resolvedInputLinks) @@ -261,14 +287,32 @@ export function mapSubgraphInputsAndLinks(resolvedInputLinks: ResolvedConnection if (!input) continue // Subgraph input slot - const { color_off, color_on, dir, hasErrors, label, localized_name, name, shape, type } = input - const uniqueName = nextUniqueName(name, inputs.map(input => input.name)) - const uniqueLocalizedName = localized_name ? nextUniqueName(localized_name, inputs.map(input => input.localized_name ?? "")) : undefined + const { + color_off, + color_on, + dir, + hasErrors, + label, + localized_name, + name, + shape, + type + } = input + const uniqueName = nextUniqueName( + name, + inputs.map((input) => input.name) + ) + const uniqueLocalizedName = localized_name + ? nextUniqueName( + localized_name, + inputs.map((input) => input.localized_name ?? '') + ) + : undefined const inputData: SubgraphIO = { id: createUuidv4(), type: String(type), - linkIds: inputLinks.map(link => link.id), + linkIds: inputLinks.map((link) => link.id), name: uniqueName, color_off, color_on, @@ -276,7 +320,7 @@ export function mapSubgraphInputsAndLinks(resolvedInputLinks: ResolvedConnection label, localized_name: uniqueLocalizedName, hasErrors, - shape, + shape } inputs.push(inputData) @@ -291,7 +335,10 @@ export function mapSubgraphInputsAndLinks(resolvedInputLinks: ResolvedConnection * @param links The links to add to the subgraph. * @returns The subgraph output slots. */ -export function mapSubgraphOutputsAndLinks(resolvedOutputLinks: ResolvedConnection[], links: SerialisableLLink[]): SubgraphIO[] { +export function mapSubgraphOutputsAndLinks( + resolvedOutputLinks: ResolvedConnection[], + links: SerialisableLLink[] +): SubgraphIO[] { // Group matching links const groupedByOutput = groupResolvedByOutput(resolvedOutputLinks) @@ -318,14 +365,32 @@ export function mapSubgraphOutputsAndLinks(resolvedOutputLinks: ResolvedConnecti if (!output) continue // Subgraph output slot - const { color_off, color_on, dir, hasErrors, label, localized_name, name, shape, type } = output - const uniqueName = nextUniqueName(name, outputs.map(output => output.name)) - const uniqueLocalizedName = localized_name ? nextUniqueName(localized_name, outputs.map(output => output.localized_name ?? "")) : undefined + const { + color_off, + color_on, + dir, + hasErrors, + label, + localized_name, + name, + shape, + type + } = output + const uniqueName = nextUniqueName( + name, + outputs.map((output) => output.name) + ) + const uniqueLocalizedName = localized_name + ? nextUniqueName( + localized_name, + outputs.map((output) => output.localized_name ?? '') + ) + : undefined const outputData = { id: createUuidv4(), type: String(type), - linkIds: outputLinks.map(link => link.id), + linkIds: outputLinks.map((link) => link.id), name: uniqueName, color_off, color_on, @@ -333,7 +398,7 @@ export function mapSubgraphOutputsAndLinks(resolvedOutputLinks: ResolvedConnecti label, localized_name: uniqueLocalizedName, hasErrors, - shape, + shape } satisfies SubgraphIO outputs.push(structuredClone(outputData)) @@ -366,7 +431,7 @@ export function getDirectSubgraphIds(graph: GraphOrSubgraph): Set { */ export function findUsedSubgraphIds( rootGraph: GraphOrSubgraph, - subgraphRegistry: Map, + subgraphRegistry: Map ): Set { const usedSubgraphIds = new Set() const toVisit: GraphOrSubgraph[] = [rootGraph] @@ -395,8 +460,12 @@ export function findUsedSubgraphIds( * @returns true if the slot is a SubgraphInput */ export function isSubgraphInput(slot: unknown): slot is SubgraphInput { - return slot != null && typeof slot === "object" && "parent" in slot && + return ( + slot != null && + typeof slot === 'object' && + 'parent' in slot && slot.parent instanceof SubgraphInputNode + ) } /** @@ -405,8 +474,12 @@ export function isSubgraphInput(slot: unknown): slot is SubgraphInput { * @returns true if the slot is a SubgraphOutput */ export function isSubgraphOutput(slot: unknown): slot is SubgraphOutput { - return slot != null && typeof slot === "object" && "parent" in slot && + return ( + slot != null && + typeof slot === 'object' && + 'parent' in slot && slot.parent instanceof SubgraphOutputNode + ) } /** @@ -414,7 +487,12 @@ export function isSubgraphOutput(slot: unknown): slot is SubgraphOutput { * @param slot The slot to check * @returns true if the slot is a regular node slot */ -export function isNodeSlot(slot: unknown): slot is INodeInputSlot | INodeOutputSlot { - return slot != null && typeof slot === "object" && - ("link" in slot || "links" in slot) +export function isNodeSlot( + slot: unknown +): slot is INodeInputSlot | INodeOutputSlot { + return ( + slot != null && + typeof slot === 'object' && + ('link' in slot || 'links' in slot) + ) } diff --git a/src/lib/litegraph/src/types/NodeLike.ts b/src/lib/litegraph/src/types/NodeLike.ts index 62ace702eb..38d9ff5b1c 100644 --- a/src/lib/litegraph/src/types/NodeLike.ts +++ b/src/lib/litegraph/src/types/NodeLike.ts @@ -1,6 +1,9 @@ -import type { INodeInputSlot, INodeOutputSlot } from "@/lib/litegraph/src/interfaces" -import type { NodeId } from "@/lib/litegraph/src/LGraphNode" -import type { SubgraphIO } from "@/lib/litegraph/src/types/serialisation" +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' +import type { + INodeInputSlot, + INodeOutputSlot +} from '@/lib/litegraph/src/interfaces' +import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation' export interface NodeLike { id: NodeId @@ -8,6 +11,6 @@ export interface NodeLike { canConnectTo( node: NodeLike, toSlot: INodeInputSlot | SubgraphIO, - fromSlot: INodeOutputSlot | SubgraphIO, + fromSlot: INodeOutputSlot | SubgraphIO ): boolean } diff --git a/src/lib/litegraph/src/types/events.ts b/src/lib/litegraph/src/types/events.ts index 64cf9eb209..912cc89ffa 100644 --- a/src/lib/litegraph/src/types/events.ts +++ b/src/lib/litegraph/src/types/events.ts @@ -1,10 +1,9 @@ /** * Event interfaces for event extension */ - -import type { LGraphGroup } from "../LGraphGroup" -import type { LGraphNode } from "../LGraphNode" -import type { LinkReleaseContextExtended } from "../litegraph" +import type { LGraphGroup } from '../LGraphGroup' +import type { LGraphNode } from '../LGraphNode' +import type { LinkReleaseContextExtended } from '../litegraph' /** For Canvas*Event - adds graph space co-ordinates (property names are shipped) */ export interface ICanvasPosition { @@ -32,7 +31,9 @@ export interface IOffsetWorkaround { } /** All properties added when converting a pointer event to a CanvasPointerEvent (via {@link LGraphCanvas.adjustMouseEvent}). */ -export type CanvasPointerExtensions = ICanvasPosition & IDeltaPosition & IOffsetWorkaround +export type CanvasPointerExtensions = ICanvasPosition & + IDeltaPosition & + IOffsetWorkaround interface LegacyMouseEvent { /** @deprecated Part of DragAndScale mouse API - incomplete / not maintained */ @@ -44,15 +45,13 @@ interface LegacyMouseEvent { export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent {} /** MouseEvent with canvasX/Y and deltaX/Y properties */ -export interface CanvasMouseEvent extends - MouseEvent, - Readonly, - LegacyMouseEvent {} +export interface CanvasMouseEvent + extends MouseEvent, + Readonly, + LegacyMouseEvent {} /** DragEvent with canvasX/Y and deltaX/Y properties */ -export interface CanvasDragEvent extends - DragEvent, - CanvasPointerExtensions {} +export interface CanvasDragEvent extends DragEvent, CanvasPointerExtensions {} export type CanvasEventDetail = | GenericEventDetail @@ -62,7 +61,7 @@ export type CanvasEventDetail = | EmptyReleaseEventDetail export interface GenericEventDetail { - subType: "before-change" | "after-change" + subType: 'before-change' | 'after-change' } export interface OriginalEvent { @@ -70,20 +69,20 @@ export interface OriginalEvent { } export interface EmptyReleaseEventDetail extends OriginalEvent { - subType: "empty-release" + subType: 'empty-release' linkReleaseContext: LinkReleaseContextExtended } export interface EmptyDoubleClickEventDetail extends OriginalEvent { - subType: "empty-double-click" + subType: 'empty-double-click' } export interface GroupDoubleClickEventDetail extends OriginalEvent { - subType: "group-double-click" + subType: 'group-double-click' group: LGraphGroup } export interface NodeDoubleClickEventDetail extends OriginalEvent { - subType: "node-double-click" + subType: 'node-double-click' node: LGraphNode } diff --git a/src/lib/litegraph/src/types/globalEnums.ts b/src/lib/litegraph/src/types/globalEnums.ts index 7beb43b936..5e2afcadc3 100644 --- a/src/lib/litegraph/src/types/globalEnums.ts +++ b/src/lib/litegraph/src/types/globalEnums.ts @@ -1,7 +1,7 @@ /** Node slot type - input or output */ export enum NodeSlotType { INPUT = 1, - OUTPUT = 2, + OUTPUT = 2 } /** Shape that an object will render as - used by nodes and slots */ @@ -19,7 +19,7 @@ export enum RenderShape { /** Slot shape: Grid */ GRID = 6, /** Slot shape: Hollow circle */ - HollowCircle = 7, + HollowCircle = 7 } /** Bit flags used to indicate what the pointer is currently hovering over. */ @@ -39,7 +39,7 @@ export enum CanvasItem { /** A subgraph input or output node */ SubgraphIoNode = 1 << 6, /** A subgraph input or output slot */ - SubgraphIoSlot = 1 << 7, + SubgraphIoSlot = 1 << 7 } /** The direction that a link point will flow towards - e.g. horizontal outputs are right by default */ @@ -49,7 +49,7 @@ export enum LinkDirection { DOWN = 2, LEFT = 3, RIGHT = 4, - CENTER = 5, + CENTER = 5 } /** The path calculation that links follow */ @@ -60,7 +60,7 @@ export enum LinkRenderType { /** 90° angles, clean and box-like */ LINEAR_LINK = 1, /** Smooth curved links - default */ - SPLINE_LINK = 2, + SPLINE_LINK = 2 } /** The marker in the middle of a link */ @@ -70,14 +70,14 @@ export enum LinkMarkerShape { /** Circles (default) */ Circle = 1, /** Directional arrows */ - Arrow = 2, + Arrow = 2 } export enum TitleMode { NORMAL_TITLE = 0, NO_TITLE = 1, TRANSPARENT_TITLE = 2, - AUTOHIDE_TITLE = 3, + AUTOHIDE_TITLE = 3 } export enum LGraphEventMode { @@ -85,14 +85,14 @@ export enum LGraphEventMode { ON_EVENT = 1, NEVER = 2, ON_TRIGGER = 3, - BYPASS = 4, + BYPASS = 4 } export enum EaseFunction { - LINEAR = "linear", - EASE_IN_QUAD = "easeInQuad", - EASE_OUT_QUAD = "easeOutQuad", - EASE_IN_OUT_QUAD = "easeInOutQuad", + LINEAR = 'linear', + EASE_IN_QUAD = 'easeInQuad', + EASE_OUT_QUAD = 'easeOutQuad', + EASE_IN_OUT_QUAD = 'easeInOutQuad' } /** Bit flags used to indicate what the pointer is currently hovering over. */ @@ -128,7 +128,7 @@ export enum Alignment { /** Bottom side, horizontally centred */ BottomCentre = Bottom | Centre, /** Bottom right */ - BottomRight = Bottom | Right, + BottomRight = Bottom | Right } /** diff --git a/src/lib/litegraph/src/types/serialisation.ts b/src/lib/litegraph/src/types/serialisation.ts index 627d0d48ac..46b4ad7cc8 100644 --- a/src/lib/litegraph/src/types/serialisation.ts +++ b/src/lib/litegraph/src/types/serialisation.ts @@ -1,3 +1,10 @@ +import type { UUID } from '@/lib/litegraph/src/utils/uuid' + +import type { LGraphConfig, LGraphExtra, LGraphState } from '../LGraph' +import type { IGraphGroupFlags } from '../LGraphGroup' +import type { NodeId, NodeProperty } from '../LGraphNode' +import type { LinkId, SerialisedLLinkArray } from '../LLink' +import type { FloatingRerouteSlot, RerouteId } from '../Reroute' import type { Dictionary, INodeFlags, @@ -6,17 +13,11 @@ import type { INodeSlot, ISlotType, Point, - Size, -} from "../interfaces" -import type { LGraphConfig, LGraphExtra, LGraphState } from "../LGraph" -import type { IGraphGroupFlags } from "../LGraphGroup" -import type { NodeId, NodeProperty } from "../LGraphNode" -import type { LiteGraph } from "../litegraph" -import type { LinkId, SerialisedLLinkArray } from "../LLink" -import type { FloatingRerouteSlot, RerouteId } from "../Reroute" -import type { TWidgetValue } from "../types/widgets" -import type { RenderShape } from "./globalEnums" -import type { UUID } from "@/lib/litegraph/src/utils/uuid" + Size +} from '../interfaces' +import type { LiteGraph } from '../litegraph' +import type { TWidgetValue } from '../types/widgets' +import type { RenderShape } from './globalEnums' /** * An object that implements custom pre-serialization logic via {@link Serialisable.asSerialisable}. @@ -57,10 +58,16 @@ export interface SerialisableGraph extends BaseExportedGraph { extra?: LGraphExtra } -export type ISerialisableNodeInput = Omit & { +export type ISerialisableNodeInput = Omit< + INodeInputSlot, + 'boundingRect' | 'widget' +> & { widget?: { name: string } } -export type ISerialisableNodeOutput = Omit & { +export type ISerialisableNodeOutput = Omit< + INodeOutputSlot, + 'boundingRect' | '_data' +> & { widget?: { name: string } } @@ -92,7 +99,10 @@ export interface ISerialisedNode { } /** Properties of nodes that are used by subgraph instances. */ -type NodeSubgraphSharedProps = Omit +type NodeSubgraphSharedProps = Omit< + ISerialisedNode, + 'properties' | 'showAdvanced' +> /** A single instance of a subgraph; where it is used on a graph, any customisation to shape / colour etc. */ export interface ExportedSubgraphInstance extends NodeSubgraphSharedProps { @@ -136,7 +146,10 @@ export interface ExportedSubgraph extends SerialisableGraph { } /** Properties shared by subgraph and node I/O slots. */ -type SubgraphIOShared = Omit +type SubgraphIOShared = Omit< + INodeSlot, + 'boundingRect' | 'nameLocked' | 'locked' | 'removable' | '_floatingLinks' +> /** Subgraph I/O slots */ export interface SubgraphIO extends SubgraphIOShared { @@ -171,7 +184,7 @@ export type TClipboardLink = [ originSlot: number, nodeRelativeIndex: number, targetSlot: number, - targetNodeId: NodeId, + targetNodeId: NodeId ] /** Items copied from the canvas */ diff --git a/src/lib/litegraph/src/types/widgets.ts b/src/lib/litegraph/src/types/widgets.ts index fb397abb33..e61a8360a0 100644 --- a/src/lib/litegraph/src/types/widgets.ts +++ b/src/lib/litegraph/src/types/widgets.ts @@ -1,6 +1,6 @@ -import type { CanvasColour, Point, RequiredProps, Size } from "../interfaces" -import type { CanvasPointer, LGraphCanvas, LGraphNode } from "../litegraph" -import type { CanvasPointerEvent } from "./events" +import type { CanvasColour, Point, RequiredProps, Size } from '../interfaces' +import type { CanvasPointer, LGraphCanvas, LGraphNode } from '../litegraph' +import type { CanvasPointerEvent } from './events' export interface IWidgetOptions { on?: string @@ -27,7 +27,7 @@ export interface IWidgetOptions { socketless?: boolean values?: TValues - callback?: IWidget["callback"] + callback?: IWidget['callback'] } export interface IWidgetSliderOptions extends IWidgetOptions { @@ -66,62 +66,75 @@ export type IWidget = | IButtonWidget | IKnobWidget -export interface IBooleanWidget extends IBaseWidget { - type: "toggle" +export interface IBooleanWidget extends IBaseWidget { + type: 'toggle' value: boolean } /** Any widget that uses a numeric backing */ -export interface INumericWidget extends IBaseWidget { - type: "number" +export interface INumericWidget extends IBaseWidget { + type: 'number' value: number } -export interface ISliderWidget extends IBaseWidget { - type: "slider" +export interface ISliderWidget + extends IBaseWidget { + type: 'slider' value: number marker?: number } -export interface IKnobWidget extends IBaseWidget { - type: "knob" +export interface IKnobWidget + extends IBaseWidget { + type: 'knob' value: number options: IWidgetKnobOptions } /** Avoids the type issues with the legacy IComboWidget type */ -export interface IStringComboWidget extends IBaseWidget, "values">> { - type: "combo" +export interface IStringComboWidget + extends IBaseWidget< + string, + 'combo', + RequiredProps, 'values'> + > { + type: 'combo' value: string } -type ComboWidgetValues = string[] | Record | ((widget?: IComboWidget, node?: LGraphNode) => string[]) +type ComboWidgetValues = + | string[] + | Record + | ((widget?: IComboWidget, node?: LGraphNode) => string[]) /** A combo-box widget (dropdown, select, etc) */ -export interface IComboWidget extends IBaseWidget< - string | number, - "combo", - RequiredProps, "values"> -> { - type: "combo" +export interface IComboWidget + extends IBaseWidget< + string | number, + 'combo', + RequiredProps, 'values'> + > { + type: 'combo' value: string | number } /** A widget with a string value */ -export interface IStringWidget extends IBaseWidget> { - type: "string" | "text" +export interface IStringWidget + extends IBaseWidget> { + type: 'string' | 'text' value: string } -export interface IButtonWidget extends IBaseWidget { - type: "button" +export interface IButtonWidget + extends IBaseWidget { + type: 'button' value: string | undefined clicked: boolean } /** A custom widget - accepts any value and has no built-in special handling */ -export interface ICustomWidget extends IBaseWidget { - type: "custom" +export interface ICustomWidget extends IBaseWidget { + type: 'custom' value: string | object } @@ -130,8 +143,8 @@ export interface ICustomWidget extends IBaseWidget { * Override linkedWidgets[] * Values not in this list will not result in litegraph errors, however they will be treated the same as "custom". */ -export type TWidgetType = IWidget["type"] -export type TWidgetValue = IWidget["value"] +export type TWidgetType = IWidget['type'] +export type TWidgetValue = IWidget['value'] /** * The base type for all widgets. Should not be implemented directly. @@ -143,7 +156,7 @@ export type TWidgetValue = IWidget["value"] export interface IBaseWidget< TValue = boolean | number | string | object | undefined, TType extends string = string, - TOptions extends IWidgetOptions = IWidgetOptions, + TOptions extends IWidgetOptions = IWidgetOptions > { linkedWidgets?: IBaseWidget[] @@ -207,7 +220,7 @@ export interface IBaseWidget< canvas?: LGraphCanvas, node?: LGraphNode, pos?: Point, - e?: CanvasPointerEvent, + e?: CanvasPointerEvent ): void /** @@ -217,7 +230,11 @@ export interface IBaseWidget< * @param node The node this widget belongs to * @todo Expose CanvasPointer API to custom widgets */ - mouse?(event: CanvasPointerEvent, pointerOffset: Point, node: LGraphNode): boolean + mouse?( + event: CanvasPointerEvent, + pointerOffset: Point, + node: LGraphNode + ): boolean /** * Draw the widget. * @param ctx The canvas context to draw on. @@ -233,7 +250,7 @@ export interface IBaseWidget< widget_width: number, y: number, H: number, - lowQuality?: boolean, + lowQuality?: boolean ): void /** @@ -272,5 +289,9 @@ export interface IBaseWidget< * @returns Returning `true` from this callback forces Litegraph to ignore the event and * not process it any further. */ - onPointerDown?(pointer: CanvasPointer, node: LGraphNode, canvas: LGraphCanvas): boolean + onPointerDown?( + pointer: CanvasPointer, + node: LGraphNode, + canvas: LGraphCanvas + ): boolean } diff --git a/src/lib/litegraph/src/utils/arrange.ts b/src/lib/litegraph/src/utils/arrange.ts index 847461cd8d..3225d21a81 100644 --- a/src/lib/litegraph/src/utils/arrange.ts +++ b/src/lib/litegraph/src/utils/arrange.ts @@ -1,5 +1,5 @@ -import type { Direction, IBoundaryNodes } from "../interfaces" -import type { LGraphNode } from "../LGraphNode" +import type { LGraphNode } from '../LGraphNode' +import type { Direction, IBoundaryNodes } from '../interfaces' /** * Finds the nodes that are farthest in all four directions, representing the boundary of the nodes. @@ -8,7 +8,7 @@ import type { LGraphNode } from "../LGraphNode" * `null` if no nodes were supplied or the first node was falsy. */ export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null { - const valid = nodes?.find(x => x) + const valid = nodes?.find((x) => x) if (!valid) return null let top = valid @@ -31,7 +31,7 @@ export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null { top, right, bottom, - left, + left } } @@ -40,7 +40,10 @@ export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null { * @param nodes The nodes to distribute * @param horizontal If true, distributes along the horizontal plane. Otherwise, the vertical plane. */ -export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void { +export function distributeNodes( + nodes: LGraphNode[], + horizontal?: boolean +): void { const nodeCount = nodes?.length if (!(nodeCount > 1)) return @@ -76,30 +79,33 @@ export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void export function alignNodes( nodes: LGraphNode[], direction: Direction, - align_to?: LGraphNode, + align_to?: LGraphNode ): void { if (!nodes) return - const boundary = align_to === undefined - ? getBoundaryNodes(nodes) - : { top: align_to, right: align_to, bottom: align_to, left: align_to } + const boundary = + align_to === undefined + ? getBoundaryNodes(nodes) + : { top: align_to, right: align_to, bottom: align_to, left: align_to } if (boundary === null) return for (const node of nodes) { switch (direction) { - case "right": - node.pos[0] = boundary.right.pos[0] + boundary.right.size[0] - node.size[0] - break - case "left": - node.pos[0] = boundary.left.pos[0] - break - case "top": - node.pos[1] = boundary.top.pos[1] - break - case "bottom": - node.pos[1] = boundary.bottom.pos[1] + boundary.bottom.size[1] - node.size[1] - break + case 'right': + node.pos[0] = + boundary.right.pos[0] + boundary.right.size[0] - node.size[0] + break + case 'left': + node.pos[0] = boundary.left.pos[0] + break + case 'top': + node.pos[1] = boundary.top.pos[1] + break + case 'bottom': + node.pos[1] = + boundary.bottom.pos[1] + boundary.bottom.size[1] - node.size[1] + break } } } diff --git a/src/lib/litegraph/src/utils/collections.ts b/src/lib/litegraph/src/utils/collections.ts index 0bec845e70..422d278a63 100644 --- a/src/lib/litegraph/src/utils/collections.ts +++ b/src/lib/litegraph/src/utils/collections.ts @@ -1,8 +1,8 @@ -import type { ConnectingLink, ISlotType, Positionable } from "../interfaces" -import type { LinkId } from "@/lib/litegraph/src/LLink" +import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LinkId } from '@/lib/litegraph/src/LLink' +import { parseSlotTypes } from '@/lib/litegraph/src/strings' -import { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import { parseSlotTypes } from "@/lib/litegraph/src/strings" +import type { ConnectingLink, ISlotType, Positionable } from '../interfaces' /** * Creates a flat set of all positionable items by recursively iterating through all child items. @@ -11,14 +11,19 @@ import { parseSlotTypes } from "@/lib/litegraph/src/strings" * @param items The original set of items to iterate through * @returns All unpinned items in the original set, and recursively, their children */ -export function getAllNestedItems(items: ReadonlySet): Set { +export function getAllNestedItems( + items: ReadonlySet +): Set { const allItems = new Set() if (items) { for (const item of items) addRecursively(item, allItems) } return allItems - function addRecursively(item: Positionable, flatSet: Set): void { + function addRecursively( + item: Positionable, + flatSet: Set + ): void { if (flatSet.has(item) || item.pinned) return flatSet.add(item) if (item.children) { @@ -32,14 +37,19 @@ export function getAllNestedItems(items: ReadonlySet): Set): LGraphNode | undefined { +export function findFirstNode( + items: Iterable +): LGraphNode | undefined { for (const item of items) { if (item instanceof LGraphNode) return item } } /** @returns `true` if the provided link ID is currently being dragged. */ -export function isDraggingLink(linkId: LinkId, connectingLinks: ConnectingLink[] | null | undefined): ConnectingLink | undefined { +export function isDraggingLink( + linkId: LinkId, + connectingLinks: ConnectingLink[] | null | undefined +): ConnectingLink | undefined { if (connectingLinks == null) return for (const connectingLink of connectingLinks) { @@ -48,7 +58,9 @@ export function isDraggingLink(linkId: LinkId, connectingLinks: ConnectingLink[] } } -type FreeSlotResult = { index: number, slot: T } | undefined +type FreeSlotResult = + | { index: number; slot: T } + | undefined /** * Finds the first free in/out slot with any of the comma-delimited types in {@link type}. @@ -65,7 +77,7 @@ type FreeSlotResult = { index: number, slot: T } export function findFreeSlotOfType( slots: T[], type: ISlotType, - hasNoLinks: (slot: T) => boolean, + hasNoLinks: (slot: T) => boolean ) { if (!slots?.length) return @@ -87,7 +99,7 @@ export function findFreeSlotOfType( } // In case we can't find a free slot. occupiedSlot ??= { index, slot } - } else if (!wildSlot && (validType === "*" || slotType === "*")) { + } else if (!wildSlot && (validType === '*' || slotType === '*')) { // Save the first free wildcard slot as a fallback if (hasNoLinks(slot)) { wildSlot = { index, slot } diff --git a/src/lib/litegraph/src/utils/feedback.ts b/src/lib/litegraph/src/utils/feedback.ts index d0d55aa380..8fbe54e207 100644 --- a/src/lib/litegraph/src/utils/feedback.ts +++ b/src/lib/litegraph/src/utils/feedback.ts @@ -1,4 +1,4 @@ -import { LiteGraph } from "@/lib/litegraph/src/litegraph" +import { LiteGraph } from '@/lib/litegraph/src/litegraph' /** Guard against unbound allocation. */ const UNIQUE_MESSAGE_LIMIT = 10_000 diff --git a/src/lib/litegraph/src/utils/object.ts b/src/lib/litegraph/src/utils/object.ts index 9e95fd422b..a6046c7b1d 100644 --- a/src/lib/litegraph/src/utils/object.ts +++ b/src/lib/litegraph/src/utils/object.ts @@ -1,5 +1,8 @@ -export function omitBy(obj: T, predicate: (value: any) => boolean): Partial { +export function omitBy( + obj: T, + predicate: (value: any) => boolean +): Partial { return Object.fromEntries( - Object.entries(obj).filter(([_key, value]) => !predicate(value)), + Object.entries(obj).filter(([_key, value]) => !predicate(value)) ) as Partial } diff --git a/src/lib/litegraph/src/utils/spaceDistribution.ts b/src/lib/litegraph/src/utils/spaceDistribution.ts index b307331908..4db0a46f48 100644 --- a/src/lib/litegraph/src/utils/spaceDistribution.ts +++ b/src/lib/litegraph/src/utils/spaceDistribution.ts @@ -11,7 +11,7 @@ export interface SpaceRequest { */ export function distributeSpace( totalSpace: number, - requests: SpaceRequest[], + requests: SpaceRequest[] ): number[] { // Handle edge cases if (requests.length === 0) return [] @@ -21,14 +21,14 @@ export function distributeSpace( // If we can't meet minimum requirements, return the minimum sizes if (totalSpace < totalMinSize) { - return requests.map(req => req.minSize) + return requests.map((req) => req.minSize) } // Initialize allocations with minimum sizes - let allocations = requests.map(req => ({ + let allocations = requests.map((req) => ({ computedSize: req.minSize, maxSize: req.maxSize ?? Infinity, - remaining: (req.maxSize ?? Infinity) - req.minSize, + remaining: (req.maxSize ?? Infinity) - req.minSize })) // Calculate remaining space to distribute @@ -37,11 +37,11 @@ export function distributeSpace( // Distribute remaining space iteratively while ( remainingSpace > 0 && - allocations.some(alloc => alloc.remaining > 0) + allocations.some((alloc) => alloc.remaining > 0) ) { // Count items that can still grow const growableItems = allocations.filter( - alloc => alloc.remaining > 0, + (alloc) => alloc.remaining > 0 ).length if (growableItems === 0) break @@ -62,7 +62,7 @@ export function distributeSpace( return { ...alloc, computedSize: alloc.computedSize + growth, - remaining: alloc.remaining - growth, + remaining: alloc.remaining - growth } }) diff --git a/src/lib/litegraph/src/utils/textUtils.ts b/src/lib/litegraph/src/utils/textUtils.ts index d75c3b7e75..d3bc0c49b8 100644 --- a/src/lib/litegraph/src/utils/textUtils.ts +++ b/src/lib/litegraph/src/utils/textUtils.ts @@ -10,7 +10,7 @@ export function truncateText( ctx: CanvasRenderingContext2D, text: string, maxWidth: number, - ellipsis: string = "...", + ellipsis: string = '...' ): string { const textWidth = ctx.measureText(text).width diff --git a/src/lib/litegraph/src/utils/type.ts b/src/lib/litegraph/src/utils/type.ts index 21d795755f..57c45872a8 100644 --- a/src/lib/litegraph/src/utils/type.ts +++ b/src/lib/litegraph/src/utils/type.ts @@ -1,4 +1,4 @@ -import type { IColorable } from "@/lib/litegraph/src/interfaces" +import type { IColorable } from '@/lib/litegraph/src/interfaces' /** * Converts a plain object to a class instance if it is not already an instance of the class. @@ -19,5 +19,10 @@ export function toClass( * Checks if an object is an instance of {@link IColorable}. */ export function isColorable(obj: unknown): obj is IColorable { - return typeof obj === "object" && obj !== null && "setColorOption" in obj && "getColorOption" in obj + return ( + typeof obj === 'object' && + obj !== null && + 'setColorOption' in obj && + 'getColorOption' in obj + ) } diff --git a/src/lib/litegraph/src/utils/uuid.ts b/src/lib/litegraph/src/utils/uuid.ts index a9ee701097..941b127b7d 100644 --- a/src/lib/litegraph/src/utils/uuid.ts +++ b/src/lib/litegraph/src/utils/uuid.ts @@ -2,7 +2,7 @@ export type UUID = string /** Special-case zero-UUID, consisting entirely of zeros. Used as a default value. */ -export const zeroUuid = "00000000-0000-0000-0000-000000000000" +export const zeroUuid = '00000000-0000-0000-0000-000000000000' /** Pre-allocated storage for uuid random values. */ const randomStorage = new Uint32Array(31) @@ -17,13 +17,18 @@ const randomStorage = new Uint32Array(31) * {@link crypto.getRandomValues}, then finally the legacy {@link Math.random} method. */ export function createUuidv4(): UUID { - if (typeof crypto?.randomUUID === "function") return crypto.randomUUID() - if (typeof crypto?.getRandomValues === "function") { + if (typeof crypto?.randomUUID === 'function') return crypto.randomUUID() + if (typeof crypto?.getRandomValues === 'function') { const random = crypto.getRandomValues(randomStorage) let i = 0 - return "10000000-1000-4000-8000-100000000000".replaceAll(/[018]/g, a => - (Number(a) ^ ((random[i++] * 3.725_290_298_461_914e-9) >> (Number(a) * 0.25))).toString(16)) + return '10000000-1000-4000-8000-100000000000'.replaceAll(/[018]/g, (a) => + ( + Number(a) ^ + ((random[i++] * 3.725_290_298_461_914e-9) >> (Number(a) * 0.25)) + ).toString(16) + ) } - return "10000000-1000-4000-8000-100000000000".replaceAll(/[018]/g, a => - (Number(a) ^ ((Math.random() * 16) >> (Number(a) * 0.25))).toString(16)) + return '10000000-1000-4000-8000-100000000000'.replaceAll(/[018]/g, (a) => + (Number(a) ^ ((Math.random() * 16) >> (Number(a) * 0.25))).toString(16) + ) } diff --git a/src/lib/litegraph/src/utils/widget.ts b/src/lib/litegraph/src/utils/widget.ts index 1628a9dfdb..e5d7369013 100644 --- a/src/lib/litegraph/src/utils/widget.ts +++ b/src/lib/litegraph/src/utils/widget.ts @@ -1,4 +1,4 @@ -import type { IWidgetOptions } from "@/lib/litegraph/src/types/widgets" +import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets' /** * The step value for numeric widgets. @@ -6,5 +6,5 @@ import type { IWidgetOptions } from "@/lib/litegraph/src/types/widgets" * {@link IWidgetOptions.step} which is scaled up by 10x in the legacy frontend logic. */ export function getWidgetStep(options: IWidgetOptions): number { - return options.step2 || ((options.step || 10) * 0.1) + return options.step2 || (options.step || 10) * 0.1 } diff --git a/src/lib/litegraph/src/widgets/BaseSteppedWidget.ts b/src/lib/litegraph/src/widgets/BaseSteppedWidget.ts index 71facd9687..a593497d92 100644 --- a/src/lib/litegraph/src/widgets/BaseSteppedWidget.ts +++ b/src/lib/litegraph/src/widgets/BaseSteppedWidget.ts @@ -1,11 +1,17 @@ -import type { IBaseWidget } from "@/lib/litegraph/src/types/widgets" +import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' -import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget" +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' /** * Base class for widgets that have increment and decrement buttons. */ -export abstract class BaseSteppedWidget extends BaseWidget { +export abstract class BaseSteppedWidget< + TWidget extends IBaseWidget = IBaseWidget +> extends BaseWidget { /** * Whether the widget can increment its value * @returns `true` if the widget can increment its value, otherwise `false` @@ -55,7 +61,10 @@ export abstract class BaseSteppedWidget implements IBaseWidget { +export abstract class BaseWidget + implements IBaseWidget +{ /** From node edge to widget edge */ static margin = 15 /** From widget edge to tip of arrow button */ @@ -58,9 +64,9 @@ export abstract class BaseWidget impl linkedWidgets?: IBaseWidget[] name: string - options: TWidget["options"] + options: TWidget['options'] label?: string - type: TWidget["type"] + type: TWidget['type'] y: number = 0 last_y?: number width?: number @@ -75,18 +81,26 @@ export abstract class BaseWidget impl canvas?: LGraphCanvas, node?: LGraphNode, pos?: Point, - e?: CanvasPointerEvent, + e?: CanvasPointerEvent ): void - mouse?(event: CanvasPointerEvent, pointerOffset: Point, node: LGraphNode): boolean + mouse?( + event: CanvasPointerEvent, + pointerOffset: Point, + node: LGraphNode + ): boolean computeSize?(width?: number): Size - onPointerDown?(pointer: CanvasPointer, node: LGraphNode, canvas: LGraphCanvas): boolean + onPointerDown?( + pointer: CanvasPointer, + node: LGraphNode, + canvas: LGraphCanvas + ): boolean - #value?: TWidget["value"] - get value(): TWidget["value"] { + #value?: TWidget['value'] + get value(): TWidget['value'] { return this.#value } - set value(value: TWidget["value"]) { + set value(value: TWidget['value']) { this.#value = value } @@ -105,15 +119,37 @@ export abstract class BaseWidget impl // `node` has no setter - Object.assign will throw. // TODO: Resolve this workaround. Ref: https://github.com/Comfy-Org/litegraph.js/issues/1022 - // @ts-expect-error Prevent naming conflicts with custom nodes. // eslint-disable-next-line unused-imports/no-unused-vars - const { node: _, outline_color, background_color, height, text_color, secondary_text_color, disabledTextColor, displayName, displayValue, labelBaseline, ...safeValues } = widget + const { + node: _, + // @ts-expect-error Prevent naming conflicts with custom nodes. + outline_color, + // @ts-expect-error Prevent naming conflicts with custom nodes. + background_color, + // @ts-expect-error Prevent naming conflicts with custom nodes. + height, + // @ts-expect-error Prevent naming conflicts with custom nodes. + text_color, + // @ts-expect-error Prevent naming conflicts with custom nodes. + secondary_text_color, + // @ts-expect-error Prevent naming conflicts with custom nodes. + disabledTextColor, + // @ts-expect-error Prevent naming conflicts with custom nodes. + displayName, + // @ts-expect-error Prevent naming conflicts with custom nodes. + displayValue, + // @ts-expect-error Prevent naming conflicts with custom nodes. + labelBaseline, + ...safeValues + } = widget Object.assign(this, safeValues) } get outline_color() { - return this.advanced ? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR : LiteGraph.WIDGET_OUTLINE_COLOR + return this.advanced + ? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR + : LiteGraph.WIDGET_OUTLINE_COLOR } get background_color() { @@ -142,7 +178,7 @@ export abstract class BaseWidget impl // TODO: Resolve this workaround. Ref: https://github.com/Comfy-Org/litegraph.js/issues/1022 get _displayValue(): string { - return this.computedDisabled ? "" : String(this.value) + return this.computedDisabled ? '' : String(this.value) } get labelBaseline() { @@ -156,7 +192,10 @@ export abstract class BaseWidget impl * @remarks Not naming this `draw` as `draw` conflicts with the `draw` method in * custom widgets. */ - abstract drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void + abstract drawWidget( + ctx: CanvasRenderingContext2D, + options: DrawWidgetOptions + ): void /** * Draws the standard widget shape - elongated capsule. The path of the widget shape is not @@ -165,11 +204,14 @@ export abstract class BaseWidget impl * @param options The options for drawing the widget * @remarks Leaves {@link ctx} dirty. */ - protected drawWidgetShape(ctx: CanvasRenderingContext2D, { width, showText }: DrawWidgetOptions): void { + protected drawWidgetShape( + ctx: CanvasRenderingContext2D, + { width, showText }: DrawWidgetOptions + ): void { const { height, y } = this const { margin } = BaseWidget - ctx.textAlign = "left" + ctx.textAlign = 'left' ctx.strokeStyle = this.outline_color ctx.fillStyle = this.background_color ctx.beginPath() @@ -191,7 +233,7 @@ export abstract class BaseWidget impl ctx, width, leftPadding = 5, - rightPadding = 20, + rightPadding = 20 }: DrawTruncatingTextOptions): void { const { height, y } = this const { margin } = BaseWidget @@ -213,13 +255,13 @@ export abstract class BaseWidget impl if (requiredWidth <= totalWidth) { // Draw label & value normally - drawTextInArea({ ctx, text: displayName, area, align: "left" }) + drawTextInArea({ ctx, text: displayName, area, align: 'left' }) } else if (LiteGraph.truncateWidgetTextEvenly) { // Label + value will not fit - scale evenly to fit const scale = (totalWidth - gap) / (requiredWidth - gap) area.width = labelWidth * scale - drawTextInArea({ ctx, text: displayName, area, align: "left" }) + drawTextInArea({ ctx, text: displayName, area, align: 'left' }) // Move the area to the right to render the value area.right = x + totalWidth @@ -229,22 +271,24 @@ export abstract class BaseWidget impl const cappedLabelWidth = Math.min(labelWidth, totalWidth) area.width = cappedLabelWidth - drawTextInArea({ ctx, text: displayName, area, align: "left" }) + drawTextInArea({ ctx, text: displayName, area, align: 'left' }) area.right = x + totalWidth - area.setWidthRightAnchored(Math.max(totalWidth - gap - cappedLabelWidth, 0)) + area.setWidthRightAnchored( + Math.max(totalWidth - gap - cappedLabelWidth, 0) + ) } else { // Label + value will not fit - scale label first const cappedValueWidth = Math.min(valueWidth, totalWidth) area.width = Math.max(totalWidth - gap - cappedValueWidth, 0) - drawTextInArea({ ctx, text: displayName, area, align: "left" }) + drawTextInArea({ ctx, text: displayName, area, align: 'left' }) area.right = x + totalWidth area.setWidthRightAnchored(cappedValueWidth) } ctx.fillStyle = this.text_color - drawTextInArea({ ctx, text: _displayValue, area, align: "right" }) + drawTextInArea({ ctx, text: _displayValue, area, align: 'right' }) } /** @@ -264,11 +308,14 @@ export abstract class BaseWidget impl * @param value The value to set * @param options The options for setting the value */ - setValue(value: TWidget["value"], { e, node, canvas }: WidgetEventOptions): void { + setValue( + value: TWidget['value'], + { e, node, canvas }: WidgetEventOptions + ): void { const oldValue = this.value if (value === this.value) return - const v = this.type === "number" ? Number(value) : value + const v = this.type === 'number' ? Number(value) : value this.value = v if ( this.options?.property && @@ -279,7 +326,7 @@ export abstract class BaseWidget impl const pos = canvas.graph_mouse this.callback?.(this.value, canvas, node, pos, e) - node.onWidgetChanged?.(this.name ?? "", v, oldValue, this) + node.onWidgetChanged?.(this.name ?? '', v, oldValue, this) if (node.graph) node.graph._version++ } diff --git a/src/lib/litegraph/src/widgets/BooleanWidget.ts b/src/lib/litegraph/src/widgets/BooleanWidget.ts index b6903528d5..1a2b3ff067 100644 --- a/src/lib/litegraph/src/widgets/BooleanWidget.ts +++ b/src/lib/litegraph/src/widgets/BooleanWidget.ts @@ -1,28 +1,29 @@ -import type { IBooleanWidget } from "@/lib/litegraph/src/types/widgets" +import type { IBooleanWidget } from '@/lib/litegraph/src/types/widgets' -import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget" +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' -export class BooleanWidget extends BaseWidget implements IBooleanWidget { - override type = "toggle" as const +export class BooleanWidget + extends BaseWidget + implements IBooleanWidget +{ + override type = 'toggle' as const - override drawWidget(ctx: CanvasRenderingContext2D, { - width, - showText = true, - }: DrawWidgetOptions) { + override drawWidget( + ctx: CanvasRenderingContext2D, + { width, showText = true }: DrawWidgetOptions + ) { const { height, y } = this const { margin } = BaseWidget this.drawWidgetShape(ctx, { width, showText }) - ctx.fillStyle = this.value ? "#89A" : "#333" + ctx.fillStyle = this.value ? '#89A' : '#333' ctx.beginPath() - ctx.arc( - width - margin * 2, - y + height * 0.5, - height * 0.36, - 0, - Math.PI * 2, - ) + ctx.arc(width - margin * 2, y + height * 0.5, height * 0.36, 0, Math.PI * 2) ctx.fill() if (showText) { @@ -41,8 +42,10 @@ export class BooleanWidget extends BaseWidget implements IBoolea drawValue(ctx: CanvasRenderingContext2D, x: number): void { // Draw value ctx.fillStyle = this.value ? this.text_color : this.secondary_text_color - ctx.textAlign = "right" - const value = this.value ? this.options.on || "true" : this.options.off || "false" + ctx.textAlign = 'right' + const value = this.value + ? this.options.on || 'true' + : this.options.off || 'false' ctx.fillText(value, x, this.labelBaseline) } diff --git a/src/lib/litegraph/src/widgets/ButtonWidget.ts b/src/lib/litegraph/src/widgets/ButtonWidget.ts index bbee612ba1..650ffbbea0 100644 --- a/src/lib/litegraph/src/widgets/ButtonWidget.ts +++ b/src/lib/litegraph/src/widgets/ButtonWidget.ts @@ -1,10 +1,17 @@ -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { IButtonWidget } from "@/lib/litegraph/src/types/widgets" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { IButtonWidget } from '@/lib/litegraph/src/types/widgets' -import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget" +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' -export class ButtonWidget extends BaseWidget implements IButtonWidget { - override type = "button" as const +export class ButtonWidget + extends BaseWidget + implements IButtonWidget +{ + override type = 'button' as const clicked: boolean constructor(widget: IButtonWidget, node: LGraphNode) { @@ -17,10 +24,10 @@ export class ButtonWidget extends BaseWidget implements IButtonWi * @param ctx The canvas context * @param options The options for drawing the widget */ - override drawWidget(ctx: CanvasRenderingContext2D, { - width, - showText = true, - }: DrawWidgetOptions) { + override drawWidget( + ctx: CanvasRenderingContext2D, + { width, showText = true }: DrawWidgetOptions + ) { // Store original context attributes const { fillStyle, strokeStyle, textAlign } = ctx @@ -30,7 +37,7 @@ export class ButtonWidget extends BaseWidget implements IButtonWi // Draw button background ctx.fillStyle = this.background_color if (this.clicked) { - ctx.fillStyle = "#AAA" + ctx.fillStyle = '#AAA' this.clicked = false } ctx.fillRect(margin, y, width - margin * 2, height) @@ -49,7 +56,7 @@ export class ButtonWidget extends BaseWidget implements IButtonWi } drawLabel(ctx: CanvasRenderingContext2D, x: number): void { - ctx.textAlign = "center" + ctx.textAlign = 'center' ctx.fillStyle = this.text_color ctx.fillText(this.displayName, x, this.y + this.height * 0.7) } diff --git a/src/lib/litegraph/src/widgets/ComboWidget.ts b/src/lib/litegraph/src/widgets/ComboWidget.ts index 56b8dbc932..f4cbbf850e 100644 --- a/src/lib/litegraph/src/widgets/ComboWidget.ts +++ b/src/lib/litegraph/src/widgets/ComboWidget.ts @@ -1,11 +1,13 @@ -import type { WidgetEventOptions } from "./BaseWidget" -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { IComboWidget, IStringComboWidget } from "@/lib/litegraph/src/types/widgets" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LiteGraph, clamp } from '@/lib/litegraph/src/litegraph' +import type { + IComboWidget, + IStringComboWidget +} from '@/lib/litegraph/src/types/widgets' +import { warnDeprecated } from '@/lib/litegraph/src/utils/feedback' -import { clamp, LiteGraph } from "@/lib/litegraph/src/litegraph" -import { warnDeprecated } from "@/lib/litegraph/src/utils/feedback" - -import { BaseSteppedWidget } from "./BaseSteppedWidget" +import { BaseSteppedWidget } from './BaseSteppedWidget' +import type { WidgetEventOptions } from './BaseWidget' /** * This is used as an (invalid) assertion to resolve issues with legacy duck-typed values. @@ -13,35 +15,39 @@ import { BaseSteppedWidget } from "./BaseSteppedWidget" * Function style in use by: * https://github.com/kijai/ComfyUI-KJNodes/blob/c3dc82108a2a86c17094107ead61d63f8c76200e/web/js/setgetnodes.js#L401-L404 */ -type Values = string[] | Record | ((widget?: ComboWidget, node?: LGraphNode) => string[]) +type Values = + | string[] + | Record + | ((widget?: ComboWidget, node?: LGraphNode) => string[]) function toArray(values: Values): string[] { return Array.isArray(values) ? values : Object.keys(values) } -export class ComboWidget extends BaseSteppedWidget implements IComboWidget { - override type = "combo" as const +export class ComboWidget + extends BaseSteppedWidget + implements IComboWidget +{ + override type = 'combo' as const override get _displayValue() { - if (this.computedDisabled) return "" + if (this.computedDisabled) return '' const { values: rawValues } = this.options if (rawValues) { - const values = typeof rawValues === "function" ? rawValues() : rawValues + const values = typeof rawValues === 'function' ? rawValues() : rawValues if (values && !Array.isArray(values)) { return values[this.value] } } - return typeof this.value === "number" ? String(this.value) : this.value + return typeof this.value === 'number' ? String(this.value) : this.value } #getValues(node: LGraphNode): Values { const { values } = this.options - if (values == null) throw new Error("[ComboWidget]: values is required") + if (values == null) throw new Error('[ComboWidget]: values is required') - return typeof values === "function" - ? values(this, node) - : values + return typeof values === 'function' ? values(this, node) : values } /** @@ -52,7 +58,7 @@ export class ComboWidget extends BaseSteppedWidget 1)) return false @@ -92,16 +98,15 @@ export class ComboWidget extends BaseSteppedWidget { this.setValue( - values != values_list - ? text_values.indexOf(value) - : value, - { e, node, canvas }, + values != values_list ? text_values.indexOf(value) : value, + { e, node, canvas } ) - }, + } }) } } diff --git a/src/lib/litegraph/src/widgets/KnobWidget.ts b/src/lib/litegraph/src/widgets/KnobWidget.ts index 6caf6fadbe..da789c4893 100644 --- a/src/lib/litegraph/src/widgets/KnobWidget.ts +++ b/src/lib/litegraph/src/widgets/KnobWidget.ts @@ -1,12 +1,15 @@ -import type { IKnobWidget } from "@/lib/litegraph/src/types/widgets" +import { clamp } from '@/lib/litegraph/src/litegraph' +import type { IKnobWidget } from '@/lib/litegraph/src/types/widgets' +import { getWidgetStep } from '@/lib/litegraph/src/utils/widget' -import { clamp } from "@/lib/litegraph/src/litegraph" -import { getWidgetStep } from "@/lib/litegraph/src/utils/widget" - -import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget" +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' export class KnobWidget extends BaseWidget implements IKnobWidget { - override type = "knob" as const + override type = 'knob' as const /** * Compute the layout size of the widget. @@ -22,7 +25,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { minHeight: 60, minWidth: 20, maxHeight: 1_000_000, - maxWidth: 1_000_000, + maxWidth: 1_000_000 } } @@ -32,10 +35,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { drawWidget( ctx: CanvasRenderingContext2D, - { - width, - showText = true, - }: DrawWidgetOptions, + { width, showText = true }: DrawWidgetOptions ): void { // Store original context attributes const { fillStyle, strokeStyle, textAlign } = ctx @@ -43,7 +43,8 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { const { y } = this const { margin } = BaseWidget - const { gradient_stops = "rgb(14, 182, 201); rgb(0, 216, 72)" } = this.options + const { gradient_stops = 'rgb(14, 182, 201); rgb(0, 216, 72)' } = + this.options const effective_height = this.computedHeight || this.height // Draw background const size_modifier = @@ -54,7 +55,8 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { const arc_size = (Math.min(width, effective_height) - margin * size_modifier - - ctx.lineWidth) / 2 + ctx.lineWidth) / + 2 { const gradient = ctx.createRadialGradient( arc_center.x, @@ -62,10 +64,10 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { arc_size + ctx.lineWidth, 0, 0, - arc_size + ctx.lineWidth, + arc_size + ctx.lineWidth ) - gradient.addColorStop(0, "rgb(29, 29, 29)") - gradient.addColorStop(1, "rgb(116, 116, 116)") + gradient.addColorStop(0, 'rgb(29, 29, 29)') + gradient.addColorStop(1, 'rgb(116, 116, 116)') ctx.fillStyle = gradient } ctx.beginPath() @@ -77,7 +79,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { arc_size + ctx.lineWidth / 2, 0, Math.PI * 2, - false, + false ) ctx.fill() ctx.closePath() @@ -86,7 +88,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { // Draw knob's background const arc = { start_angle: Math.PI * 0.6, - end_angle: Math.PI * 2.4, + end_angle: Math.PI * 2.4 } ctx.beginPath() { @@ -96,10 +98,10 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { arc_size + ctx.lineWidth, 0, 0, - arc_size + ctx.lineWidth, + arc_size + ctx.lineWidth ) - gradient.addColorStop(0, "rgb(99, 99, 99)") - gradient.addColorStop(1, "rgb(36, 36, 36)") + gradient.addColorStop(0, 'rgb(99, 99, 99)') + gradient.addColorStop(1, 'rgb(36, 36, 36)') ctx.strokeStyle = gradient } ctx.arc( @@ -108,7 +110,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { arc_size, arc.start_angle, arc.end_angle, - false, + false ) ctx.stroke() ctx.closePath() @@ -122,9 +124,9 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { const gradient = ctx.createConicGradient( arc.start_angle, arc_center.x, - arc_center.y, + arc_center.y ) - const gs = gradient_stops.split(";") + const gs = gradient_stops.split(';') for (const [index, stop] of gs.entries()) { gradient.addColorStop(index, stop.trim()) } @@ -138,7 +140,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { arc_size, arc.start_angle, value_end_angle, - false, + false ) ctx.stroke() ctx.closePath() @@ -155,7 +157,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { arc_size + ctx.lineWidth / 2, 0, Math.PI * 2, - false, + false ) ctx.lineWidth = 1 ctx.stroke() @@ -167,13 +169,13 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { // Draw text if (showText) { - ctx.textAlign = "center" + ctx.textAlign = 'center' ctx.fillStyle = this.text_color const fixedValue = Number(this.value).toFixed(this.options.precision ?? 3) ctx.fillText( `${this.label || this.name}\n${fixedValue}`, width * 0.5, - y + effective_height * 0.5, + y + effective_height * 0.5 ) } @@ -191,13 +193,19 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { const { e } = options const step = getWidgetStep(this.options) // Shift to move by 10% increments - const range = (this.options.max - this.options.min) + const range = this.options.max - this.options.min const range_10_percent = range / 10 const range_1_percent = range / 100 const step_for = { delta_x: step, - shift: range_10_percent > step ? range_10_percent - (range_10_percent % step) : step, - delta_y: range_1_percent > step ? range_1_percent - (range_1_percent % step) : step, // 1% increments + shift: + range_10_percent > step + ? range_10_percent - (range_10_percent % step) + : step, + delta_y: + range_1_percent > step + ? range_1_percent - (range_1_percent % step) + : step // 1% increments } const use_y = Math.abs(e.movementY) > Math.abs(e.movementX) @@ -216,15 +224,15 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { const step_with_shift_modifier = e.shiftKey ? step_for.shift - : (use_y + : use_y ? step_for.delta_y - : step) + : step const deltaValue = adjustment * step_with_shift_modifier const newValue = clamp( this.value + deltaValue, this.options.min, - this.options.max, + this.options.max ) if (newValue !== this.value) { this.setValue(newValue, options) diff --git a/src/lib/litegraph/src/widgets/LegacyWidget.ts b/src/lib/litegraph/src/widgets/LegacyWidget.ts index c812fb4833..273f610b6c 100644 --- a/src/lib/litegraph/src/widgets/LegacyWidget.ts +++ b/src/lib/litegraph/src/widgets/LegacyWidget.ts @@ -1,9 +1,8 @@ -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { IBaseWidget } from "@/lib/litegraph/src/types/widgets" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' -import { LiteGraph } from "@/lib/litegraph/src/litegraph" - -import { BaseWidget, type DrawWidgetOptions } from "./BaseWidget" +import { BaseWidget, type DrawWidgetOptions } from './BaseWidget' /** * Wraps a legacy POJO custom widget, so that all widgets may be called via the same internal interface. @@ -11,22 +10,30 @@ import { BaseWidget, type DrawWidgetOptions } from "./BaseWidget" * Support will eventually be removed. * @remarks Expect this class to undergo breaking changes without warning. */ -export class LegacyWidget extends BaseWidget implements IBaseWidget { +export class LegacyWidget + extends BaseWidget + implements IBaseWidget +{ override draw?( ctx: CanvasRenderingContext2D, node: LGraphNode, widget_width: number, y: number, H: number, - lowQuality?: boolean, + lowQuality?: boolean ): void - override drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions) { + override drawWidget( + ctx: CanvasRenderingContext2D, + options: DrawWidgetOptions + ) { const H = LiteGraph.NODE_WIDGET_HEIGHT this.draw?.(ctx, this.node, options.width, this.y, H, !!options.showText) } override onClick() { - console.warn("Custom widget wrapper onClick was just called. Handling for third party widgets is done via LGraphCanvas - the mouse callback.") + console.warn( + 'Custom widget wrapper onClick was just called. Handling for third party widgets is done via LGraphCanvas - the mouse callback.' + ) } } diff --git a/src/lib/litegraph/src/widgets/NumberWidget.ts b/src/lib/litegraph/src/widgets/NumberWidget.ts index 7bbf81c75f..5b7b5d94ca 100644 --- a/src/lib/litegraph/src/widgets/NumberWidget.ts +++ b/src/lib/litegraph/src/widgets/NumberWidget.ts @@ -1,19 +1,19 @@ -import type { WidgetEventOptions } from "./BaseWidget" -import type { INumericWidget } from "@/lib/litegraph/src/types/widgets" +import type { INumericWidget } from '@/lib/litegraph/src/types/widgets' +import { getWidgetStep } from '@/lib/litegraph/src/utils/widget' -import { getWidgetStep } from "@/lib/litegraph/src/utils/widget" +import { BaseSteppedWidget } from './BaseSteppedWidget' +import type { WidgetEventOptions } from './BaseWidget' -import { BaseSteppedWidget } from "./BaseSteppedWidget" - -export class NumberWidget extends BaseSteppedWidget implements INumericWidget { - override type = "number" as const +export class NumberWidget + extends BaseSteppedWidget + implements INumericWidget +{ + override type = 'number' as const override get _displayValue() { - if (this.computedDisabled) return "" + if (this.computedDisabled) return '' return Number(this.value).toFixed( - this.options.precision !== undefined - ? this.options.precision - : 3, + this.options.precision !== undefined ? this.options.precision : 3 ) } @@ -51,32 +51,37 @@ export class NumberWidget extends BaseSteppedWidget implements I const width = this.width || node.size[0] // Determine if clicked on left/right arrows - const delta = x < 40 - ? -1 - : (x > width - 40 - ? 1 - : 0) + const delta = x < 40 ? -1 : x > width - 40 ? 1 : 0 if (delta) { // Handle left/right arrow clicks - this.setValue(this.value + delta * getWidgetStep(this.options), { e, node, canvas }) + this.setValue(this.value + delta * getWidgetStep(this.options), { + e, + node, + canvas + }) return } // Handle center click - show prompt - canvas.prompt("Value", this.value, (v: string) => { - // Check if v is a valid equation or a number - if (/^[\d\s()*+/-]+|\d+\.\d+$/.test(v)) { - // Solve the equation if possible - try { - v = eval(v) - } catch {} - } - const newValue = Number(v) - if (!isNaN(newValue)) { - this.setValue(newValue, { e, node, canvas }) - } - }, e) + canvas.prompt( + 'Value', + this.value, + (v: string) => { + // Check if v is a valid equation or a number + if (/^[\d\s()*+/-]+|\d+\.\d+$/.test(v)) { + // Solve the equation if possible + try { + v = eval(v) + } catch {} + } + const newValue = Number(v) + if (!isNaN(newValue)) { + this.setValue(newValue, { e, node, canvas }) + } + }, + e + ) } /** @@ -86,13 +91,13 @@ export class NumberWidget extends BaseSteppedWidget implements I override onDrag({ e, node, canvas }: WidgetEventOptions) { const width = this.width || node.width const x = e.canvasX - node.pos[0] - const delta = x < 40 - ? -1 - : (x > width - 40 - ? 1 - : 0) + const delta = x < 40 ? -1 : x > width - 40 ? 1 : 0 - if (delta && (x > -3 && x < width + 3)) return - this.setValue(this.value + (e.deltaX ?? 0) * getWidgetStep(this.options), { e, node, canvas }) + if (delta && x > -3 && x < width + 3) return + this.setValue(this.value + (e.deltaX ?? 0) * getWidgetStep(this.options), { + e, + node, + canvas + }) } } diff --git a/src/lib/litegraph/src/widgets/SliderWidget.ts b/src/lib/litegraph/src/widgets/SliderWidget.ts index 811910914a..3aec6224ed 100644 --- a/src/lib/litegraph/src/widgets/SliderWidget.ts +++ b/src/lib/litegraph/src/widgets/SliderWidget.ts @@ -1,11 +1,17 @@ -import type { ISliderWidget } from "@/lib/litegraph/src/types/widgets" +import { clamp } from '@/lib/litegraph/src/litegraph' +import type { ISliderWidget } from '@/lib/litegraph/src/types/widgets' -import { clamp } from "@/lib/litegraph/src/litegraph" +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' -import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget" - -export class SliderWidget extends BaseWidget implements ISliderWidget { - override type = "slider" as const +export class SliderWidget + extends BaseWidget + implements ISliderWidget +{ + override type = 'slider' as const marker?: number @@ -14,10 +20,10 @@ export class SliderWidget extends BaseWidget implements ISliderWi * @param ctx The canvas context * @param options The options for drawing the widget */ - override drawWidget(ctx: CanvasRenderingContext2D, { - width, - showText = true, - }: DrawWidgetOptions) { + override drawWidget( + ctx: CanvasRenderingContext2D, + { width, showText = true }: DrawWidgetOptions + ) { // Store original context attributes const { fillStyle, strokeStyle, textAlign } = ctx @@ -34,7 +40,7 @@ export class SliderWidget extends BaseWidget implements ISliderWi nvalue = clamp(nvalue, 0, 1) // Draw slider bar - ctx.fillStyle = this.options.slider_color ?? "#678" + ctx.fillStyle = this.options.slider_color ?? '#678' ctx.fillRect(margin, y, nvalue * (width - margin * 2), height) // Draw outline if not disabled @@ -47,24 +53,19 @@ export class SliderWidget extends BaseWidget implements ISliderWi if (this.marker != null) { let marker_nvalue = (this.marker - this.options.min) / range marker_nvalue = clamp(marker_nvalue, 0, 1) - ctx.fillStyle = this.options.marker_color ?? "#AA9" - ctx.fillRect( - margin + marker_nvalue * (width - margin * 2), - y, - 2, - height, - ) + ctx.fillStyle = this.options.marker_color ?? '#AA9' + ctx.fillRect(margin + marker_nvalue * (width - margin * 2), y, 2, height) } // Draw text if (showText) { - ctx.textAlign = "center" + ctx.textAlign = 'center' ctx.fillStyle = this.text_color const fixedValue = Number(this.value).toFixed(this.options.precision ?? 3) ctx.fillText( `${this.label || this.name} ${fixedValue}`, width * 0.5, - y + height * 0.7, + y + height * 0.7 ) } @@ -84,7 +85,8 @@ export class SliderWidget extends BaseWidget implements ISliderWi // Calculate new value based on click position const slideFactor = clamp((x - 15) / (width - 30), 0, 1) - const newValue = this.options.min + (this.options.max - this.options.min) * slideFactor + const newValue = + this.options.min + (this.options.max - this.options.min) * slideFactor if (newValue !== this.value) { this.setValue(newValue, options) @@ -103,7 +105,8 @@ export class SliderWidget extends BaseWidget implements ISliderWi // Calculate new value based on drag position const slideFactor = clamp((x - 15) / (width - 30), 0, 1) - const newValue = this.options.min + (this.options.max - this.options.min) * slideFactor + const newValue = + this.options.min + (this.options.max - this.options.min) * slideFactor if (newValue !== this.value) { this.setValue(newValue, options) diff --git a/src/lib/litegraph/src/widgets/TextWidget.ts b/src/lib/litegraph/src/widgets/TextWidget.ts index a15ce05327..8cb27d20b2 100644 --- a/src/lib/litegraph/src/widgets/TextWidget.ts +++ b/src/lib/litegraph/src/widgets/TextWidget.ts @@ -1,13 +1,20 @@ -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" -import type { IStringWidget } from "@/lib/litegraph/src/types/widgets" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { IStringWidget } from '@/lib/litegraph/src/types/widgets' -import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget" +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' -export class TextWidget extends BaseWidget implements IStringWidget { +export class TextWidget + extends BaseWidget + implements IStringWidget +{ constructor(widget: IStringWidget, node: LGraphNode) { super(widget, node) - this.type ??= "string" - this.value = widget.value?.toString() ?? "" + this.type ??= 'string' + this.value = widget.value?.toString() ?? '' } /** @@ -15,10 +22,10 @@ export class TextWidget extends BaseWidget implements IStringWidg * @param ctx The canvas context * @param options The options for drawing the widget */ - override drawWidget(ctx: CanvasRenderingContext2D, { - width, - showText = true, - }: DrawWidgetOptions) { + override drawWidget( + ctx: CanvasRenderingContext2D, + { width, showText = true }: DrawWidgetOptions + ) { // Store original context attributes const { fillStyle, strokeStyle, textAlign } = ctx @@ -35,7 +42,7 @@ export class TextWidget extends BaseWidget implements IStringWidg override onClick({ e, node, canvas }: WidgetEventOptions) { // Show prompt dialog for text input canvas.prompt( - "Value", + 'Value', this.value, (v: string) => { if (v !== null) { @@ -43,7 +50,7 @@ export class TextWidget extends BaseWidget implements IStringWidg } }, e, - this.options?.multiline ?? false, + this.options?.multiline ?? false ) } } diff --git a/src/lib/litegraph/src/widgets/widgetMap.ts b/src/lib/litegraph/src/widgets/widgetMap.ts index 12c39cbe8a..ffbb7ae694 100644 --- a/src/lib/litegraph/src/widgets/widgetMap.ts +++ b/src/lib/litegraph/src/widgets/widgetMap.ts @@ -1,4 +1,4 @@ -import type { LGraphNode } from "@/lib/litegraph/src/LGraphNode" +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import type { IBaseWidget, IBooleanWidget, @@ -10,20 +10,19 @@ import type { ISliderWidget, IStringWidget, IWidget, - TWidgetType, -} from "@/lib/litegraph/src/types/widgets" + TWidgetType +} from '@/lib/litegraph/src/types/widgets' +import { toClass } from '@/lib/litegraph/src/utils/type' -import { toClass } from "@/lib/litegraph/src/utils/type" - -import { BaseWidget } from "./BaseWidget" -import { BooleanWidget } from "./BooleanWidget" -import { ButtonWidget } from "./ButtonWidget" -import { ComboWidget } from "./ComboWidget" -import { KnobWidget } from "./KnobWidget" -import { LegacyWidget } from "./LegacyWidget" -import { NumberWidget } from "./NumberWidget" -import { SliderWidget } from "./SliderWidget" -import { TextWidget } from "./TextWidget" +import { BaseWidget } from './BaseWidget' +import { BooleanWidget } from './BooleanWidget' +import { ButtonWidget } from './ButtonWidget' +import { ComboWidget } from './ComboWidget' +import { KnobWidget } from './KnobWidget' +import { LegacyWidget } from './LegacyWidget' +import { NumberWidget } from './NumberWidget' +import { SliderWidget } from './SliderWidget' +import { TextWidget } from './TextWidget' export type WidgetTypeMap = { button: ButtonWidget @@ -48,17 +47,18 @@ export type WidgetTypeMap = { export function toConcreteWidget( widget: TWidget, node: LGraphNode, - wrapLegacyWidgets?: true, -): WidgetTypeMap[TWidget["type"]] + wrapLegacyWidgets?: true +): WidgetTypeMap[TWidget['type']] export function toConcreteWidget( widget: TWidget, node: LGraphNode, - wrapLegacyWidgets: false): WidgetTypeMap[TWidget["type"]] | undefined + wrapLegacyWidgets: false +): WidgetTypeMap[TWidget['type']] | undefined export function toConcreteWidget( widget: TWidget, node: LGraphNode, - wrapLegacyWidgets = true, -): WidgetTypeMap[TWidget["type"]] | undefined { + wrapLegacyWidgets = true +): WidgetTypeMap[TWidget['type']] | undefined { if (widget instanceof BaseWidget) return widget // Assertion: TypeScript has no concept of "all strings except X" @@ -66,17 +66,25 @@ export function toConcreteWidget( const narrowedWidget = widget as RemoveBaseWidgetType switch (narrowedWidget.type) { - case "button": return toClass(ButtonWidget, narrowedWidget, node) - case "toggle": return toClass(BooleanWidget, narrowedWidget, node) - case "slider": return toClass(SliderWidget, narrowedWidget, node) - case "knob": return toClass(KnobWidget, narrowedWidget, node) - case "combo": return toClass(ComboWidget, narrowedWidget, node) - case "number": return toClass(NumberWidget, narrowedWidget, node) - case "string": return toClass(TextWidget, narrowedWidget, node) - case "text": return toClass(TextWidget, narrowedWidget, node) - default: { - if (wrapLegacyWidgets) return toClass(LegacyWidget, widget, node) - } + case 'button': + return toClass(ButtonWidget, narrowedWidget, node) + case 'toggle': + return toClass(BooleanWidget, narrowedWidget, node) + case 'slider': + return toClass(SliderWidget, narrowedWidget, node) + case 'knob': + return toClass(KnobWidget, narrowedWidget, node) + case 'combo': + return toClass(ComboWidget, narrowedWidget, node) + case 'number': + return toClass(NumberWidget, narrowedWidget, node) + case 'string': + return toClass(TextWidget, narrowedWidget, node) + case 'text': + return toClass(TextWidget, narrowedWidget, node) + default: { + if (wrapLegacyWidgets) return toClass(LegacyWidget, widget, node) + } } } @@ -84,47 +92,47 @@ export function toConcreteWidget( /** Type guard: Narrow **from {@link IBaseWidget}** to {@link IButtonWidget}. */ export function isButtonWidget(widget: IBaseWidget): widget is IButtonWidget { - return widget.type === "button" + return widget.type === 'button' } /** Type guard: Narrow **from {@link IBaseWidget}** to {@link IBooleanWidget}. */ export function isBooleanWidget(widget: IBaseWidget): widget is IBooleanWidget { - return widget.type === "toggle" + return widget.type === 'toggle' } /** Type guard: Narrow **from {@link IBaseWidget}** to {@link ISliderWidget}. */ export function isSliderWidget(widget: IBaseWidget): widget is ISliderWidget { - return widget.type === "slider" + return widget.type === 'slider' } /** Type guard: Narrow **from {@link IBaseWidget}** to {@link IKnobWidget}. */ export function isKnobWidget(widget: IBaseWidget): widget is IKnobWidget { - return widget.type === "knob" + return widget.type === 'knob' } /** Type guard: Narrow **from {@link IBaseWidget}** to {@link IComboWidget}. */ export function isComboWidget(widget: IBaseWidget): widget is IComboWidget { - return widget.type === "combo" + return widget.type === 'combo' } /** Type guard: Narrow **from {@link IBaseWidget}** to {@link INumericWidget}. */ export function isNumberWidget(widget: IBaseWidget): widget is INumericWidget { - return widget.type === "number" + return widget.type === 'number' } /** Type guard: Narrow **from {@link IBaseWidget}** to {@link IStringWidget}. */ export function isStringWidget(widget: IBaseWidget): widget is IStringWidget { - return widget.type === "string" + return widget.type === 'string' } /** Type guard: Narrow **from {@link IBaseWidget}** to {@link ITextWidget}. */ export function isTextWidget(widget: IBaseWidget): widget is IStringWidget { - return widget.type === "text" + return widget.type === 'text' } /** Type guard: Narrow **from {@link IBaseWidget}** to {@link ICustomWidget}. */ export function isCustomWidget(widget: IBaseWidget): widget is ICustomWidget { - return widget.type === "custom" + return widget.type === 'custom' } // #endregion Type Guards diff --git a/src/lib/litegraph/test/ConfigureGraph.test.ts b/src/lib/litegraph/test/ConfigureGraph.test.ts index 61ce4eec6d..80185171ba 100644 --- a/src/lib/litegraph/test/ConfigureGraph.test.ts +++ b/src/lib/litegraph/test/ConfigureGraph.test.ts @@ -1,17 +1,20 @@ -import { describe } from "vitest" +import { describe } from 'vitest' -import { LGraph } from "@/lib/litegraph/src/litegraph" +import { LGraph } from '@/lib/litegraph/src/litegraph' -import { dirtyTest } from "./testExtensions" +import { dirtyTest } from './testExtensions' -describe("LGraph configure()", () => { - dirtyTest("LGraph matches previous snapshot (normal configure() usage)", ({ expect, minimalSerialisableGraph, basicSerialisableGraph }) => { - const configuredMinGraph = new LGraph() - configuredMinGraph.configure(minimalSerialisableGraph) - expect(configuredMinGraph).toMatchSnapshot("configuredMinGraph") +describe('LGraph configure()', () => { + dirtyTest( + 'LGraph matches previous snapshot (normal configure() usage)', + ({ expect, minimalSerialisableGraph, basicSerialisableGraph }) => { + const configuredMinGraph = new LGraph() + configuredMinGraph.configure(minimalSerialisableGraph) + expect(configuredMinGraph).toMatchSnapshot('configuredMinGraph') - const configuredBasicGraph = new LGraph() - configuredBasicGraph.configure(basicSerialisableGraph) - expect(configuredBasicGraph).toMatchSnapshot("configuredBasicGraph") - }) + const configuredBasicGraph = new LGraph() + configuredBasicGraph.configure(basicSerialisableGraph) + expect(configuredBasicGraph).toMatchSnapshot('configuredBasicGraph') + } + ) }) diff --git a/src/lib/litegraph/test/LGraph.test.ts b/src/lib/litegraph/test/LGraph.test.ts index addce6c02c..b54fd62246 100644 --- a/src/lib/litegraph/test/LGraph.test.ts +++ b/src/lib/litegraph/test/LGraph.test.ts @@ -1,41 +1,44 @@ -import { describe } from "vitest" +import { describe } from 'vitest' -import { LGraph, LiteGraph } from "@/lib/litegraph/src/litegraph" +import { LGraph, LiteGraph } from '@/lib/litegraph/src/litegraph' -import { test } from "./testExtensions" +import { test } from './testExtensions' -describe("LGraph", () => { - test("can be instantiated", ({ expect }) => { +describe('LGraph', () => { + test('can be instantiated', ({ expect }) => { // @ts-expect-error Intentional - extra holds any / all consumer data that should be serialised - const graph = new LGraph({ extra: "TestGraph" }) + const graph = new LGraph({ extra: 'TestGraph' }) expect(graph).toBeInstanceOf(LGraph) - expect(graph.extra).toBe("TestGraph") - expect(graph.extra).toBe("TestGraph") + expect(graph.extra).toBe('TestGraph') + expect(graph.extra).toBe('TestGraph') }) - test("is exactly the same type", async ({ expect }) => { - const directImport = await import("@/lib/litegraph/src/LGraph") - const entryPointImport = await import("@/lib/litegraph/src/litegraph") + test('is exactly the same type', async ({ expect }) => { + const directImport = await import('@/lib/litegraph/src/LGraph') + const entryPointImport = await import('@/lib/litegraph/src/litegraph') expect(LiteGraph.LGraph).toBe(directImport.LGraph) expect(LiteGraph.LGraph).toBe(entryPointImport.LGraph) }) - test("populates optional values", ({ expect, minimalSerialisableGraph }) => { + test('populates optional values', ({ expect, minimalSerialisableGraph }) => { const dGraph = new LGraph(minimalSerialisableGraph) expect(dGraph.links).toBeInstanceOf(Map) expect(dGraph.nodes).toBeInstanceOf(Array) expect(dGraph.groups).toBeInstanceOf(Array) }) - test("supports schema v0.4 graphs", ({ expect, oldSchemaGraph }) => { + test('supports schema v0.4 graphs', ({ expect, oldSchemaGraph }) => { const fromOldSchema = new LGraph(oldSchemaGraph) - expect(fromOldSchema).toMatchSnapshot("oldSchemaGraph") + expect(fromOldSchema).toMatchSnapshot('oldSchemaGraph') }) }) -describe("Floating Links / Reroutes", () => { - test("Floating reroute should be removed when node and link are removed", ({ expect, floatingLinkGraph }) => { +describe('Floating Links / Reroutes', () => { + test('Floating reroute should be removed when node and link are removed', ({ + expect, + floatingLinkGraph + }) => { const graph = new LGraph(floatingLinkGraph) expect(graph.nodes.length).toBe(1) graph.remove(graph.nodes[0]) @@ -45,7 +48,7 @@ describe("Floating Links / Reroutes", () => { expect(graph.reroutes.size).toBe(0) }) - test("Can add reroute to existing link", ({ expect, linkedNodesGraph }) => { + test('Can add reroute to existing link', ({ expect, linkedNodesGraph }) => { const graph = new LGraph(linkedNodesGraph) expect(graph.nodes.length).toBe(2) expect(graph.links.size).toBe(1) @@ -56,7 +59,10 @@ describe("Floating Links / Reroutes", () => { expect(graph.reroutes.size).toBe(1) }) - test("Create floating reroute when one side of node is removed", ({ expect, linkedNodesGraph }) => { + test('Create floating reroute when one side of node is removed', ({ + expect, + linkedNodesGraph + }) => { const graph = new LGraph(linkedNodesGraph) graph.createReroute([0, 0], graph.links.values().next().value!) graph.remove(graph.nodes[0]) @@ -67,7 +73,10 @@ describe("Floating Links / Reroutes", () => { expect(graph.reroutes.values().next().value!.floating).not.toBeUndefined() }) - test("Create floating reroute when one side of link is removed", ({ expect, linkedNodesGraph }) => { + test('Create floating reroute when one side of link is removed', ({ + expect, + linkedNodesGraph + }) => { const graph = new LGraph(linkedNodesGraph) graph.createReroute([0, 0], graph.links.values().next().value!) graph.nodes[0].disconnectOutput(0) @@ -78,7 +87,10 @@ describe("Floating Links / Reroutes", () => { expect(graph.reroutes.values().next().value!.floating).not.toBeUndefined() }) - test("Reroutes and branches should be retained when the input node is removed", ({ expect, floatingBranchGraph: graph }) => { + test('Reroutes and branches should be retained when the input node is removed', ({ + expect, + floatingBranchGraph: graph + }) => { expect(graph.nodes.length).toBe(3) graph.remove(graph.nodes[2]) expect(graph.nodes.length).toBe(2) @@ -92,7 +104,10 @@ describe("Floating Links / Reroutes", () => { expect(graph.reroutes.size).toBe(4) }) - test("Floating reroutes should be removed when neither input nor output is connected", ({ expect, floatingBranchGraph: graph }) => { + test('Floating reroutes should be removed when neither input nor output is connected', ({ + expect, + floatingBranchGraph: graph + }) => { // Remove output node graph.remove(graph.nodes[0]) expect(graph.nodes.length).toBe(2) @@ -113,17 +128,17 @@ describe("Floating Links / Reroutes", () => { }) }) -describe("Legacy LGraph Compatibility Layer", () => { - test("can be extended via prototype", ({ expect, minimalGraph }) => { +describe('Legacy LGraph Compatibility Layer', () => { + test('can be extended via prototype', ({ expect, minimalGraph }) => { // @ts-expect-error Should always be an error. LGraph.prototype.newMethod = function () { - return "New method added via prototype" + return 'New method added via prototype' } // @ts-expect-error Should always be an error. - expect(minimalGraph.newMethod()).toBe("New method added via prototype") + expect(minimalGraph.newMethod()).toBe('New method added via prototype') }) - test("is correctly assigned to LiteGraph", ({ expect }) => { + test('is correctly assigned to LiteGraph', ({ expect }) => { expect(LiteGraph.LGraph).toBe(LGraph) }) }) diff --git a/src/lib/litegraph/test/LGraphButton.test.ts b/src/lib/litegraph/test/LGraphButton.test.ts index 3a8e9f1a30..54d0a66278 100644 --- a/src/lib/litegraph/test/LGraphButton.test.ts +++ b/src/lib/litegraph/test/LGraphButton.test.ts @@ -1,45 +1,48 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from 'vitest' -import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" -import { LGraphButton } from "@/lib/litegraph/src/LGraphButton" +import { LGraphButton } from '@/lib/litegraph/src/LGraphButton' +import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' -describe("LGraphButton", () => { - describe("Constructor", () => { - it("should create a button with default options", () => { +describe('LGraphButton', () => { + describe('Constructor', () => { + it('should create a button with default options', () => { const button = new LGraphButton({}) expect(button).toBeInstanceOf(LGraphButton) expect(button.name).toBeUndefined() expect(button._last_area).toBeInstanceOf(Rectangle) }) - it("should create a button with custom name", () => { - const button = new LGraphButton({ name: "test_button" }) - expect(button.name).toBe("test_button") + it('should create a button with custom name', () => { + const button = new LGraphButton({ name: 'test_button' }) + expect(button.name).toBe('test_button') }) - it("should inherit badge properties", () => { + it('should inherit badge properties', () => { const button = new LGraphButton({ - text: "Test", - fgColor: "#FF0000", - bgColor: "#0000FF", - fontSize: 16, + text: 'Test', + fgColor: '#FF0000', + bgColor: '#0000FF', + fontSize: 16 }) - expect(button.text).toBe("Test") - expect(button.fgColor).toBe("#FF0000") - expect(button.bgColor).toBe("#0000FF") + expect(button.text).toBe('Test') + expect(button.fgColor).toBe('#FF0000') + expect(button.bgColor).toBe('#0000FF') expect(button.fontSize).toBe(16) expect(button.visible).toBe(true) // visible is computed based on text length }) }) - describe("draw", () => { - it("should not draw if not visible", () => { - const button = new LGraphButton({ text: "" }) // Empty text makes it invisible + describe('draw', () => { + it('should not draw if not visible', () => { + const button = new LGraphButton({ text: '' }) // Empty text makes it invisible const ctx = { - measureText: vi.fn().mockReturnValue({ width: 100 }), + measureText: vi.fn().mockReturnValue({ width: 100 }) } as unknown as CanvasRenderingContext2D - const superDrawSpy = vi.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(button)), "draw") + const superDrawSpy = vi.spyOn( + Object.getPrototypeOf(Object.getPrototypeOf(button)), + 'draw' + ) button.draw(ctx, 50, 100) @@ -47,11 +50,11 @@ describe("LGraphButton", () => { expect(button._last_area.width).toBe(0) // Rectangle default width }) - it("should draw and update last area when visible", () => { + it('should draw and update last area when visible', () => { const button = new LGraphButton({ - text: "Click", + text: 'Click', xOffset: 5, - yOffset: 10, + yOffset: 10 }) const ctx = { @@ -61,9 +64,9 @@ describe("LGraphButton", () => { beginPath: vi.fn(), roundRect: vi.fn(), fill: vi.fn(), - font: "", - fillStyle: "", - globalAlpha: 1, + font: '', + fillStyle: '', + globalAlpha: 1 } as unknown as CanvasRenderingContext2D const mockGetWidth = vi.fn().mockReturnValue(80) @@ -81,9 +84,9 @@ describe("LGraphButton", () => { expect(button._last_area[3]).toBe(button.height) }) - it("should calculate last area without offsets", () => { + it('should calculate last area without offsets', () => { const button = new LGraphButton({ - text: "Test", + text: 'Test' }) const ctx = { @@ -93,9 +96,9 @@ describe("LGraphButton", () => { beginPath: vi.fn(), roundRect: vi.fn(), fill: vi.fn(), - font: "", - fillStyle: "", - globalAlpha: 1, + font: '', + fillStyle: '', + globalAlpha: 1 } as unknown as CanvasRenderingContext2D const mockGetWidth = vi.fn().mockReturnValue(50) @@ -109,9 +112,9 @@ describe("LGraphButton", () => { }) }) - describe("isPointInside", () => { - it("should return true when point is inside button area", () => { - const button = new LGraphButton({ text: "Test" }) + describe('isPointInside', () => { + it('should return true when point is inside button area', () => { + const button = new LGraphButton({ text: 'Test' }) // Set the last area manually button._last_area[0] = 100 button._last_area[1] = 50 @@ -124,8 +127,8 @@ describe("LGraphButton", () => { expect(button.isPointInside(140, 60)).toBe(true) // Center }) - it("should return false when point is outside button area", () => { - const button = new LGraphButton({ text: "Test" }) + it('should return false when point is outside button area', () => { + const button = new LGraphButton({ text: 'Test' }) // Set the last area manually button._last_area[0] = 100 button._last_area[1] = 50 @@ -140,8 +143,8 @@ describe("LGraphButton", () => { expect(button.isPointInside(0, 0)).toBe(false) // Far away }) - it("should work with buttons that have not been drawn yet", () => { - const button = new LGraphButton({ text: "Test" }) + it('should work with buttons that have not been drawn yet', () => { + const button = new LGraphButton({ text: 'Test' }) // _last_area has default values (0, 0, 0, 0) expect(button.isPointInside(10, 10)).toBe(false) @@ -149,15 +152,15 @@ describe("LGraphButton", () => { }) }) - describe("Integration with LGraphBadge", () => { - it("should properly inherit and use badge functionality", () => { + describe('Integration with LGraphBadge', () => { + it('should properly inherit and use badge functionality', () => { const button = new LGraphButton({ - text: "→", + text: '→', fontSize: 20, - color: "#FFFFFF", - backgroundColor: "#333333", + color: '#FFFFFF', + backgroundColor: '#333333', xOffset: -10, - yOffset: 5, + yOffset: 5 }) const ctx = { @@ -167,9 +170,9 @@ describe("LGraphButton", () => { beginPath: vi.fn(), roundRect: vi.fn(), fill: vi.fn(), - font: "", - fillStyle: "", - globalAlpha: 1, + font: '', + fillStyle: '', + globalAlpha: 1 } as unknown as CanvasRenderingContext2D // Draw the button @@ -179,7 +182,11 @@ describe("LGraphButton", () => { expect(ctx.beginPath).not.toHaveBeenCalled() // No background expect(ctx.roundRect).not.toHaveBeenCalled() // No background expect(ctx.fill).not.toHaveBeenCalled() // No background - expect(ctx.fillText).toHaveBeenCalledWith("→", expect.any(Number), expect.any(Number)) // Just text + expect(ctx.fillText).toHaveBeenCalledWith( + '→', + expect.any(Number), + expect.any(Number) + ) // Just text }) }) }) diff --git a/src/lib/litegraph/test/LGraphCanvas.titleButtons.test.ts b/src/lib/litegraph/test/LGraphCanvas.titleButtons.test.ts index c5ff9df86b..2b8d4917f9 100644 --- a/src/lib/litegraph/test/LGraphCanvas.titleButtons.test.ts +++ b/src/lib/litegraph/test/LGraphCanvas.titleButtons.test.ts @@ -1,16 +1,16 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" +import { beforeEach, describe, expect, it, vi } from 'vitest' -import { LGraphCanvas } from "@/lib/litegraph/src/LGraphCanvas" -import { LGraphNode, LiteGraph } from "@/lib/litegraph/src/litegraph" +import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' +import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' -describe("LGraphCanvas Title Button Rendering", () => { +describe('LGraphCanvas Title Button Rendering', () => { let canvas: LGraphCanvas let ctx: CanvasRenderingContext2D let node: LGraphNode beforeEach(() => { // Create a mock canvas element - const canvasElement = document.createElement("canvas") + const canvasElement = document.createElement('canvas') ctx = { save: vi.fn(), restore: vi.fn(), @@ -32,23 +32,23 @@ describe("LGraphCanvas Title Button Rendering", () => { clearRect: vi.fn(), setTransform: vi.fn(), roundRect: vi.fn(), - font: "", - fillStyle: "", - strokeStyle: "", + font: '', + fillStyle: '', + strokeStyle: '', lineWidth: 1, globalAlpha: 1, - textAlign: "left" as CanvasTextAlign, - textBaseline: "alphabetic" as CanvasTextBaseline, + textAlign: 'left' as CanvasTextAlign, + textBaseline: 'alphabetic' as CanvasTextBaseline } as unknown as CanvasRenderingContext2D canvasElement.getContext = vi.fn().mockReturnValue(ctx) canvas = new LGraphCanvas(canvasElement, null, { skip_render: true, - skip_events: true, + skip_events: true }) - node = new LGraphNode("Test Node") + node = new LGraphNode('Test Node') node.pos = [100, 200] node.size = [200, 100] @@ -70,25 +70,25 @@ describe("LGraphCanvas Title Button Rendering", () => { node.isSelectable = vi.fn().mockReturnValue(true) }) - describe("drawNode title button rendering", () => { - it("should render visible title buttons", () => { + describe('drawNode title button rendering', () => { + it('should render visible title buttons', () => { const button1 = node.addTitleButton({ - name: "button1", - text: "A", - visible: true, + name: 'button1', + text: 'A', + visible: true }) const button2 = node.addTitleButton({ - name: "button2", - text: "B", - visible: true, + name: 'button2', + text: 'B', + visible: true }) // Mock button methods const getWidth1 = vi.fn().mockReturnValue(20) const getWidth2 = vi.fn().mockReturnValue(25) - const draw1 = vi.spyOn(button1, "draw") - const draw2 = vi.spyOn(button2, "draw") + const draw1 = vi.spyOn(button1, 'draw') + const draw2 = vi.spyOn(button2, 'draw') button1.getWidth = getWidth1 button2.getWidth = getWidth2 @@ -113,22 +113,22 @@ describe("LGraphCanvas Title Button Rendering", () => { expect(draw2).toHaveBeenCalledWith(ctx, 153, buttonY) // 180 - 2 - 25 }) - it("should skip invisible title buttons", () => { + it('should skip invisible title buttons', () => { const visibleButton = node.addTitleButton({ - name: "visible", - text: "V", - visible: true, + name: 'visible', + text: 'V', + visible: true }) const invisibleButton = node.addTitleButton({ - name: "invisible", - text: "", // Empty text makes it invisible + name: 'invisible', + text: '' // Empty text makes it invisible }) const getWidthVisible = vi.fn().mockReturnValue(30) const getWidthInvisible = vi.fn().mockReturnValue(30) - const drawVisible = vi.spyOn(visibleButton, "draw") - const drawInvisible = vi.spyOn(invisibleButton, "draw") + const drawVisible = vi.spyOn(visibleButton, 'draw') + const drawInvisible = vi.spyOn(invisibleButton, 'draw') visibleButton.getWidth = getWidthVisible invisibleButton.getWidth = getWidthInvisible @@ -143,7 +143,7 @@ describe("LGraphCanvas Title Button Rendering", () => { expect(drawInvisible).not.toHaveBeenCalled() }) - it("should handle nodes without title buttons", () => { + it('should handle nodes without title buttons', () => { // Node has no title buttons expect(node.title_buttons).toHaveLength(0) @@ -151,7 +151,7 @@ describe("LGraphCanvas Title Button Rendering", () => { expect(() => canvas.drawNode(node, ctx)).not.toThrow() }) - it("should position multiple buttons with correct spacing", () => { + it('should position multiple buttons with correct spacing', () => { const buttons = [] const drawSpies = [] @@ -160,10 +160,10 @@ describe("LGraphCanvas Title Button Rendering", () => { const button = node.addTitleButton({ name: `button${i}`, text: String(i), - visible: true, + visible: true }) button.getWidth = vi.fn().mockReturnValue(15) // All same width for simplicity - const spy = vi.spyOn(button, "draw") + const spy = vi.spyOn(button, 'draw') buttons.push(button) drawSpies.push(spy) } @@ -180,15 +180,15 @@ describe("LGraphCanvas Title Button Rendering", () => { expect(drawSpies[2]).toHaveBeenCalledWith(ctx, 151, buttonY) // 168 - 2 - 15 }) - it("should render buttons in low quality mode", () => { + it('should render buttons in low quality mode', () => { const button = node.addTitleButton({ - name: "test", - text: "T", - visible: true, + name: 'test', + text: 'T', + visible: true }) button.getWidth = vi.fn().mockReturnValue(20) - const drawSpy = vi.spyOn(button, "draw") + const drawSpy = vi.spyOn(button, 'draw') // Set low quality rendering canvas.lowQualityRenderingRequired = true @@ -196,28 +196,29 @@ describe("LGraphCanvas Title Button Rendering", () => { canvas.drawNode(node, ctx) // Buttons should still be rendered in low quality mode - const buttonY = -LiteGraph.NODE_TITLE_HEIGHT + (LiteGraph.NODE_TITLE_HEIGHT - 20) / 2 + const buttonY = + -LiteGraph.NODE_TITLE_HEIGHT + (LiteGraph.NODE_TITLE_HEIGHT - 20) / 2 expect(drawSpy).toHaveBeenCalledWith(ctx, 180, buttonY) }) - it("should handle buttons with different widths", () => { + it('should handle buttons with different widths', () => { const smallButton = node.addTitleButton({ - name: "small", - text: "S", - visible: true, + name: 'small', + text: 'S', + visible: true }) const largeButton = node.addTitleButton({ - name: "large", - text: "LARGE", - visible: true, + name: 'large', + text: 'LARGE', + visible: true }) smallButton.getWidth = vi.fn().mockReturnValue(15) largeButton.getWidth = vi.fn().mockReturnValue(50) - const drawSmall = vi.spyOn(smallButton, "draw") - const drawLarge = vi.spyOn(largeButton, "draw") + const drawSmall = vi.spyOn(smallButton, 'draw') + const drawLarge = vi.spyOn(largeButton, 'draw') canvas.drawNode(node, ctx) @@ -232,18 +233,18 @@ describe("LGraphCanvas Title Button Rendering", () => { }) }) - describe("Integration with node properties", () => { - it("should respect node size for button positioning", () => { + describe('Integration with node properties', () => { + it('should respect node size for button positioning', () => { node.size = [300, 150] // Wider node const button = node.addTitleButton({ - name: "test", - text: "X", - visible: true, + name: 'test', + text: 'X', + visible: true }) button.getWidth = vi.fn().mockReturnValue(20) - const drawSpy = vi.spyOn(button, "draw") + const drawSpy = vi.spyOn(button, 'draw') canvas.drawNode(node, ctx) @@ -253,16 +254,16 @@ describe("LGraphCanvas Title Button Rendering", () => { expect(drawSpy).toHaveBeenCalledWith(ctx, 280, buttonY) }) - it("should NOT render buttons on collapsed nodes", () => { + it('should NOT render buttons on collapsed nodes', () => { node.flags.collapsed = true const button = node.addTitleButton({ - name: "test", - text: "C", + name: 'test', + text: 'C' }) button.getWidth = vi.fn().mockReturnValue(20) - const drawSpy = vi.spyOn(button, "draw") + const drawSpy = vi.spyOn(button, 'draw') canvas.drawNode(node, ctx) diff --git a/src/lib/litegraph/test/LGraphGroup.test.ts b/src/lib/litegraph/test/LGraphGroup.test.ts index 5c8bac5e72..f158abfff3 100644 --- a/src/lib/litegraph/test/LGraphGroup.test.ts +++ b/src/lib/litegraph/test/LGraphGroup.test.ts @@ -1,12 +1,12 @@ -import { describe, expect } from "vitest" +import { describe, expect } from 'vitest' -import { LGraphGroup } from "@/lib/litegraph/src/litegraph" +import { LGraphGroup } from '@/lib/litegraph/src/litegraph' -import { test } from "./testExtensions" +import { test } from './testExtensions' -describe("LGraphGroup", () => { - test("serializes to the existing format", () => { - const link = new LGraphGroup("title", 929) - expect(link.serialize()).toMatchSnapshot("Basic") +describe('LGraphGroup', () => { + test('serializes to the existing format', () => { + const link = new LGraphGroup('title', 929) + expect(link.serialize()).toMatchSnapshot('Basic') }) }) diff --git a/src/lib/litegraph/test/LGraphNode.resize.test.ts b/src/lib/litegraph/test/LGraphNode.resize.test.ts index 3b05321031..55da0cbc94 100644 --- a/src/lib/litegraph/test/LGraphNode.resize.test.ts +++ b/src/lib/litegraph/test/LGraphNode.resize.test.ts @@ -1,17 +1,17 @@ -import { beforeEach, describe, expect } from "vitest" +import { beforeEach, describe, expect } from 'vitest' -import { LGraphNode, LiteGraph } from "@/lib/litegraph/src/litegraph" +import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' -import { test } from "./testExtensions" +import { test } from './testExtensions' -describe("LGraphNode resize functionality", () => { +describe('LGraphNode resize functionality', () => { let node: LGraphNode beforeEach(() => { // Set up LiteGraph constants needed for measure LiteGraph.NODE_TITLE_HEIGHT = 20 - node = new LGraphNode("Test Node") + node = new LGraphNode('Test Node') node.pos = [100, 100] node.size = [200, 150] @@ -22,57 +22,57 @@ describe("LGraphNode resize functionality", () => { node.updateArea(mockCtx) }) - describe("findResizeDirection", () => { - describe("corners", () => { - test("should detect NW (top-left) corner", () => { + describe('findResizeDirection', () => { + describe('corners', () => { + test('should detect NW (top-left) corner', () => { // With title bar, top is at y=80 (100 - 20) // Corner is from (100, 80) to (100 + 15, 80 + 15) - expect(node.findResizeDirection(100, 80)).toBe("NW") - expect(node.findResizeDirection(110, 90)).toBe("NW") - expect(node.findResizeDirection(114, 94)).toBe("NW") + expect(node.findResizeDirection(100, 80)).toBe('NW') + expect(node.findResizeDirection(110, 90)).toBe('NW') + expect(node.findResizeDirection(114, 94)).toBe('NW') }) - test("should detect NE (top-right) corner", () => { + test('should detect NE (top-right) corner', () => { // Corner is from (300 - 15, 80) to (300, 80 + 15) - expect(node.findResizeDirection(285, 80)).toBe("NE") - expect(node.findResizeDirection(290, 90)).toBe("NE") - expect(node.findResizeDirection(299, 94)).toBe("NE") + expect(node.findResizeDirection(285, 80)).toBe('NE') + expect(node.findResizeDirection(290, 90)).toBe('NE') + expect(node.findResizeDirection(299, 94)).toBe('NE') }) - test("should detect SW (bottom-left) corner", () => { + test('should detect SW (bottom-left) corner', () => { // Bottom is at y=250 (100 + 150) // Corner is from (100, 250 - 15) to (100 + 15, 250) - expect(node.findResizeDirection(100, 235)).toBe("SW") - expect(node.findResizeDirection(110, 240)).toBe("SW") - expect(node.findResizeDirection(114, 249)).toBe("SW") + expect(node.findResizeDirection(100, 235)).toBe('SW') + expect(node.findResizeDirection(110, 240)).toBe('SW') + expect(node.findResizeDirection(114, 249)).toBe('SW') }) - test("should detect SE (bottom-right) corner", () => { + test('should detect SE (bottom-right) corner', () => { // Corner is from (300 - 15, 250 - 15) to (300, 250) - expect(node.findResizeDirection(285, 235)).toBe("SE") - expect(node.findResizeDirection(290, 240)).toBe("SE") - expect(node.findResizeDirection(299, 249)).toBe("SE") + expect(node.findResizeDirection(285, 235)).toBe('SE') + expect(node.findResizeDirection(290, 240)).toBe('SE') + expect(node.findResizeDirection(299, 249)).toBe('SE') }) }) - describe("priority", () => { - test("corners should have priority over edges", () => { + describe('priority', () => { + test('corners should have priority over edges', () => { // These points are technically on both corner and edge // Corner should win - expect(node.findResizeDirection(100, 84)).toBe("NW") // Not "W" - expect(node.findResizeDirection(104, 80)).toBe("NW") // Not "N" + expect(node.findResizeDirection(100, 84)).toBe('NW') // Not "W" + expect(node.findResizeDirection(104, 80)).toBe('NW') // Not "N" }) }) - describe("negative cases", () => { - test("should return undefined when outside node bounds", () => { + describe('negative cases', () => { + test('should return undefined when outside node bounds', () => { expect(node.findResizeDirection(50, 50)).toBeUndefined() expect(node.findResizeDirection(350, 300)).toBeUndefined() expect(node.findResizeDirection(99, 150)).toBeUndefined() expect(node.findResizeDirection(301, 150)).toBeUndefined() }) - test("should return undefined when inside node but not on resize areas", () => { + test('should return undefined when inside node but not on resize areas', () => { // Center of node (accounting for title bar offset) expect(node.findResizeDirection(200, 165)).toBeUndefined() // Just inside the edge threshold @@ -82,7 +82,7 @@ describe("LGraphNode resize functionality", () => { expect(node.findResizeDirection(150, 244)).toBeUndefined() }) - test("should return undefined when node is not resizable", () => { + test('should return undefined when node is not resizable', () => { node.resizable = false expect(node.findResizeDirection(100, 100)).toBeUndefined() expect(node.findResizeDirection(300, 250)).toBeUndefined() @@ -90,8 +90,8 @@ describe("LGraphNode resize functionality", () => { }) }) - describe("edge cases", () => { - test("should handle nodes at origin", () => { + describe('edge cases', () => { + test('should handle nodes at origin', () => { node.pos = [0, 0] node.size = [100, 100] @@ -99,11 +99,11 @@ describe("LGraphNode resize functionality", () => { const mockCtx = {} as CanvasRenderingContext2D node.updateArea(mockCtx) - expect(node.findResizeDirection(0, -20)).toBe("NW") // Account for title bar - expect(node.findResizeDirection(99, 99)).toBe("SE") // Bottom-right corner (100-1, 100-1) + expect(node.findResizeDirection(0, -20)).toBe('NW') // Account for title bar + expect(node.findResizeDirection(99, 99)).toBe('SE') // Bottom-right corner (100-1, 100-1) }) - test("should handle very small nodes", () => { + test('should handle very small nodes', () => { node.size = [20, 20] // Smaller than corner size // Update boundingRect with new size @@ -111,20 +111,20 @@ describe("LGraphNode resize functionality", () => { node.updateArea(mockCtx) // Corners still work (accounting for title bar offset) - expect(node.findResizeDirection(100, 80)).toBe("NW") - expect(node.findResizeDirection(119, 119)).toBe("SE") + expect(node.findResizeDirection(100, 80)).toBe('NW') + expect(node.findResizeDirection(119, 119)).toBe('SE') }) }) }) - describe("resizeEdgeSize static property", () => { - test("should have default value of 5", () => { + describe('resizeEdgeSize static property', () => { + test('should have default value of 5', () => { expect(LGraphNode.resizeEdgeSize).toBe(5) }) }) - describe("resizeHandleSize static property", () => { - test("should have default value of 15", () => { + describe('resizeHandleSize static property', () => { + test('should have default value of 15', () => { expect(LGraphNode.resizeHandleSize).toBe(15) }) }) diff --git a/src/lib/litegraph/test/LGraphNode.test.ts b/src/lib/litegraph/test/LGraphNode.test.ts index 21c01b46c2..66dc7f0f19 100644 --- a/src/lib/litegraph/test/LGraphNode.test.ts +++ b/src/lib/litegraph/test/LGraphNode.test.ts @@ -1,28 +1,32 @@ -import type { INodeInputSlot, Point } from "@/lib/litegraph/src/interfaces" -import type { ISerialisedNode } from "@/lib/litegraph/src/types/serialisation" +import { afterEach, beforeEach, describe, expect, vi } from 'vitest' -import { afterEach, beforeEach, describe, expect, vi } from "vitest" +import type { INodeInputSlot, Point } from '@/lib/litegraph/src/interfaces' +import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' +import { LGraph } from '@/lib/litegraph/src/litegraph' +import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot' +import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot' +import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation' -import { LGraphNode, LiteGraph } from "@/lib/litegraph/src/litegraph" -import { LGraph } from "@/lib/litegraph/src/litegraph" -import { NodeInputSlot } from "@/lib/litegraph/src/node/NodeInputSlot" -import { NodeOutputSlot } from "@/lib/litegraph/src/node/NodeOutputSlot" +import { test } from './testExtensions' -import { test } from "./testExtensions" - -function getMockISerialisedNode(data: Partial): ISerialisedNode { - return Object.assign({ - id: 0, - flags: {}, - type: "TestNode", - pos: [100, 100], - size: [100, 100], - order: 0, - mode: 0, - }, data) +function getMockISerialisedNode( + data: Partial +): ISerialisedNode { + return Object.assign( + { + id: 0, + flags: {}, + type: 'TestNode', + pos: [100, 100], + size: [100, 100], + order: 0, + mode: 0 + }, + data + ) } -describe("LGraphNode", () => { +describe('LGraphNode', () => { let node: LGraphNode let origLiteGraph: typeof LiteGraph @@ -35,11 +39,11 @@ describe("LGraphNode", () => { NODE_TITLE_HEIGHT: 20, NODE_SLOT_HEIGHT: 15, NODE_TEXT_SIZE: 14, - DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", + DEFAULT_SHADOW_COLOR: 'rgba(0,0,0,0.5)', DEFAULT_GROUP_FONT_SIZE: 24, - isValidConnection: vi.fn().mockReturnValue(true), + isValidConnection: vi.fn().mockReturnValue(true) }) - node = new LGraphNode("Test Node") + node = new LGraphNode('Test Node') node.pos = [100, 200] node.size = [150, 100] // Example size @@ -51,8 +55,8 @@ describe("LGraphNode", () => { Object.assign(LiteGraph, origLiteGraph) }) - test("should serialize position/size correctly", () => { - const node = new LGraphNode("TestNode") + test('should serialize position/size correctly', () => { + const node = new LGraphNode('TestNode') node.pos = [10, 20] node.size = [30, 40] const json = node.serialize() @@ -67,22 +71,33 @@ describe("LGraphNode", () => { flags: {}, order: node.order, mode: node.mode, - inputs: node.inputs?.map(i => ({ name: i.name, type: i.type, link: i.link })), - outputs: node.outputs?.map(o => ({ name: o.name, type: o.type, links: o.links, slot_index: o.slot_index })), + inputs: node.inputs?.map((i) => ({ + name: i.name, + type: i.type, + link: i.link + })), + outputs: node.outputs?.map((o) => ({ + name: o.name, + type: o.type, + links: o.links, + slot_index: o.slot_index + })) } node.configure(configureData) expect(node.pos).toEqual(new Float32Array([50, 60])) expect(node.size).toEqual(new Float32Array([70, 80])) }) - test("should configure inputs correctly", () => { - const node = new LGraphNode("TestNode") - node.configure(getMockISerialisedNode({ - id: 0, - inputs: [{ name: "TestInput", type: "number", link: null }], - })) + test('should configure inputs correctly', () => { + const node = new LGraphNode('TestNode') + node.configure( + getMockISerialisedNode({ + id: 0, + inputs: [{ name: 'TestInput', type: 'number', link: null }] + }) + ) expect(node.inputs.length).toEqual(1) - expect(node.inputs[0].name).toEqual("TestInput") + expect(node.inputs[0].name).toEqual('TestInput') expect(node.inputs[0].link).toEqual(null) expect(node.inputs[0]).instanceOf(NodeInputSlot) @@ -92,15 +107,17 @@ describe("LGraphNode", () => { expect(node.inputs.length).toEqual(1) }) - test("should configure outputs correctly", () => { - const node = new LGraphNode("TestNode") - node.configure(getMockISerialisedNode({ - id: 0, - outputs: [{ name: "TestOutput", type: "number", links: [] }], - })) + test('should configure outputs correctly', () => { + const node = new LGraphNode('TestNode') + node.configure( + getMockISerialisedNode({ + id: 0, + outputs: [{ name: 'TestOutput', type: 'number', links: [] }] + }) + ) expect(node.outputs.length).toEqual(1) - expect(node.outputs[0].name).toEqual("TestOutput") - expect(node.outputs[0].type).toEqual("number") + expect(node.outputs[0].name).toEqual('TestOutput') + expect(node.outputs[0].type).toEqual('number') expect(node.outputs[0].links).toEqual([]) expect(node.outputs[0]).instanceOf(NodeOutputSlot) @@ -110,20 +127,24 @@ describe("LGraphNode", () => { expect(node.outputs.length).toEqual(1) }) - describe("Disconnect I/O Slots", () => { - test("should disconnect input correctly", () => { - const node1 = new LGraphNode("SourceNode") - const node2 = new LGraphNode("TargetNode") + describe('Disconnect I/O Slots', () => { + test('should disconnect input correctly', () => { + const node1 = new LGraphNode('SourceNode') + const node2 = new LGraphNode('TargetNode') // Configure nodes with input/output slots - node1.configure(getMockISerialisedNode({ - id: 1, - outputs: [{ name: "Output1", type: "number", links: [] }], - })) - node2.configure(getMockISerialisedNode({ - id: 2, - inputs: [{ name: "Input1", type: "number", link: null }], - })) + node1.configure( + getMockISerialisedNode({ + id: 1, + outputs: [{ name: 'Output1', type: 'number', links: [] }] + }) + ) + node2.configure( + getMockISerialisedNode({ + id: 2, + inputs: [{ name: 'Input1', type: 'number', link: null }] + }) + ) // Create a graph and add nodes to it const graph = new LGraph() @@ -145,7 +166,7 @@ describe("LGraphNode", () => { // Test disconnecting by slot name node1.connect(0, node2, 0) - const disconnectedByName = node2.disconnectInput("Input1") + const disconnectedByName = node2.disconnectInput('Input1') expect(disconnectedByName).toBe(true) expect(node2.inputs[0].link).toBeNull() @@ -158,27 +179,33 @@ describe("LGraphNode", () => { expect(alreadyDisconnected).toBe(true) }) - test("should disconnect output correctly", () => { - const sourceNode = new LGraphNode("SourceNode") - const targetNode1 = new LGraphNode("TargetNode1") - const targetNode2 = new LGraphNode("TargetNode2") + test('should disconnect output correctly', () => { + const sourceNode = new LGraphNode('SourceNode') + const targetNode1 = new LGraphNode('TargetNode1') + const targetNode2 = new LGraphNode('TargetNode2') // Configure nodes with input/output slots - sourceNode.configure(getMockISerialisedNode({ - id: 1, - outputs: [ - { name: "Output1", type: "number", links: [] }, - { name: "Output2", type: "number", links: [] }, - ], - })) - targetNode1.configure(getMockISerialisedNode({ - id: 2, - inputs: [{ name: "Input1", type: "number", link: null }], - })) - targetNode2.configure(getMockISerialisedNode({ - id: 3, - inputs: [{ name: "Input1", type: "number", link: null }], - })) + sourceNode.configure( + getMockISerialisedNode({ + id: 1, + outputs: [ + { name: 'Output1', type: 'number', links: [] }, + { name: 'Output2', type: 'number', links: [] } + ] + }) + ) + targetNode1.configure( + getMockISerialisedNode({ + id: 2, + inputs: [{ name: 'Input1', type: 'number', link: null }] + }) + ) + targetNode2.configure( + getMockISerialisedNode({ + id: 3, + inputs: [{ name: 'Input1', type: 'number', link: null }] + }) + ) // Create a graph and add nodes to it const graph = new LGraph() @@ -204,7 +231,10 @@ describe("LGraphNode", () => { // Test disconnecting by slot name const link3 = sourceNode.connect(1, targetNode1, 0) expect(link3).not.toBeNull() - const disconnectedByName = sourceNode.disconnectOutput("Output2", targetNode1) + const disconnectedByName = sourceNode.disconnectOutput( + 'Output2', + targetNode1 + ) expect(disconnectedByName).toBe(true) expect(targetNode1.inputs[0].link).toBeNull() expect(sourceNode.outputs[1].links?.length).toBe(0) @@ -231,20 +261,25 @@ describe("LGraphNode", () => { }) }) - describe("getInputPos and getOutputPos", () => { - test("should handle collapsed nodes correctly", () => { - const node = new LGraphNode("TestNode") as unknown as Omit & { boundingRect: Float32Array } + describe('getInputPos and getOutputPos', () => { + test('should handle collapsed nodes correctly', () => { + const node = new LGraphNode('TestNode') as unknown as Omit< + LGraphNode, + 'boundingRect' + > & { boundingRect: Float32Array } node.pos = [100, 100] node.size = [100, 100] node.boundingRect[0] = 100 node.boundingRect[1] = 100 node.boundingRect[2] = 100 node.boundingRect[3] = 100 - node.configure(getMockISerialisedNode({ - id: 1, - inputs: [{ name: "Input1", type: "number", link: null }], - outputs: [{ name: "Output1", type: "number", links: [] }], - })) + node.configure( + getMockISerialisedNode({ + id: 1, + inputs: [{ name: 'Input1', type: 'number', link: null }], + outputs: [{ name: 'Output1', type: 'number', links: [] }] + }) + ) // Collapse the node node.flags.collapsed = true @@ -257,15 +292,17 @@ describe("LGraphNode", () => { expect(outputPos).toEqual([180, 90]) }) - test("should return correct positions for input and output slots", () => { - const node = new LGraphNode("TestNode") + test('should return correct positions for input and output slots', () => { + const node = new LGraphNode('TestNode') node.pos = [100, 100] node.size = [100, 100] - node.configure(getMockISerialisedNode({ - id: 1, - inputs: [{ name: "Input1", type: "number", link: null }], - outputs: [{ name: "Output1", type: "number", links: [] }], - })) + node.configure( + getMockISerialisedNode({ + id: 1, + inputs: [{ name: 'Input1', type: 'number', link: null }], + outputs: [{ name: 'Output1', type: 'number', links: [] }] + }) + ) const inputPos = node.getInputPos(0) const outputPos = node.getOutputPos(0) @@ -275,16 +312,18 @@ describe("LGraphNode", () => { }) }) - describe("getSlotOnPos", () => { - test("should return undefined when point is outside node bounds", () => { - const node = new LGraphNode("TestNode") + describe('getSlotOnPos', () => { + test('should return undefined when point is outside node bounds', () => { + const node = new LGraphNode('TestNode') node.pos = [100, 100] node.size = [100, 100] - node.configure(getMockISerialisedNode({ - id: 1, - inputs: [{ name: "Input1", type: "number", link: null }], - outputs: [{ name: "Output1", type: "number", links: [] }], - })) + node.configure( + getMockISerialisedNode({ + id: 1, + inputs: [{ name: 'Input1', type: 'number', link: null }], + outputs: [{ name: 'Output1', type: 'number', links: [] }] + }) + ) // Test point far outside node bounds expect(node.getSlotOnPos([0, 0])).toBeUndefined() @@ -292,74 +331,89 @@ describe("LGraphNode", () => { expect(node.getSlotOnPos([99, 99])).toBeUndefined() }) - test("should detect input slots correctly", () => { - const node = new LGraphNode("TestNode") as unknown as Omit & { boundingRect: Float32Array } + test('should detect input slots correctly', () => { + const node = new LGraphNode('TestNode') as unknown as Omit< + LGraphNode, + 'boundingRect' + > & { boundingRect: Float32Array } node.pos = [100, 100] node.size = [100, 100] node.boundingRect[0] = 100 node.boundingRect[1] = 100 node.boundingRect[2] = 200 node.boundingRect[3] = 200 - node.configure(getMockISerialisedNode({ - id: 1, - inputs: [ - { name: "Input1", type: "number", link: null }, - { name: "Input2", type: "string", link: null }, - ], - })) + node.configure( + getMockISerialisedNode({ + id: 1, + inputs: [ + { name: 'Input1', type: 'number', link: null }, + { name: 'Input2', type: 'string', link: null } + ] + }) + ) // Get position of first input slot const inputPos = node.getInputPos(0) // Test point directly on input slot const slot = node.getSlotOnPos(inputPos) expect(slot).toBeDefined() - expect(slot?.name).toBe("Input1") + expect(slot?.name).toBe('Input1') // Test point near but not on input slot expect(node.getSlotOnPos([inputPos[0] - 15, inputPos[1]])).toBeUndefined() }) - test("should detect output slots correctly", () => { - const node = new LGraphNode("TestNode") as unknown as Omit & { boundingRect: Float32Array } + test('should detect output slots correctly', () => { + const node = new LGraphNode('TestNode') as unknown as Omit< + LGraphNode, + 'boundingRect' + > & { boundingRect: Float32Array } node.pos = [100, 100] node.size = [100, 100] node.boundingRect[0] = 100 node.boundingRect[1] = 100 node.boundingRect[2] = 200 node.boundingRect[3] = 200 - node.configure(getMockISerialisedNode({ - id: 1, - outputs: [ - { name: "Output1", type: "number", links: [] }, - { name: "Output2", type: "string", links: [] }, - ], - })) + node.configure( + getMockISerialisedNode({ + id: 1, + outputs: [ + { name: 'Output1', type: 'number', links: [] }, + { name: 'Output2', type: 'string', links: [] } + ] + }) + ) // Get position of first output slot const outputPos = node.getOutputPos(0) // Test point directly on output slot const slot = node.getSlotOnPos(outputPos) expect(slot).toBeDefined() - expect(slot?.name).toBe("Output1") + expect(slot?.name).toBe('Output1') // Test point near but not on output slot const gotslot = node.getSlotOnPos([outputPos[0] + 30, outputPos[1]]) expect(gotslot).toBeUndefined() }) - test("should prioritize input slots over output slots", () => { - const node = new LGraphNode("TestNode") as unknown as Omit & { boundingRect: Float32Array } + test('should prioritize input slots over output slots', () => { + const node = new LGraphNode('TestNode') as unknown as Omit< + LGraphNode, + 'boundingRect' + > & { boundingRect: Float32Array } node.pos = [100, 100] node.size = [100, 100] node.boundingRect[0] = 100 node.boundingRect[1] = 100 node.boundingRect[2] = 200 node.boundingRect[3] = 200 - node.configure(getMockISerialisedNode({ - id: 1, - inputs: [{ name: "Input1", type: "number", link: null }], - outputs: [{ name: "Output1", type: "number", links: [] }], - })) + node.configure( + getMockISerialisedNode({ + id: 1, + inputs: [{ name: 'Input1', type: 'number', link: null }], + outputs: [{ name: 'Output1', type: 'number', links: [] }] + }) + ) // Get positions of first input and output slots const inputPos = node.getInputPos(0) @@ -368,21 +422,21 @@ describe("LGraphNode", () => { // Should return the input slot due to priority const slot = node.getSlotOnPos(inputPos) expect(slot).toBeDefined() - expect(slot?.name).toBe("Input1") + expect(slot?.name).toBe('Input1') }) }) - describe("LGraphNode slot positioning", () => { - test("should correctly position slots with absolute coordinates", () => { + describe('LGraphNode slot positioning', () => { + test('should correctly position slots with absolute coordinates', () => { // Setup - const node = new LGraphNode("test") + const node = new LGraphNode('test') node.pos = [100, 100] // Add input/output with absolute positions - node.addInput("abs-input", "number") + node.addInput('abs-input', 'number') node.inputs[0].pos = [10, 20] - node.addOutput("abs-output", "number") + node.addOutput('abs-output', 'number') node.outputs[0].pos = [50, 30] // Test @@ -394,16 +448,16 @@ describe("LGraphNode", () => { expect(outputPos).toEqual([150, 130]) // node.pos + slot.pos }) - test("should correctly position default vertical slots", () => { + test('should correctly position default vertical slots', () => { // Setup - const node = new LGraphNode("test") + const node = new LGraphNode('test') node.pos = [100, 100] // Add multiple inputs/outputs without absolute positions - node.addInput("input1", "number") - node.addInput("input2", "number") - node.addOutput("output1", "number") - node.addOutput("output2", "number") + node.addInput('input1', 'number') + node.addInput('input2', 'number') + node.addOutput('output1', 'number') + node.addOutput('output2', 'number') // Calculate expected positions const slotOffset = LiteGraph.NODE_SLOT_HEIGHT * 0.5 @@ -413,34 +467,34 @@ describe("LGraphNode", () => { // Test input positions expect(node.getInputPos(0)).toEqual([ 100 + slotOffset, - 100 + (0 + 0.7) * slotSpacing, + 100 + (0 + 0.7) * slotSpacing ]) expect(node.getInputPos(1)).toEqual([ 100 + slotOffset, - 100 + (1 + 0.7) * slotSpacing, + 100 + (1 + 0.7) * slotSpacing ]) // Test output positions expect(node.getOutputPos(0)).toEqual([ 100 + nodeWidth + 1 - slotOffset, - 100 + (0 + 0.7) * slotSpacing, + 100 + (0 + 0.7) * slotSpacing ]) expect(node.getOutputPos(1)).toEqual([ 100 + nodeWidth + 1 - slotOffset, - 100 + (1 + 0.7) * slotSpacing, + 100 + (1 + 0.7) * slotSpacing ]) }) - test("should skip absolute positioned slots when calculating vertical positions", () => { + test('should skip absolute positioned slots when calculating vertical positions', () => { // Setup - const node = new LGraphNode("test") + const node = new LGraphNode('test') node.pos = [100, 100] // Add mix of absolute and default positioned slots - node.addInput("abs-input", "number") + node.addInput('abs-input', 'number') node.inputs[0].pos = [10, 20] - node.addInput("default-input1", "number") - node.addInput("default-input2", "number") + node.addInput('default-input1', 'number') + node.addInput('default-input2', 'number') const slotOffset = LiteGraph.NODE_SLOT_HEIGHT * 0.5 const slotSpacing = LiteGraph.NODE_SLOT_HEIGHT @@ -448,24 +502,24 @@ describe("LGraphNode", () => { // Test: default positioned slots should be consecutive, ignoring absolute positioned ones expect(node.getInputPos(1)).toEqual([ 100 + slotOffset, - 100 + (0 + 0.7) * slotSpacing, // First default slot starts at index 0 + 100 + (0 + 0.7) * slotSpacing // First default slot starts at index 0 ]) expect(node.getInputPos(2)).toEqual([ 100 + slotOffset, - 100 + (1 + 0.7) * slotSpacing, // Second default slot at index 1 + 100 + (1 + 0.7) * slotSpacing // Second default slot at index 1 ]) }) }) - describe("widget serialization", () => { - test("should only serialize widgets with serialize flag not set to false", () => { - const node = new LGraphNode("TestNode") + describe('widget serialization', () => { + test('should only serialize widgets with serialize flag not set to false', () => { + const node = new LGraphNode('TestNode') node.serialize_widgets = true // Add widgets with different serialization settings - node.addWidget("number", "serializable1", 1, null) - node.addWidget("number", "serializable2", 2, null) - node.addWidget("number", "non-serializable", 3, null) + node.addWidget('number', 'serializable1', 1, null) + node.addWidget('number', 'serializable2', 2, null) + node.addWidget('number', 'non-serializable', 3, null) expect(node.widgets?.length).toBe(3) // Set serialize flag to false for the last widget @@ -484,42 +538,49 @@ describe("LGraphNode", () => { expect(serialized.widgets_values).toHaveLength(2) }) - test("should only configure widgets with serialize flag not set to false", () => { - const node = new LGraphNode("TestNode") + test('should only configure widgets with serialize flag not set to false', () => { + const node = new LGraphNode('TestNode') node.serialize_widgets = true - node.addWidget("number", "non-serializable", 1, null) - node.addWidget("number", "serializable1", 2, null) + node.addWidget('number', 'non-serializable', 1, null) + node.addWidget('number', 'serializable1', 2, null) expect(node.widgets?.length).toBe(2) node.widgets![0].serialize = false - node.configure(getMockISerialisedNode({ - id: 1, - type: "TestNode", - pos: [100, 100], - size: [100, 100], - properties: {}, - widgets_values: [100], - })) + node.configure( + getMockISerialisedNode({ + id: 1, + type: 'TestNode', + pos: [100, 100], + size: [100, 100], + properties: {}, + widgets_values: [100] + }) + ) expect(node.widgets![0].value).toBe(1) expect(node.widgets![1].value).toBe(100) }) }) - describe("getInputSlotPos", () => { + describe('getInputSlotPos', () => { let inputSlot: INodeInputSlot beforeEach(() => { - inputSlot = { name: "test_in", type: "string", link: null, boundingRect: new Float32Array([0, 0, 0, 0]) } + inputSlot = { + name: 'test_in', + type: 'string', + link: null, + boundingRect: new Float32Array([0, 0, 0, 0]) + } }) - test("should return position based on title height when collapsed", () => { + test('should return position based on title height when collapsed', () => { node.flags.collapsed = true const expectedPos: Point = [100, 200 - LiteGraph.NODE_TITLE_HEIGHT * 0.5] expect(node.getInputSlotPos(inputSlot)).toEqual(expectedPos) }) - test("should return position based on input.pos when defined and not collapsed", () => { + test('should return position based on input.pos when defined and not collapsed', () => { node.flags.collapsed = false inputSlot.pos = [10, 50] node.inputs = [inputSlot] @@ -527,39 +588,58 @@ describe("LGraphNode", () => { expect(node.getInputSlotPos(inputSlot)).toEqual(expectedPos) }) - test("should return default vertical position when input.pos is undefined and not collapsed", () => { + test('should return default vertical position when input.pos is undefined and not collapsed', () => { node.flags.collapsed = false - const inputSlot2 = { name: "test_in_2", type: "number", link: null, boundingRect: new Float32Array([0, 0, 0, 0]) } + const inputSlot2 = { + name: 'test_in_2', + type: 'number', + link: null, + boundingRect: new Float32Array([0, 0, 0, 0]) + } node.inputs = [inputSlot, inputSlot2] const slotIndex = 0 const nodeOffsetY = (node.constructor as any).slot_start_y || 0 - const expectedY = 200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY + const expectedY = + 200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY const expectedX = 100 + LiteGraph.NODE_SLOT_HEIGHT * 0.5 expect(node.getInputSlotPos(inputSlot)).toEqual([expectedX, expectedY]) const slotIndex2 = 1 - const expectedY2 = 200 + (slotIndex2 + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY + const expectedY2 = + 200 + (slotIndex2 + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY expect(node.getInputSlotPos(inputSlot2)).toEqual([expectedX, expectedY2]) }) - test("should return default vertical position including slot_start_y when defined", () => { - (node.constructor as any).slot_start_y = 25 + test('should return default vertical position including slot_start_y when defined', () => { + ;(node.constructor as any).slot_start_y = 25 node.flags.collapsed = false node.inputs = [inputSlot] const slotIndex = 0 const nodeOffsetY = 25 - const expectedY = 200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY + const expectedY = + 200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY const expectedX = 100 + LiteGraph.NODE_SLOT_HEIGHT * 0.5 expect(node.getInputSlotPos(inputSlot)).toEqual([expectedX, expectedY]) delete (node.constructor as any).slot_start_y }) }) - describe("getInputPos", () => { - test("should call getInputSlotPos with the correct input slot from inputs array", () => { - const input0: INodeInputSlot = { name: "in0", type: "string", link: null, boundingRect: new Float32Array([0, 0, 0, 0]) } - const input1: INodeInputSlot = { name: "in1", type: "number", link: null, boundingRect: new Float32Array([0, 0, 0, 0]), pos: [5, 45] } + describe('getInputPos', () => { + test('should call getInputSlotPos with the correct input slot from inputs array', () => { + const input0: INodeInputSlot = { + name: 'in0', + type: 'string', + link: null, + boundingRect: new Float32Array([0, 0, 0, 0]) + } + const input1: INodeInputSlot = { + name: 'in1', + type: 'number', + link: null, + boundingRect: new Float32Array([0, 0, 0, 0]), + pos: [5, 45] + } node.inputs = [input0, input1] - const spy = vi.spyOn(node, "getInputSlotPos") + const spy = vi.spyOn(node, 'getInputSlotPos') node.getInputPos(1) expect(spy).toHaveBeenCalledWith(input1) const expectedPos: Point = [100 + 5, 200 + 45] @@ -569,7 +649,8 @@ describe("LGraphNode", () => { expect(spy).toHaveBeenCalledWith(input0) const slotIndex = 0 const nodeOffsetY = (node.constructor as any).slot_start_y || 0 - const expectedDefaultY = 200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY + const expectedDefaultY = + 200 + (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + nodeOffsetY const expectedDefaultX = 100 + LiteGraph.NODE_SLOT_HEIGHT * 0.5 expect(node.getInputPos(0)).toEqual([expectedDefaultX, expectedDefaultY]) spy.mockRestore() diff --git a/src/lib/litegraph/test/LGraphNode.titleButtons.test.ts b/src/lib/litegraph/test/LGraphNode.titleButtons.test.ts index 3600c95a1a..fb83b04c74 100644 --- a/src/lib/litegraph/test/LGraphNode.titleButtons.test.ts +++ b/src/lib/litegraph/test/LGraphNode.titleButtons.test.ts @@ -1,34 +1,34 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from 'vitest' -import { LGraphButton } from "@/lib/litegraph/src/LGraphButton" -import { LGraphCanvas } from "@/lib/litegraph/src/LGraphCanvas" -import { LGraphNode } from "@/lib/litegraph/src/LGraphNode" +import { LGraphButton } from '@/lib/litegraph/src/LGraphButton' +import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' +import { LGraphNode } from '@/lib/litegraph/src/LGraphNode' -describe("LGraphNode Title Buttons", () => { - describe("addTitleButton", () => { - it("should add a title button to the node", () => { - const node = new LGraphNode("Test Node") +describe('LGraphNode Title Buttons', () => { + describe('addTitleButton', () => { + it('should add a title button to the node', () => { + const node = new LGraphNode('Test Node') const button = node.addTitleButton({ - name: "test_button", - text: "X", - fgColor: "#FF0000", + name: 'test_button', + text: 'X', + fgColor: '#FF0000' }) expect(button).toBeInstanceOf(LGraphButton) - expect(button.name).toBe("test_button") - expect(button.text).toBe("X") - expect(button.fgColor).toBe("#FF0000") + expect(button.name).toBe('test_button') + expect(button.text).toBe('X') + expect(button.fgColor).toBe('#FF0000') expect(node.title_buttons).toHaveLength(1) expect(node.title_buttons[0]).toBe(button) }) - it("should add multiple title buttons", () => { - const node = new LGraphNode("Test Node") + it('should add multiple title buttons', () => { + const node = new LGraphNode('Test Node') - const button1 = node.addTitleButton({ name: "button1", text: "A" }) - const button2 = node.addTitleButton({ name: "button2", text: "B" }) - const button3 = node.addTitleButton({ name: "button3", text: "C" }) + const button1 = node.addTitleButton({ name: 'button1', text: 'A' }) + const button2 = node.addTitleButton({ name: 'button2', text: 'B' }) + const button3 = node.addTitleButton({ name: 'button3', text: 'C' }) expect(node.title_buttons).toHaveLength(3) expect(node.title_buttons[0]).toBe(button1) @@ -36,8 +36,8 @@ describe("LGraphNode Title Buttons", () => { expect(node.title_buttons[2]).toBe(button3) }) - it("should create buttons with default options", () => { - const node = new LGraphNode("Test Node") + it('should create buttons with default options', () => { + const node = new LGraphNode('Test Node') const button = node.addTitleButton({}) @@ -47,16 +47,16 @@ describe("LGraphNode Title Buttons", () => { }) }) - describe("onMouseDown with title buttons", () => { - it("should handle click on title button", () => { - const node = new LGraphNode("Test Node") + describe('onMouseDown with title buttons', () => { + it('should handle click on title button', () => { + const node = new LGraphNode('Test Node') node.pos = [100, 200] node.size = [180, 60] const button = node.addTitleButton({ - name: "close_button", - text: "X", - visible: true, + name: 'close_button', + text: 'X', + visible: true }) // Mock button dimensions @@ -74,39 +74,42 @@ describe("LGraphNode Title Buttons", () => { const canvas = { ctx: {} as CanvasRenderingContext2D, - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas const event = { canvasX: 265, // node.pos[0] + node.size[0] - 5 - button_width = 100 + 180 - 5 - 20 = 255, click in middle = 265 - canvasY: 178, // node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 8 = 200 - 30 + 8 = 178 + canvasY: 178 // node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 8 = 200 - 30 + 8 = 178 } as any // Calculate node-relative position for the click const clickPosRelativeToNode: [number, number] = [ 265 - node.pos[0], // 265 - 100 = 165 - 178 - node.pos[1], // 178 - 200 = -22 + 178 - node.pos[1] // 178 - 200 = -22 ] // Simulate the click - onMouseDown should detect button click const handled = node.onMouseDown(event, clickPosRelativeToNode, canvas) expect(handled).toBe(true) - expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", { - node: node, - button: button, - }) + expect(canvas.dispatch).toHaveBeenCalledWith( + 'litegraph:node-title-button-clicked', + { + node: node, + button: button + } + ) }) - it("should not handle click outside title buttons", () => { - const node = new LGraphNode("Test Node") + it('should not handle click outside title buttons', () => { + const node = new LGraphNode('Test Node') node.pos = [100, 200] node.size = [180, 60] const button = node.addTitleButton({ - name: "test_button", - text: "T", - visible: true, + name: 'test_button', + text: 'T', + visible: true }) button.getWidth = vi.fn().mockReturnValue(20) @@ -120,18 +123,18 @@ describe("LGraphNode Title Buttons", () => { const canvas = { ctx: {} as CanvasRenderingContext2D, - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas const event = { canvasX: 150, // Click in the middle of the node, not on button - canvasY: 180, + canvasY: 180 } as any // Calculate node-relative position const clickPosRelativeToNode: [number, number] = [ 150 - node.pos[0], // 150 - 100 = 50 - 180 - node.pos[1], // 180 - 200 = -20 + 180 - node.pos[1] // 180 - 200 = -20 ] const handled = node.onMouseDown(event, clickPosRelativeToNode, canvas) @@ -140,21 +143,21 @@ describe("LGraphNode Title Buttons", () => { expect(canvas.dispatch).not.toHaveBeenCalled() }) - it("should handle multiple buttons correctly", () => { - const node = new LGraphNode("Test Node") + it('should handle multiple buttons correctly', () => { + const node = new LGraphNode('Test Node') node.pos = [100, 200] node.size = [200, 60] const button1 = node.addTitleButton({ - name: "button1", - text: "A", - visible: true, + name: 'button1', + text: 'A', + visible: true }) const button2 = node.addTitleButton({ - name: "button2", - text: "B", - visible: true, + name: 'button2', + text: 'B', + visible: true }) // Mock button dimensions @@ -177,44 +180,47 @@ describe("LGraphNode Title Buttons", () => { const canvas = { ctx: {} as CanvasRenderingContext2D, - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas // Click on second button (leftmost, since they're right-aligned) const titleY = 170 + 8 // node.pos[1] - NODE_TITLE_HEIGHT + 8 = 200 - 30 + 8 = 178 const event = { canvasX: 255, // First button at: 100 + 200 - 5 - 20 = 275, Second button at: 275 - 5 - 20 = 250, click in middle = 255 - canvasY: titleY, + canvasY: titleY } as any // Calculate node-relative position const clickPosRelativeToNode: [number, number] = [ 255 - node.pos[0], // 255 - 100 = 155 - titleY - node.pos[1], // 178 - 200 = -22 + titleY - node.pos[1] // 178 - 200 = -22 ] const handled = node.onMouseDown(event, clickPosRelativeToNode, canvas) expect(handled).toBe(true) - expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", { - node: node, - button: button2, - }) + expect(canvas.dispatch).toHaveBeenCalledWith( + 'litegraph:node-title-button-clicked', + { + node: node, + button: button2 + } + ) }) - it("should skip invisible buttons", () => { - const node = new LGraphNode("Test Node") + it('should skip invisible buttons', () => { + const node = new LGraphNode('Test Node') node.pos = [100, 200] node.size = [180, 60] const button1 = node.addTitleButton({ - name: "invisible_button", - text: "", // Empty text makes it invisible + name: 'invisible_button', + text: '' // Empty text makes it invisible }) const button2 = node.addTitleButton({ - name: "visible_button", - text: "V", + name: 'visible_button', + text: 'V' }) button1.getWidth = vi.fn().mockReturnValue(20) @@ -230,47 +236,53 @@ describe("LGraphNode Title Buttons", () => { const canvas = { ctx: {} as CanvasRenderingContext2D, - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas // Click where the visible button is (invisible button is skipped) const titleY = 178 // node.pos[1] - NODE_TITLE_HEIGHT + 8 = 200 - 30 + 8 = 178 const event = { canvasX: 265, // Visible button at: 100 + 180 - 5 - 20 = 255, click in middle = 265 - canvasY: titleY, + canvasY: titleY } as any // Calculate node-relative position const clickPosRelativeToNode: [number, number] = [ 265 - node.pos[0], // 265 - 100 = 165 - titleY - node.pos[1], // 178 - 200 = -22 + titleY - node.pos[1] // 178 - 200 = -22 ] const handled = node.onMouseDown(event, clickPosRelativeToNode, canvas) expect(handled).toBe(true) - expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", { - node: node, - button: button2, // Should click visible button, not invisible - }) + expect(canvas.dispatch).toHaveBeenCalledWith( + 'litegraph:node-title-button-clicked', + { + node: node, + button: button2 // Should click visible button, not invisible + } + ) }) }) - describe("onTitleButtonClick", () => { - it("should dispatch litegraph:node-title-button-clicked event", () => { - const node = new LGraphNode("Test Node") - const button = new LGraphButton({ name: "test_button" }) + describe('onTitleButtonClick', () => { + it('should dispatch litegraph:node-title-button-clicked event', () => { + const node = new LGraphNode('Test Node') + const button = new LGraphButton({ name: 'test_button' }) const canvas = { - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas node.onTitleButtonClick(button, canvas) - expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", { - node: node, - button: button, - }) + expect(canvas.dispatch).toHaveBeenCalledWith( + 'litegraph:node-title-button-clicked', + { + node: node, + button: button + } + ) }) }) }) diff --git a/src/lib/litegraph/test/LGraph_constructor.test.ts b/src/lib/litegraph/test/LGraph_constructor.test.ts index dfe5e29149..30c08a03fe 100644 --- a/src/lib/litegraph/test/LGraph_constructor.test.ts +++ b/src/lib/litegraph/test/LGraph_constructor.test.ts @@ -1,15 +1,18 @@ -import { describe } from "vitest" +import { describe } from 'vitest' -import { LGraph } from "@/lib/litegraph/src/litegraph" +import { LGraph } from '@/lib/litegraph/src/litegraph' -import { dirtyTest } from "./testExtensions" +import { dirtyTest } from './testExtensions' -describe("LGraph (constructor only)", () => { - dirtyTest("Matches previous snapshot", ({ expect, minimalSerialisableGraph, basicSerialisableGraph }) => { - const minLGraph = new LGraph(minimalSerialisableGraph) - expect(minLGraph).toMatchSnapshot("minLGraph") +describe('LGraph (constructor only)', () => { + dirtyTest( + 'Matches previous snapshot', + ({ expect, minimalSerialisableGraph, basicSerialisableGraph }) => { + const minLGraph = new LGraph(minimalSerialisableGraph) + expect(minLGraph).toMatchSnapshot('minLGraph') - const basicLGraph = new LGraph(basicSerialisableGraph) - expect(basicLGraph).toMatchSnapshot("basicLGraph") - }) + const basicLGraph = new LGraph(basicSerialisableGraph) + expect(basicLGraph).toMatchSnapshot('basicLGraph') + } + ) }) diff --git a/src/lib/litegraph/test/LLink.test.ts b/src/lib/litegraph/test/LLink.test.ts index fea2bd6571..58f2501fbf 100644 --- a/src/lib/litegraph/test/LLink.test.ts +++ b/src/lib/litegraph/test/LLink.test.ts @@ -1,17 +1,17 @@ -import { describe, expect } from "vitest" +import { describe, expect } from 'vitest' -import { LLink } from "@/lib/litegraph/src/litegraph" +import { LLink } from '@/lib/litegraph/src/litegraph' -import { test } from "./testExtensions" +import { test } from './testExtensions' -describe("LLink", () => { - test("matches previous snapshot", () => { - const link = new LLink(1, "float", 4, 2, 5, 3) - expect(link.serialize()).toMatchSnapshot("Basic") +describe('LLink', () => { + test('matches previous snapshot', () => { + const link = new LLink(1, 'float', 4, 2, 5, 3) + expect(link.serialize()).toMatchSnapshot('Basic') }) - test("serializes to the previous snapshot", () => { - const link = new LLink(1, "float", 4, 2, 5, 3) - expect(link.serialize()).toMatchSnapshot("Basic") + test('serializes to the previous snapshot', () => { + const link = new LLink(1, 'float', 4, 2, 5, 3) + expect(link.serialize()).toMatchSnapshot('Basic') }) }) diff --git a/src/lib/litegraph/test/LinkConnector.integration.test.ts b/src/lib/litegraph/test/LinkConnector.integration.test.ts index 6ef80b54f0..d9b67054f2 100644 --- a/src/lib/litegraph/test/LinkConnector.integration.test.ts +++ b/src/lib/litegraph/test/LinkConnector.integration.test.ts @@ -1,11 +1,16 @@ -import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events" +import { afterEach, describe, expect, vi } from 'vitest' -import { afterEach, describe, expect, vi } from "vitest" +import { + LGraph, + LGraphNode, + LLink, + Reroute, + type RerouteId +} from '@/lib/litegraph/src/litegraph' +import { LinkConnector } from '@/lib/litegraph/src/litegraph' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' -import { LGraph, LGraphNode, LLink, Reroute, type RerouteId } from "@/lib/litegraph/src/litegraph" -import { LinkConnector } from "@/lib/litegraph/src/litegraph" - -import { test as baseTest } from "./testExtensions" +import { test as baseTest } from './testExtensions' interface TestContext { graph: LGraph @@ -16,7 +21,10 @@ interface TestContext { validateIntegrityNoChanges: () => void validateIntegrityFloatingRemoved: () => void validateLinkIntegrity: () => void - getNextLinkIds: (linkIds: Set, expectedExtraLinks?: number) => number[] + getNextLinkIds: ( + linkIds: Set, + expectedExtraLinks?: number + ) => number[] readonly floatingReroute: Reroute } @@ -32,7 +40,10 @@ const test = baseTest.extend({ } await use(reroutesComplexGraph) }, - setConnectingLinks: async ({}, use: (mock: ReturnType) => Promise) => { + setConnectingLinks: async ( + {}, + use: (mock: ReturnType) => Promise + ) => { const mock = vi.fn() await use(mock) }, @@ -42,27 +53,35 @@ const test = baseTest.extend({ }, createTestNode: async ({ graph }, use) => { await use((id): LGraphNode => { - const node = new LGraphNode("test") + const node = new LGraphNode('test') node.id = id graph.add(node) return node }) }, - validateIntegrityNoChanges: async ({ graph, reroutesBeforeTest, expect }, use) => { + validateIntegrityNoChanges: async ( + { graph, reroutesBeforeTest, expect }, + use + ) => { await use(() => { expect(graph.floatingLinks.size).toBe(1) expect([...graph.reroutes]).toEqual(reroutesBeforeTest) // Only the original reroute should be floating - const reroutesExceptOne = [...graph.reroutes.values()].filter(reroute => reroute.id !== 1) + const reroutesExceptOne = [...graph.reroutes.values()].filter( + (reroute) => reroute.id !== 1 + ) for (const reroute of reroutesExceptOne) { expect(reroute.floating).toBeUndefined() } }) }, - validateIntegrityFloatingRemoved: async ({ graph, reroutesBeforeTest, expect }, use) => { + validateIntegrityFloatingRemoved: async ( + { graph, reroutesBeforeTest, expect }, + use + ) => { await use(() => { expect(graph.floatingLinks.size).toBe(0) expect([...graph.reroutes]).toEqual(reroutesBeforeTest) @@ -120,8 +139,12 @@ const test = baseTest.extend({ } for (const link of graph._links.values()) { - expect(graph.getNodeById(link!.origin_id)?.outputs[link!.origin_slot].links).toContain(link.id) - expect(graph.getNodeById(link!.target_id)?.inputs[link!.target_slot].link).toBe(link.id) + expect( + graph.getNodeById(link!.origin_id)?.outputs[link!.origin_slot].links + ).toContain(link.id) + expect( + graph.getNodeById(link!.target_id)?.inputs[link!.target_slot].link + ).toBe(link.id) } for (const link of graph.floatingLinks.values()) { @@ -129,14 +152,17 @@ const test = baseTest.extend({ expect(link.origin_id).not.toBe(-1) expect(link.origin_slot).not.toBe(-1) expect(link.target_slot).toBe(-1) - const outputFloatingLinks = graph.getNodeById(link.origin_id)?.outputs[link.origin_slot]._floatingLinks + const outputFloatingLinks = graph.getNodeById(link.origin_id) + ?.outputs[link.origin_slot]._floatingLinks expect(outputFloatingLinks).toBeDefined() expect(outputFloatingLinks).toContain(link) } else { expect(link.origin_id).toBe(-1) expect(link.origin_slot).toBe(-1) expect(link.target_slot).not.toBe(-1) - const inputFloatingLinks = graph.getNodeById(link.target_id)?.inputs[link.target_slot]._floatingLinks + const inputFloatingLinks = graph.getNodeById(link.target_id)?.inputs[ + link.target_slot + ]._floatingLinks expect(inputFloatingLinks).toBeDefined() expect(inputFloatingLinks).toContain(link) } @@ -147,56 +173,65 @@ const test = baseTest.extend({ getNextLinkIds: async ({ graph }, use) => { await use((linkIds, expectedExtraLinks = 0) => { const indexes = [...new Array(linkIds.size + expectedExtraLinks).keys()] - return indexes.map(index => graph.last_link_id + index + 1) + return indexes.map((index) => graph.last_link_id + index + 1) }) }, floatingReroute: async ({ graph, expect }, use) => { const floatingReroute = graph.reroutes.get(1)! - expect(floatingReroute.floating).toEqual({ slotType: "output" }) + expect(floatingReroute.floating).toEqual({ slotType: 'output' }) await use(floatingReroute) - }, + } }) function mockedNodeTitleDropEvent(node: LGraphNode): CanvasPointerEvent { return { canvasX: node.pos[0] + node.size[0] / 2, - canvasY: node.pos[1] + 16, + canvasY: node.pos[1] + 16 } as any } -function mockedInputDropEvent(node: LGraphNode, slot: number): CanvasPointerEvent { +function mockedInputDropEvent( + node: LGraphNode, + slot: number +): CanvasPointerEvent { const pos = node.getInputPos(slot) return { canvasX: pos[0], - canvasY: pos[1], + canvasY: pos[1] } as any } -function mockedOutputDropEvent(node: LGraphNode, slot: number): CanvasPointerEvent { +function mockedOutputDropEvent( + node: LGraphNode, + slot: number +): CanvasPointerEvent { const pos = node.getOutputPos(slot) return { canvasX: pos[0], - canvasY: pos[1], + canvasY: pos[1] } as any } -describe("LinkConnector Integration", () => { +describe('LinkConnector Integration', () => { afterEach(({ validateLinkIntegrity }) => { validateLinkIntegrity() }) - describe("Moving input links", () => { - test("Should move input links", ({ graph, connector }) => { + describe('Moving input links', () => { + test('Should move input links', ({ graph, connector }) => { const nextLinkId = graph.last_link_id + 1 const hasInputNode = graph.getNodeById(2)! const disconnectedNode = graph.getNodeById(9)! - const reroutesBefore = LLink.getReroutes(graph, graph.links.get(hasInputNode.inputs[0].link!)!) + const reroutesBefore = LLink.getReroutes( + graph, + graph.links.get(hasInputNode.inputs[0].link!)! + ) connector.moveInputLink(graph, hasInputNode.inputs[0]) - expect(connector.state.connectingTo).toBe("input") + expect(connector.state.connectingTo).toBe('input') expect(connector.state.draggingExistingLinks).toBe(true) expect(connector.renderLinks.length).toBe(1) expect(connector.inputLinks.length).toBe(1) @@ -217,11 +252,18 @@ describe("LinkConnector Integration", () => { expect(disconnectedNode.inputs[0].link).toBe(nextLinkId) expect(hasInputNode.inputs[0].link).toBeNull() - const reroutesAfter = LLink.getReroutes(graph, graph.links.get(disconnectedNode.inputs[0].link!)!) + const reroutesAfter = LLink.getReroutes( + graph, + graph.links.get(disconnectedNode.inputs[0].link!)! + ) expect(reroutesAfter).toEqual(reroutesBefore) }) - test("Should connect from floating reroutes", ({ graph, connector, reroutesBeforeTest }) => { + test('Should connect from floating reroutes', ({ + graph, + connector, + reroutesBeforeTest + }) => { const nextLinkId = graph.last_link_id + 1 const floatingLink = graph.floatingLinks.values().next().value! @@ -231,7 +273,7 @@ describe("LinkConnector Integration", () => { const disconnectedNode = graph.getNodeById(9)! connector.dragFromReroute(graph, floatingReroute) - expect(connector.state.connectingTo).toBe("input") + expect(connector.state.connectingTo).toBe('input') expect(connector.state.draggingExistingLinks).toBe(false) expect(connector.renderLinks.length).toBe(1) expect(connector.inputLinks.length).toBe(0) @@ -258,7 +300,10 @@ describe("LinkConnector Integration", () => { } }) - test("Should drop floating links when both sides are disconnected", ({ graph, reroutesBeforeTest }) => { + test('Should drop floating links when both sides are disconnected', ({ + graph, + reroutesBeforeTest + }) => { expect(graph.floatingLinks.size).toBe(1) const floatingOutNode = graph.getNodeById(1)! @@ -282,7 +327,10 @@ describe("LinkConnector Integration", () => { // All four nodes should have no links for (const nodeId of [1, 2, 3, 9]) { - const { inputs: [input], outputs: [output] } = graph.getNodeById(nodeId)! + const { + inputs: [input], + outputs: [output] + } = graph.getNodeById(nodeId)! expect(input.link).toBeNull() expect(output.links?.length).toBeOneOf([0, undefined]) @@ -292,12 +340,18 @@ describe("LinkConnector Integration", () => { } }) - test("Should prevent node loopback when dropping on node", ({ graph, connector }) => { + test('Should prevent node loopback when dropping on node', ({ + graph, + connector + }) => { const hasOutputNode = graph.getNodeById(1)! const hasInputNode = graph.getNodeById(2)! const hasInputNode2 = graph.getNodeById(3)! - const reroutesBefore = LLink.getReroutes(graph, graph.links.get(hasInputNode.inputs[0].link!)!) + const reroutesBefore = LLink.getReroutes( + graph, + graph.links.get(hasInputNode.inputs[0].link!)! + ) const atOutputNodeEvent = mockedNodeTitleDropEvent(hasOutputNode) @@ -308,16 +362,25 @@ describe("LinkConnector Integration", () => { const outputNodes = hasOutputNode.getOutputNodes(0) expect(outputNodes).toEqual([hasInputNode, hasInputNode2]) - const reroutesAfter = LLink.getReroutes(graph, graph.links.get(hasInputNode.inputs[0].link!)!) + const reroutesAfter = LLink.getReroutes( + graph, + graph.links.get(hasInputNode.inputs[0].link!)! + ) expect(reroutesAfter).toEqual(reroutesBefore) }) - test("Should prevent node loopback when dropping on input", ({ graph, connector }) => { + test('Should prevent node loopback when dropping on input', ({ + graph, + connector + }) => { const hasOutputNode = graph.getNodeById(1)! const hasInputNode = graph.getNodeById(2)! const originalOutputNodes = hasOutputNode.getOutputNodes(0) - const reroutesBefore = LLink.getReroutes(graph, graph.links.get(hasInputNode.inputs[0].link!)!) + const reroutesBefore = LLink.getReroutes( + graph, + graph.links.get(hasInputNode.inputs[0].link!)! + ) const atHasOutputNode = mockedInputDropEvent(hasOutputNode, 0) @@ -328,24 +391,27 @@ describe("LinkConnector Integration", () => { const outputNodes = hasOutputNode.getOutputNodes(0) expect(outputNodes).toEqual(originalOutputNodes) - const reroutesAfter = LLink.getReroutes(graph, graph.links.get(hasInputNode.inputs[0].link!)!) + const reroutesAfter = LLink.getReroutes( + graph, + graph.links.get(hasInputNode.inputs[0].link!)! + ) expect(reroutesAfter).toEqual(reroutesBefore) }) }) - describe("Moving output links", () => { - test("Should move output links", ({ graph, connector }) => { + describe('Moving output links', () => { + test('Should move output links', ({ graph, connector }) => { const nextLinkIds = [graph.last_link_id + 1, graph.last_link_id + 2] const hasOutputNode = graph.getNodeById(1)! const disconnectedNode = graph.getNodeById(9)! const reroutesBefore = hasOutputNode.outputs[0].links - ?.map(linkId => graph.links.get(linkId)!) - .map(link => LLink.getReroutes(graph, link)) + ?.map((linkId) => graph.links.get(linkId)!) + .map((link) => LLink.getReroutes(graph, link)) connector.moveOutputLink(graph, hasOutputNode.outputs[0]) - expect(connector.state.connectingTo).toBe("output") + expect(connector.state.connectingTo).toBe('output') expect(connector.state.draggingExistingLinks).toBe(true) expect(connector.renderLinks.length).toBe(3) expect(connector.outputLinks.length).toBe(2) @@ -364,13 +430,17 @@ describe("LinkConnector Integration", () => { expect(hasOutputNode.outputs[0].links).toEqual([]) const reroutesAfter = disconnectedNode.outputs[0].links - ?.map(linkId => graph.links.get(linkId)!) - .map(link => LLink.getReroutes(graph, link)) + ?.map((linkId) => graph.links.get(linkId)!) + .map((link) => LLink.getReroutes(graph, link)) expect(reroutesAfter).toEqual(reroutesBefore) }) - test("Should connect to floating reroutes from outputs", ({ graph, connector, reroutesBeforeTest }) => { + test('Should connect to floating reroutes from outputs', ({ + graph, + connector, + reroutesBeforeTest + }) => { const nextLinkIds = [graph.last_link_id + 1, graph.last_link_id + 2] const floatingOutNode = graph.getNodeById(1)! @@ -384,9 +454,13 @@ describe("LinkConnector Integration", () => { expect(graph.floatingLinks.size).toBe(2) const disconnectedNode = graph.getNodeById(9)! - connector.dragNewFromOutput(graph, disconnectedNode, disconnectedNode.outputs[0]) + connector.dragNewFromOutput( + graph, + disconnectedNode, + disconnectedNode.outputs[0] + ) - expect(connector.state.connectingTo).toBe("input") + expect(connector.state.connectingTo).toBe('input') expect(connector.state.draggingExistingLinks).toBe(false) expect(connector.renderLinks.length).toBe(1) expect(connector.outputLinks.length).toBe(0) @@ -417,14 +491,19 @@ describe("LinkConnector Integration", () => { } }) - test("Should drop floating links when both sides are disconnected", ({ graph, reroutesBeforeTest }) => { + test('Should drop floating links when both sides are disconnected', ({ + graph, + reroutesBeforeTest + }) => { expect(graph.floatingLinks.size).toBe(1) graph.getNodeById(2)!.disconnectInput(0, true) expect(graph.floatingLinks.size).toBe(1) // Only the original reroute should be floating - const reroutesExceptOne = [...graph.reroutes.values()].filter(reroute => reroute.id !== 1) + const reroutesExceptOne = [...graph.reroutes.values()].filter( + (reroute) => reroute.id !== 1 + ) for (const reroute of reroutesExceptOne) { expect(reroute.floating).toBeUndefined() } @@ -434,7 +513,7 @@ describe("LinkConnector Integration", () => { // The normal link should now be floating expect(graph.floatingLinks.size).toBe(2) - expect(graph.reroutes.get(3)!.floating).toEqual({ slotType: "output" }) + expect(graph.reroutes.get(3)!.floating).toEqual({ slotType: 'output' }) const floatingOutNode = graph.getNodeById(1)! floatingOutNode.disconnectOutput(0) @@ -448,7 +527,10 @@ describe("LinkConnector Integration", () => { // All four nodes should have no links for (const nodeId of [1, 2, 3, 9]) { - const { inputs: [input], outputs: [output] } = graph.getNodeById(nodeId)! + const { + inputs: [input], + outputs: [output] + } = graph.getNodeById(nodeId)! expect(input.link).toBeNull() expect(output.links?.length).toBeOneOf([0, undefined]) @@ -458,7 +540,12 @@ describe("LinkConnector Integration", () => { } }) - test("Should support moving multiple output links to a floating reroute", ({ graph, connector, floatingReroute, validateIntegrityFloatingRemoved }) => { + test('Should support moving multiple output links to a floating reroute', ({ + graph, + connector, + floatingReroute, + validateIntegrityFloatingRemoved + }) => { const manyOutputsNode = graph.getNodeById(4)! const canvasX = floatingReroute.pos[0] const canvasY = floatingReroute.pos[1] @@ -474,7 +561,11 @@ describe("LinkConnector Integration", () => { validateIntegrityFloatingRemoved() }) - test("Should prevent dragging from an output to a child reroute", ({ graph, connector, floatingReroute }) => { + test('Should prevent dragging from an output to a child reroute', ({ + graph, + connector, + floatingReroute + }) => { const manyOutputsNode = graph.getNodeById(4)! const reroute7 = graph.reroutes.get(7)! @@ -485,12 +576,15 @@ describe("LinkConnector Integration", () => { const canvasY = reroute7.pos[1] const reroute7Event = { canvasX, canvasY } as any - const toSortedRerouteChain = (linkIds: number[]) => linkIds - .map(x => graph.links.get(x)!) - .map(x => LLink.getReroutes(graph, x)) - .sort((a, b) => a.at(-1)!.id - b.at(-1)!.id) + const toSortedRerouteChain = (linkIds: number[]) => + linkIds + .map((x) => graph.links.get(x)!) + .map((x) => LLink.getReroutes(graph, x)) + .sort((a, b) => a.at(-1)!.id - b.at(-1)!.id) - const reroutesBefore = toSortedRerouteChain(manyOutputsNode.outputs[0].links!) + const reroutesBefore = toSortedRerouteChain( + manyOutputsNode.outputs[0].links! + ) connector.moveOutputLink(graph, manyOutputsNode.outputs[0]) expect(connector.isRerouteValidDrop(reroute7)).toBe(false) @@ -498,22 +592,34 @@ describe("LinkConnector Integration", () => { expect(connector.isRerouteValidDrop(reroute13)).toBe(false) // Prevent link disconnect when dropped on canvas (just for this test) - connector.events.addEventListener("dropped-on-canvas", e => e.preventDefault(), { once: true }) + connector.events.addEventListener( + 'dropped-on-canvas', + (e) => e.preventDefault(), + { once: true } + ) connector.dropLinks(graph, reroute7Event) connector.reset() - const reroutesAfter = toSortedRerouteChain(manyOutputsNode.outputs[0].links!) + const reroutesAfter = toSortedRerouteChain( + manyOutputsNode.outputs[0].links! + ) expect(reroutesAfter).toEqual(reroutesBefore) expect(graph.floatingLinks.size).toBe(1) expect(floatingReroute.linkIds.size).toBe(0) }) - test("Should prevent node loopback when dropping on node", ({ graph, connector }) => { + test('Should prevent node loopback when dropping on node', ({ + graph, + connector + }) => { const hasOutputNode = graph.getNodeById(1)! const hasInputNode = graph.getNodeById(2)! - const reroutesBefore = LLink.getReroutes(graph, graph.links.get(hasOutputNode.outputs[0].links![0])!) + const reroutesBefore = LLink.getReroutes( + graph, + graph.links.get(hasOutputNode.outputs[0].links![0])! + ) const atInputNodeEvent = mockedNodeTitleDropEvent(hasInputNode) @@ -525,19 +631,31 @@ describe("LinkConnector Integration", () => { expect(hasInputNode.getOutputNodes(0)).toEqual([graph.getNodeById(3)]) // Moved link should have the same reroutes - const reroutesAfter = LLink.getReroutes(graph, graph.links.get(hasInputNode.outputs[0].links![0])!) + const reroutesAfter = LLink.getReroutes( + graph, + graph.links.get(hasInputNode.outputs[0].links![0])! + ) expect(reroutesAfter).toEqual(reroutesBefore) // Link recreated to avoid loopback should have no reroutes - const reroutesAfter2 = LLink.getReroutes(graph, graph.links.get(hasOutputNode.outputs[0].links![0])!) + const reroutesAfter2 = LLink.getReroutes( + graph, + graph.links.get(hasOutputNode.outputs[0].links![0])! + ) expect(reroutesAfter2).toEqual([]) }) - test("Should prevent node loopback when dropping on output", ({ graph, connector }) => { + test('Should prevent node loopback when dropping on output', ({ + graph, + connector + }) => { const hasOutputNode = graph.getNodeById(1)! const hasInputNode = graph.getNodeById(2)! - const reroutesBefore = LLink.getReroutes(graph, graph.links.get(hasOutputNode.outputs[0].links![0])!) + const reroutesBefore = LLink.getReroutes( + graph, + graph.links.get(hasOutputNode.outputs[0].links![0])! + ) const atInputNodeOutSlot = mockedOutputDropEvent(hasInputNode, 0) @@ -549,17 +667,27 @@ describe("LinkConnector Integration", () => { expect(hasInputNode.getOutputNodes(0)).toEqual([graph.getNodeById(3)]) // Moved link should have the same reroutes - const reroutesAfter = LLink.getReroutes(graph, graph.links.get(hasInputNode.outputs[0].links![0])!) + const reroutesAfter = LLink.getReroutes( + graph, + graph.links.get(hasInputNode.outputs[0].links![0])! + ) expect(reroutesAfter).toEqual(reroutesBefore) // Link recreated to avoid loopback should have no reroutes - const reroutesAfter2 = LLink.getReroutes(graph, graph.links.get(hasOutputNode.outputs[0].links![0])!) + const reroutesAfter2 = LLink.getReroutes( + graph, + graph.links.get(hasOutputNode.outputs[0].links![0])! + ) expect(reroutesAfter2).toEqual([]) }) }) - describe("Floating links", () => { - test("Removed when connecting from reroute to input", ({ graph, connector, floatingReroute }) => { + describe('Floating links', () => { + test('Removed when connecting from reroute to input', ({ + graph, + connector, + floatingReroute + }) => { const disconnectedNode = graph.getNodeById(9)! const canvasX = disconnectedNode.pos[0] const canvasY = disconnectedNode.pos[1] @@ -572,7 +700,12 @@ describe("LinkConnector Integration", () => { expect(floatingReroute.floating).toBeUndefined() }) - test("Removed when connecting from reroute to another reroute", ({ graph, connector, floatingReroute, validateIntegrityFloatingRemoved }) => { + test('Removed when connecting from reroute to another reroute', ({ + graph, + connector, + floatingReroute, + validateIntegrityFloatingRemoved + }) => { const reroute8 = graph.reroutes.get(8)! const canvasX = reroute8.pos[0] const canvasY = reroute8.pos[1] @@ -588,7 +721,10 @@ describe("LinkConnector Integration", () => { validateIntegrityFloatingRemoved() }) - test("Dropping a floating input link onto input slot disconnects the existing link", ({ graph, connector }) => { + test('Dropping a floating input link onto input slot disconnects the existing link', ({ + graph, + connector + }) => { const manyOutputsNode = graph.getNodeById(4)! manyOutputsNode.disconnectOutput(0) @@ -610,7 +746,12 @@ describe("LinkConnector Integration", () => { expect(toInput._floatingLinks?.size).toBe(1) }) - test("Allow reroutes to be used as manual switches", ({ graph, connector, floatingReroute, validateIntegrityNoChanges }) => { + test('Allow reroutes to be used as manual switches', ({ + graph, + connector, + floatingReroute, + validateIntegrityNoChanges + }) => { const rerouteWithTwoLinks = graph.reroutes.get(3)! const targetNode = graph.getNodeById(2)! @@ -635,7 +776,7 @@ describe("LinkConnector Integration", () => { // Everything should be back the way it was when we started expect(rerouteWithTwoLinks.floating).toBeUndefined() - expect(floatingReroute.floating).toEqual({ slotType: "output" }) + expect(floatingReroute.floating).toEqual({ slotType: 'output' }) expect(rerouteWithTwoLinks.floatingLinkIds.size).toBe(0) expect(floatingReroute.floatingLinkIds.size).toBe(1) expect(rerouteWithTwoLinks.linkIds.size).toBe(2) @@ -645,12 +786,20 @@ describe("LinkConnector Integration", () => { }) }) - test("Should drop floating links when both sides are disconnected", ({ graph, connector, reroutesBeforeTest, validateIntegrityNoChanges }) => { + test('Should drop floating links when both sides are disconnected', ({ + graph, + connector, + reroutesBeforeTest, + validateIntegrityNoChanges + }) => { const floatingOutNode = graph.getNodeById(1)! connector.moveOutputLink(graph, floatingOutNode.outputs[0]) const manyOutputsNode = graph.getNodeById(4)! - const dropEvent = { canvasX: manyOutputsNode.pos[0], canvasY: manyOutputsNode.pos[1] } as any + const dropEvent = { + canvasX: manyOutputsNode.pos[0], + canvasY: manyOutputsNode.pos[1] + } as any connector.dropLinks(graph, dropEvent) connector.reset() @@ -683,7 +832,7 @@ describe("LinkConnector Integration", () => { // The final reroutes should all be floating for (const reroute of graph.reroutes.values()) { if ([3, 7, 15, 12].includes(reroute.id)) { - expect(reroute.floating).toEqual({ slotType: "input" }) + expect(reroute.floating).toEqual({ slotType: 'input' }) } else { expect(reroute.floating).toBeUndefined() } @@ -694,7 +843,10 @@ describe("LinkConnector Integration", () => { // Original nodes should have no links for (const nodeId of [1, 4]) { - const { inputs: [input], outputs: [output] } = graph.getNodeById(nodeId)! + const { + inputs: [input], + outputs: [output] + } = graph.getNodeById(nodeId)! expect(input.link).toBeNull() expect(output.links?.length).toBeOneOf([0, undefined]) @@ -723,92 +875,108 @@ describe("LinkConnector Integration", () => { parentIds: [13, 10], linksBefore: [3, 4], linksAfter: [1, 2], - runIntegrityCheck: true, + runIntegrityCheck: true }, { targetRerouteId: 7, parentIds: [6, 8, 13, 10], linksBefore: [2, 2, 3, 4], linksAfter: [undefined, undefined, 1, 2], - runIntegrityCheck: false, + runIntegrityCheck: false }, { targetRerouteId: 6, parentIds: [8, 13, 10], linksBefore: [2, 3, 4], linksAfter: [undefined, 1, 2], - runIntegrityCheck: false, + runIntegrityCheck: false }, { targetRerouteId: 13, parentIds: [10], linksBefore: [4], linksAfter: [1], - runIntegrityCheck: true, + runIntegrityCheck: true }, { targetRerouteId: 4, parentIds: [], linksBefore: [], linksAfter: [], - runIntegrityCheck: true, + runIntegrityCheck: true }, { targetRerouteId: 2, parentIds: [4], linksBefore: [2], linksAfter: [undefined], - runIntegrityCheck: false, + runIntegrityCheck: false }, { targetRerouteId: 3, parentIds: [2, 4], linksBefore: [2, 2], linksAfter: [0, 0], - runIntegrityCheck: true, - }, - ])("Should allow reconnect from output to any reroute", ( - { targetRerouteId, parentIds, linksBefore, linksAfter, runIntegrityCheck }, - { graph, connector, validateIntegrityNoChanges, getNextLinkIds }, - ) => { - const linkCreatedCallback = vi.fn() - connector.listenUntilReset("link-created", linkCreatedCallback) - - const disconnectedNode = graph.getNodeById(9)! - - // Parent reroutes of the target reroute - for (const [index, parentId] of parentIds.entries()) { - const reroute = graph.reroutes.get(parentId)! - expect(reroute.linkIds.size).toBe(linksBefore[index]) + runIntegrityCheck: true } + ])( + 'Should allow reconnect from output to any reroute', + ( + { + targetRerouteId, + parentIds, + linksBefore, + linksAfter, + runIntegrityCheck + }, + { graph, connector, validateIntegrityNoChanges, getNextLinkIds } + ) => { + const linkCreatedCallback = vi.fn() + connector.listenUntilReset('link-created', linkCreatedCallback) - const targetReroute = graph.reroutes.get(targetRerouteId)! - const nextLinkIds = getNextLinkIds(targetReroute.linkIds) - const dropEvent = { canvasX: targetReroute.pos[0], canvasY: targetReroute.pos[1] } as any + const disconnectedNode = graph.getNodeById(9)! - connector.dragNewFromOutput(graph, disconnectedNode, disconnectedNode.outputs[0]) - connector.dropLinks(graph, dropEvent) - connector.reset() + // Parent reroutes of the target reroute + for (const [index, parentId] of parentIds.entries()) { + const reroute = graph.reroutes.get(parentId)! + expect(reroute.linkIds.size).toBe(linksBefore[index]) + } - expect(disconnectedNode.outputs[0].links).toEqual(nextLinkIds) - expect([...targetReroute.linkIds.values()]).toEqual(nextLinkIds) + const targetReroute = graph.reroutes.get(targetRerouteId)! + const nextLinkIds = getNextLinkIds(targetReroute.linkIds) + const dropEvent = { + canvasX: targetReroute.pos[0], + canvasY: targetReroute.pos[1] + } as any - // Parent reroutes should have lost the links or been removed - for (const [index, parentId] of parentIds.entries()) { - const reroute = graph.reroutes.get(parentId)! - if (linksAfter[index] === undefined) { - expect(reroute).not.toBeUndefined() - } else { - expect(reroute.linkIds.size).toBe(linksAfter[index]) + connector.dragNewFromOutput( + graph, + disconnectedNode, + disconnectedNode.outputs[0] + ) + connector.dropLinks(graph, dropEvent) + connector.reset() + + expect(disconnectedNode.outputs[0].links).toEqual(nextLinkIds) + expect([...targetReroute.linkIds.values()]).toEqual(nextLinkIds) + + // Parent reroutes should have lost the links or been removed + for (const [index, parentId] of parentIds.entries()) { + const reroute = graph.reroutes.get(parentId)! + if (linksAfter[index] === undefined) { + expect(reroute).not.toBeUndefined() + } else { + expect(reroute.linkIds.size).toBe(linksAfter[index]) + } + } + + expect(linkCreatedCallback).toHaveBeenCalledTimes(nextLinkIds.length) + + if (runIntegrityCheck) { + validateIntegrityNoChanges() } } - - expect(linkCreatedCallback).toHaveBeenCalledTimes(nextLinkIds.length) - - if (runIntegrityCheck) { - validateIntegrityNoChanges() - } - }) + ) type ReconnectTestData = { /** Drag link from this reroute */ @@ -830,37 +998,37 @@ describe("LinkConnector Integration", () => { fromRerouteId: 10, toRerouteId: 15, shouldBeRemoved: [14], - shouldHaveLinkIdsRemoved: [13, 8, 6, 7], + shouldHaveLinkIdsRemoved: [13, 8, 6, 7] }, { fromRerouteId: 8, toRerouteId: 2, shouldBeRemoved: [4], - shouldHaveLinkIdsRemoved: [], + shouldHaveLinkIdsRemoved: [] }, { fromRerouteId: 3, toRerouteId: 12, shouldBeRemoved: [11], - shouldHaveLinkIdsRemoved: [10, 13, 14, 15, 8, 6, 7], + shouldHaveLinkIdsRemoved: [10, 13, 14, 15, 8, 6, 7] }, { fromRerouteId: 15, toRerouteId: 7, shouldBeRemoved: [8, 6], - shouldHaveLinkIdsRemoved: [], + shouldHaveLinkIdsRemoved: [] }, { fromRerouteId: 1, toRerouteId: 7, shouldBeRemoved: [8, 6], - shouldHaveLinkIdsRemoved: [], + shouldHaveLinkIdsRemoved: [] }, { fromRerouteId: 1, toRerouteId: 10, shouldBeRemoved: [], - shouldHaveLinkIdsRemoved: [], + shouldHaveLinkIdsRemoved: [] }, { fromRerouteId: 4, @@ -868,7 +1036,7 @@ describe("LinkConnector Integration", () => { shouldBeRemoved: [], shouldHaveLinkIdsRemoved: [], testFloatingInputs: true, - expectedExtraLinks: 2, + expectedExtraLinks: 2 }, { fromRerouteId: 2, @@ -876,80 +1044,102 @@ describe("LinkConnector Integration", () => { shouldBeRemoved: [11], shouldHaveLinkIdsRemoved: [], testFloatingInputs: true, - expectedExtraLinks: 1, - }, - ])("Should allow connecting from reroutes to another reroute", ( - { fromRerouteId, toRerouteId, shouldBeRemoved, shouldHaveLinkIdsRemoved, testFloatingInputs, expectedExtraLinks }, - { graph, connector, getNextLinkIds }, - ) => { - if (testFloatingInputs) { - // Start by disconnecting the output of the 3x3 array of reroutes - graph.getNodeById(4)!.disconnectOutput(0) + expectedExtraLinks: 1 } - - const fromReroute = graph.reroutes.get(fromRerouteId)! - const toReroute = graph.reroutes.get(toRerouteId)! - const nextLinkIds = getNextLinkIds(toReroute.linkIds, expectedExtraLinks) - - const originalParentChain = LLink.getReroutes(graph, toReroute) - - const sortAndJoin = (numbers: Iterable) => [...numbers].sort().join(",") - const hasIdenticalLinks = (a: Reroute, b: Reroute) => - sortAndJoin(a.linkIds) === sortAndJoin(b.linkIds) && - sortAndJoin(a.floatingLinkIds) === sortAndJoin(b.floatingLinkIds) - - // Sanity check shouldBeRemoved - const reroutesWithIdenticalLinkIds = originalParentChain.filter(parent => hasIdenticalLinks(parent, toReroute)) - expect(reroutesWithIdenticalLinkIds.map(reroute => reroute.id)).toEqual(shouldBeRemoved) - - connector.dragFromReroute(graph, fromReroute) - - const dropEvent = { canvasX: toReroute.pos[0], canvasY: toReroute.pos[1] } as any - connector.dropLinks(graph, dropEvent) - connector.reset() - - const newParentChain = LLink.getReroutes(graph, toReroute) - for (const rerouteId of shouldBeRemoved) { - expect(originalParentChain.map(reroute => reroute.id)).toContain(rerouteId) - expect(newParentChain.map(reroute => reroute.id)).not.toContain(rerouteId) - } - - expect([...toReroute.linkIds.values()]).toEqual(nextLinkIds) - - for (const rerouteId of shouldBeRemoved) { - const reroute = graph.reroutes.get(rerouteId)! + ])( + 'Should allow connecting from reroutes to another reroute', + ( + { + fromRerouteId, + toRerouteId, + shouldBeRemoved, + shouldHaveLinkIdsRemoved, + testFloatingInputs, + expectedExtraLinks + }, + { graph, connector, getNextLinkIds } + ) => { if (testFloatingInputs) { - // Already-floating reroutes should be removed - expect(reroute).toBeUndefined() - } else { - // Non-floating reroutes should still exist - expect(reroute).not.toBeUndefined() + // Start by disconnecting the output of the 3x3 array of reroutes + graph.getNodeById(4)!.disconnectOutput(0) + } + + const fromReroute = graph.reroutes.get(fromRerouteId)! + const toReroute = graph.reroutes.get(toRerouteId)! + const nextLinkIds = getNextLinkIds(toReroute.linkIds, expectedExtraLinks) + + const originalParentChain = LLink.getReroutes(graph, toReroute) + + const sortAndJoin = (numbers: Iterable) => + [...numbers].sort().join(',') + const hasIdenticalLinks = (a: Reroute, b: Reroute) => + sortAndJoin(a.linkIds) === sortAndJoin(b.linkIds) && + sortAndJoin(a.floatingLinkIds) === sortAndJoin(b.floatingLinkIds) + + // Sanity check shouldBeRemoved + const reroutesWithIdenticalLinkIds = originalParentChain.filter( + (parent) => hasIdenticalLinks(parent, toReroute) + ) + expect(reroutesWithIdenticalLinkIds.map((reroute) => reroute.id)).toEqual( + shouldBeRemoved + ) + + connector.dragFromReroute(graph, fromReroute) + + const dropEvent = { + canvasX: toReroute.pos[0], + canvasY: toReroute.pos[1] + } as any + connector.dropLinks(graph, dropEvent) + connector.reset() + + const newParentChain = LLink.getReroutes(graph, toReroute) + for (const rerouteId of shouldBeRemoved) { + expect(originalParentChain.map((reroute) => reroute.id)).toContain( + rerouteId + ) + expect(newParentChain.map((reroute) => reroute.id)).not.toContain( + rerouteId + ) + } + + expect([...toReroute.linkIds.values()]).toEqual(nextLinkIds) + + for (const rerouteId of shouldBeRemoved) { + const reroute = graph.reroutes.get(rerouteId)! + if (testFloatingInputs) { + // Already-floating reroutes should be removed + expect(reroute).toBeUndefined() + } else { + // Non-floating reroutes should still exist + expect(reroute).not.toBeUndefined() + } + } + + for (const rerouteId of shouldHaveLinkIdsRemoved) { + const reroute = graph.reroutes.get(rerouteId)! + for (const linkId of toReroute.linkIds) { + expect(reroute.linkIds).not.toContain(linkId) + } + } + + // Validate all links in a reroute share the same origin + for (const reroute of graph.reroutes.values()) { + for (const linkId of reroute.linkIds) { + const link = graph.links.get(linkId) + expect(link?.origin_id).toEqual(reroute.origin_id) + expect(link?.origin_slot).toEqual(reroute.origin_slot) + } + for (const linkId of reroute.floatingLinkIds) { + if (reroute.origin_id === undefined) continue + + const link = graph.floatingLinks.get(linkId) + expect(link?.origin_id).toEqual(reroute.origin_id) + expect(link?.origin_slot).toEqual(reroute.origin_slot) + } } } - - for (const rerouteId of shouldHaveLinkIdsRemoved) { - const reroute = graph.reroutes.get(rerouteId)! - for (const linkId of toReroute.linkIds) { - expect(reroute.linkIds).not.toContain(linkId) - } - } - - // Validate all links in a reroute share the same origin - for (const reroute of graph.reroutes.values()) { - for (const linkId of reroute.linkIds) { - const link = graph.links.get(linkId) - expect(link?.origin_id).toEqual(reroute.origin_id) - expect(link?.origin_slot).toEqual(reroute.origin_slot) - } - for (const linkId of reroute.floatingLinkIds) { - if (reroute.origin_id === undefined) continue - - const link = graph.floatingLinks.get(linkId) - expect(link?.origin_id).toEqual(reroute.origin_id) - expect(link?.origin_slot).toEqual(reroute.origin_slot) - } - } - }) + ) test.for([ { from: 8, to: 13 }, @@ -959,26 +1149,29 @@ describe("LinkConnector Integration", () => { { from: 14, to: 10 }, { from: 15, to: 10 }, { from: 14, to: 13 }, - { from: 10, to: 10 }, - ])("Connecting reroutes to invalid targets should do nothing", ( - { from, to }, - { graph, connector, validateIntegrityNoChanges }, - ) => { - const listener = vi.fn() - connector.listenUntilReset("link-created", listener) + { from: 10, to: 10 } + ])( + 'Connecting reroutes to invalid targets should do nothing', + ({ from, to }, { graph, connector, validateIntegrityNoChanges }) => { + const listener = vi.fn() + connector.listenUntilReset('link-created', listener) - const fromReroute = graph.reroutes.get(from)! - const toReroute = graph.reroutes.get(to)! + const fromReroute = graph.reroutes.get(from)! + const toReroute = graph.reroutes.get(to)! - const dropEvent = { canvasX: toReroute.pos[0], canvasY: toReroute.pos[1] } as any + const dropEvent = { + canvasX: toReroute.pos[0], + canvasY: toReroute.pos[1] + } as any - connector.dragFromReroute(graph, fromReroute) - connector.dropLinks(graph, dropEvent) - connector.reset() + connector.dragFromReroute(graph, fromReroute) + connector.dropLinks(graph, dropEvent) + connector.reset() - expect(listener).not.toHaveBeenCalled() - validateIntegrityNoChanges() - }) + expect(listener).not.toHaveBeenCalled() + validateIntegrityNoChanges() + } + ) const nodeReroutePairs = [ { nodeId: 1, rerouteId: 1 }, @@ -989,79 +1182,94 @@ describe("LinkConnector Integration", () => { { nodeId: 4, rerouteId: 6 }, { nodeId: 4, rerouteId: 8 }, { nodeId: 4, rerouteId: 10 }, - { nodeId: 4, rerouteId: 12 }, + { nodeId: 4, rerouteId: 12 } ] - test.for(nodeReroutePairs)("Should ignore connections from input to same node via reroutes", ( - { nodeId, rerouteId }, - { graph, connector, validateIntegrityNoChanges }, - ) => { - const listener = vi.fn() - connector.listenUntilReset("link-created", listener) + test.for(nodeReroutePairs)( + 'Should ignore connections from input to same node via reroutes', + ( + { nodeId, rerouteId }, + { graph, connector, validateIntegrityNoChanges } + ) => { + const listener = vi.fn() + connector.listenUntilReset('link-created', listener) - const node = graph.getNodeById(nodeId)! - const input = node.inputs[0] - const reroute = graph.getReroute(rerouteId)! - const dropEvent = { canvasX: reroute.pos[0], canvasY: reroute.pos[1] } as any + const node = graph.getNodeById(nodeId)! + const input = node.inputs[0] + const reroute = graph.getReroute(rerouteId)! + const dropEvent = { + canvasX: reroute.pos[0], + canvasY: reroute.pos[1] + } as any - connector.dragNewFromInput(graph, node, input) - connector.dropLinks(graph, dropEvent) - connector.reset() + connector.dragNewFromInput(graph, node, input) + connector.dropLinks(graph, dropEvent) + connector.reset() - expect(listener).not.toHaveBeenCalled() - validateIntegrityNoChanges() + expect(listener).not.toHaveBeenCalled() + validateIntegrityNoChanges() - // No links should have the same origin_id and target_id - for (const link of graph.links.values()) { - expect(link.origin_id).not.toEqual(link.target_id) + // No links should have the same origin_id and target_id + for (const link of graph.links.values()) { + expect(link.origin_id).not.toEqual(link.target_id) + } } - }) + ) - test.for(nodeReroutePairs)("Should ignore connections looping back to the origin node from a reroute", ( - { nodeId, rerouteId }, - { graph, connector, validateIntegrityNoChanges }, - ) => { - const listener = vi.fn() - connector.listenUntilReset("link-created", listener) + test.for(nodeReroutePairs)( + 'Should ignore connections looping back to the origin node from a reroute', + ( + { nodeId, rerouteId }, + { graph, connector, validateIntegrityNoChanges } + ) => { + const listener = vi.fn() + connector.listenUntilReset('link-created', listener) - const node = graph.getNodeById(nodeId)! - const reroute = graph.getReroute(rerouteId)! - const dropEvent = { canvasX: node.pos[0], canvasY: node.pos[1] } as any + const node = graph.getNodeById(nodeId)! + const reroute = graph.getReroute(rerouteId)! + const dropEvent = { canvasX: node.pos[0], canvasY: node.pos[1] } as any - connector.dragFromReroute(graph, reroute) - connector.dropLinks(graph, dropEvent) - connector.reset() + connector.dragFromReroute(graph, reroute) + connector.dropLinks(graph, dropEvent) + connector.reset() - expect(listener).not.toHaveBeenCalled() - validateIntegrityNoChanges() + expect(listener).not.toHaveBeenCalled() + validateIntegrityNoChanges() - // No links should have the same origin_id and target_id - for (const link of graph.links.values()) { - expect(link.origin_id).not.toEqual(link.target_id) + // No links should have the same origin_id and target_id + for (const link of graph.links.values()) { + expect(link.origin_id).not.toEqual(link.target_id) + } } - }) + ) - test.for(nodeReroutePairs)("Should ignore connections looping back to the origin node input from a reroute", ( - { nodeId, rerouteId }, - { graph, connector, validateIntegrityNoChanges }, - ) => { - const listener = vi.fn() - connector.listenUntilReset("link-created", listener) + test.for(nodeReroutePairs)( + 'Should ignore connections looping back to the origin node input from a reroute', + ( + { nodeId, rerouteId }, + { graph, connector, validateIntegrityNoChanges } + ) => { + const listener = vi.fn() + connector.listenUntilReset('link-created', listener) - const node = graph.getNodeById(nodeId)! - const reroute = graph.getReroute(rerouteId)! - const inputPos = node.getInputPos(0) - const dropOnInputEvent = { canvasX: inputPos[0], canvasY: inputPos[1] } as any + const node = graph.getNodeById(nodeId)! + const reroute = graph.getReroute(rerouteId)! + const inputPos = node.getInputPos(0) + const dropOnInputEvent = { + canvasX: inputPos[0], + canvasY: inputPos[1] + } as any - connector.dragFromReroute(graph, reroute) - connector.dropLinks(graph, dropOnInputEvent) - connector.reset() + connector.dragFromReroute(graph, reroute) + connector.dropLinks(graph, dropOnInputEvent) + connector.reset() - expect(listener).not.toHaveBeenCalled() - validateIntegrityNoChanges() + expect(listener).not.toHaveBeenCalled() + validateIntegrityNoChanges() - // No links should have the same origin_id and target_id - for (const link of graph.links.values()) { - expect(link.origin_id).not.toEqual(link.target_id) + // No links should have the same origin_id and target_id + for (const link of graph.links.values()) { + expect(link.origin_id).not.toEqual(link.target_id) + } } - }) + ) }) diff --git a/src/lib/litegraph/test/LinkConnector.test.ts b/src/lib/litegraph/test/LinkConnector.test.ts index c7898e7550..59ea1dc1c9 100644 --- a/src/lib/litegraph/test/LinkConnector.test.ts +++ b/src/lib/litegraph/test/LinkConnector.test.ts @@ -1,20 +1,30 @@ -import type { MovingInputLink } from "@/lib/litegraph/src/canvas/MovingInputLink" -import type { LinkNetwork } from "@/lib/litegraph/src/interfaces" -import type { ISlotType } from "@/lib/litegraph/src/interfaces" +import { test as baseTest, describe, expect, vi } from 'vitest' -import { describe, expect, test as baseTest, vi } from "vitest" - -import { LinkConnector } from "@/lib/litegraph/src/canvas/LinkConnector" -import { ToInputRenderLink } from "@/lib/litegraph/src/canvas/ToInputRenderLink" -import { LGraph, LGraphNode, LLink, Reroute, type RerouteId } from "@/lib/litegraph/src/litegraph" -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" +import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector' +import type { MovingInputLink } from '@/lib/litegraph/src/canvas/MovingInputLink' +import { ToInputRenderLink } from '@/lib/litegraph/src/canvas/ToInputRenderLink' +import type { LinkNetwork } from '@/lib/litegraph/src/interfaces' +import type { ISlotType } from '@/lib/litegraph/src/interfaces' +import { + LGraph, + LGraphNode, + LLink, + Reroute, + type RerouteId +} from '@/lib/litegraph/src/litegraph' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' interface TestContext { network: LinkNetwork & { add(node: LGraphNode): void } connector: LinkConnector setConnectingLinks: ReturnType createTestNode: (id: number, slotType?: ISlotType) => LGraphNode - createTestLink: (id: number, sourceId: number, targetId: number, slotType?: ISlotType) => LLink + createTestLink: ( + id: number, + sourceId: number, + targetId: number, + slotType?: ISlotType + ) => LLink } const test = baseTest.extend({ @@ -34,13 +44,17 @@ const test = baseTest.extend({ return link }, removeFloatingLink: (link: LLink) => floatingLinks.delete(link.id), - getReroute: ((id: RerouteId | null | undefined) => id == null ? undefined : reroutes.get(id)) as LinkNetwork["getReroute"], + getReroute: ((id: RerouteId | null | undefined) => + id == null ? undefined : reroutes.get(id)) as LinkNetwork['getReroute'], removeReroute: (id: number) => reroutes.delete(id), - add: (node: LGraphNode) => graph.add(node), + add: (node: LGraphNode) => graph.add(node) }) }, - setConnectingLinks: async ({}, use: (mock: ReturnType) => Promise) => { + setConnectingLinks: async ( + {}, + use: (mock: ReturnType) => Promise + ) => { const mock = vi.fn() await use(mock) }, @@ -51,32 +65,34 @@ const test = baseTest.extend({ createTestNode: async ({ network }, use) => { await use((id: number): LGraphNode => { - const node = new LGraphNode("test") + const node = new LGraphNode('test') node.id = id network.add(node) return node }) }, createTestLink: async ({ network }, use) => { - await use(( - id: number, - sourceId: number, - targetId: number, - slotType: ISlotType = "number", - ): LLink => { - const link = new LLink(id, slotType, sourceId, 0, targetId, 0) - network.links.set(link.id, link) - return link - }) - }, + await use( + ( + id: number, + sourceId: number, + targetId: number, + slotType: ISlotType = 'number' + ): LLink => { + const link = new LLink(id, slotType, sourceId, 0, targetId, 0) + network.links.set(link.id, link) + return link + } + ) + } }) -describe("LinkConnector", () => { - test("should initialize with default state", ({ connector }) => { +describe('LinkConnector', () => { + test('should initialize with default state', ({ connector }) => { expect(connector.state).toEqual({ connectingTo: undefined, multi: false, - draggingExistingLinks: false, + draggingExistingLinks: false }) expect(connector.renderLinks).toEqual([]) expect(connector.inputLinks).toEqual([]) @@ -84,14 +100,18 @@ describe("LinkConnector", () => { expect(connector.hiddenReroutes.size).toBe(0) }) - describe("Moving Input Links", () => { - test("should handle moving input links", ({ network, connector, createTestNode }) => { + describe('Moving Input Links', () => { + test('should handle moving input links', ({ + network, + connector, + createTestNode + }) => { const sourceNode = createTestNode(1) const targetNode = createTestNode(2) - const slotType: ISlotType = "number" - sourceNode.addOutput("out", slotType) - targetNode.addInput("in", slotType) + const slotType: ISlotType = 'number' + sourceNode.addOutput('out', slotType) + targetNode.addInput('in', slotType) const link = new LLink(1, slotType, 1, 0, 2, 0) network.links.set(link.id, link) @@ -99,29 +119,36 @@ describe("LinkConnector", () => { connector.moveInputLink(network, targetNode.inputs[0]) - expect(connector.state.connectingTo).toBe("input") + expect(connector.state.connectingTo).toBe('input') expect(connector.state.draggingExistingLinks).toBe(true) expect(connector.inputLinks).toContain(link) expect(link._dragging).toBe(true) }) - test("should not move input link if already connecting", ({ connector, network }) => { - connector.state.connectingTo = "input" + test('should not move input link if already connecting', ({ + connector, + network + }) => { + connector.state.connectingTo = 'input' expect(() => { connector.moveInputLink(network, { link: 1 } as any) - }).toThrow("Already dragging links.") + }).toThrow('Already dragging links.') }) }) - describe("Moving Output Links", () => { - test("should handle moving output links", ({ network, connector, createTestNode }) => { + describe('Moving Output Links', () => { + test('should handle moving output links', ({ + network, + connector, + createTestNode + }) => { const sourceNode = createTestNode(1) const targetNode = createTestNode(2) - const slotType: ISlotType = "number" - sourceNode.addOutput("out", slotType) - targetNode.addInput("in", slotType) + const slotType: ISlotType = 'number' + sourceNode.addOutput('out', slotType) + targetNode.addInput('in', slotType) const link = new LLink(1, slotType, 1, 0, 2, 0) network.links.set(link.id, link) @@ -129,55 +156,71 @@ describe("LinkConnector", () => { connector.moveOutputLink(network, sourceNode.outputs[0]) - expect(connector.state.connectingTo).toBe("output") + expect(connector.state.connectingTo).toBe('output') expect(connector.state.draggingExistingLinks).toBe(true) expect(connector.state.multi).toBe(true) expect(connector.outputLinks).toContain(link) expect(link._dragging).toBe(true) }) - test("should not move output link if already connecting", ({ connector, network }) => { - connector.state.connectingTo = "output" + test('should not move output link if already connecting', ({ + connector, + network + }) => { + connector.state.connectingTo = 'output' expect(() => { connector.moveOutputLink(network, { links: [1] } as any) - }).toThrow("Already dragging links.") + }).toThrow('Already dragging links.') }) }) - describe("Dragging New Links", () => { - test("should handle dragging new link from output", ({ network, connector, createTestNode }) => { + describe('Dragging New Links', () => { + test('should handle dragging new link from output', ({ + network, + connector, + createTestNode + }) => { const sourceNode = createTestNode(1) - const slotType: ISlotType = "number" - sourceNode.addOutput("out", slotType) + const slotType: ISlotType = 'number' + sourceNode.addOutput('out', slotType) connector.dragNewFromOutput(network, sourceNode, sourceNode.outputs[0]) - expect(connector.state.connectingTo).toBe("input") + expect(connector.state.connectingTo).toBe('input') expect(connector.renderLinks.length).toBe(1) expect(connector.state.draggingExistingLinks).toBe(false) }) - test("should handle dragging new link from input", ({ network, connector, createTestNode }) => { + test('should handle dragging new link from input', ({ + network, + connector, + createTestNode + }) => { const targetNode = createTestNode(1) - const slotType: ISlotType = "number" - targetNode.addInput("in", slotType) + const slotType: ISlotType = 'number' + targetNode.addInput('in', slotType) connector.dragNewFromInput(network, targetNode, targetNode.inputs[0]) - expect(connector.state.connectingTo).toBe("output") + expect(connector.state.connectingTo).toBe('output') expect(connector.renderLinks.length).toBe(1) expect(connector.state.draggingExistingLinks).toBe(false) }) }) - describe("Dragging from reroutes", () => { - test("should handle dragging from reroutes", ({ network, connector, createTestNode, createTestLink }) => { + describe('Dragging from reroutes', () => { + test('should handle dragging from reroutes', ({ + network, + connector, + createTestNode, + createTestLink + }) => { const originNode = createTestNode(1) const targetNode = createTestNode(2) - const output = originNode.addOutput("out", "number") - targetNode.addInput("in", "number") + const output = originNode.addOutput('out', 'number') + targetNode.addInput('in', 'number') const link = createTestLink(1, 1, 2) const reroute = new Reroute(1, network, [0, 0], undefined, [link.id]) @@ -186,13 +229,13 @@ describe("LinkConnector", () => { connector.dragFromReroute(network, reroute) - expect(connector.state.connectingTo).toBe("input") + expect(connector.state.connectingTo).toBe('input') expect(connector.state.draggingExistingLinks).toBe(false) expect(connector.renderLinks.length).toBe(1) const renderLink = connector.renderLinks[0] expect(renderLink instanceof ToInputRenderLink).toBe(true) - expect(renderLink.toType).toEqual("input") + expect(renderLink.toType).toEqual('input') expect(renderLink.node).toEqual(originNode) expect(renderLink.fromSlot).toEqual(output) expect(renderLink.fromReroute).toEqual(reroute) @@ -201,13 +244,13 @@ describe("LinkConnector", () => { }) }) - describe("Reset", () => { - test("should reset state and clear links", ({ network, connector }) => { - connector.state.connectingTo = "input" + describe('Reset', () => { + test('should reset state and clear links', ({ network, connector }) => { + connector.state.connectingTo = 'input' connector.state.multi = true connector.state.draggingExistingLinks = true - const link = new LLink(1, "number", 1, 0, 2, 0) + const link = new LLink(1, 'number', 1, 0, 2, 0) link._dragging = true connector.inputLinks.push(link) @@ -221,7 +264,7 @@ describe("LinkConnector", () => { expect(connector.state).toEqual({ connectingTo: undefined, multi: false, - draggingExistingLinks: false, + draggingExistingLinks: false }) expect(connector.renderLinks).toEqual([]) expect(connector.inputLinks).toEqual([]) @@ -232,37 +275,40 @@ describe("LinkConnector", () => { }) }) - describe("Event Handling", () => { - test("should handle event listeners until reset", ({ connector, createTestNode }) => { + describe('Event Handling', () => { + test('should handle event listeners until reset', ({ + connector, + createTestNode + }) => { const listener = vi.fn() - connector.listenUntilReset("input-moved", listener) + connector.listenUntilReset('input-moved', listener) const sourceNode = createTestNode(1) const mockRenderLink = { node: sourceNode, - fromSlot: { name: "out", type: "number" }, + fromSlot: { name: 'out', type: 'number' }, fromPos: [0, 0], fromDirection: LinkDirection.RIGHT, - toType: "input", - link: new LLink(1, "number", 1, 0, 2, 0), + toType: 'input', + link: new LLink(1, 'number', 1, 0, 2, 0) } as MovingInputLink - connector.events.dispatch("input-moved", mockRenderLink) + connector.events.dispatch('input-moved', mockRenderLink) expect(listener).toHaveBeenCalled() connector.reset() - connector.events.dispatch("input-moved", mockRenderLink) + connector.events.dispatch('input-moved', mockRenderLink) expect(listener).toHaveBeenCalledTimes(1) }) }) - describe("Export", () => { - test("should export current state", ({ network, connector }) => { - connector.state.connectingTo = "input" + describe('Export', () => { + test('should export current state', ({ network, connector }) => { + connector.state.connectingTo = 'input' connector.state.multi = true - const link = new LLink(1, "number", 1, 0, 2, 0) + const link = new LLink(1, 'number', 1, 0, 2, 0) connector.inputLinks.push(link) const exported = connector.export(network) diff --git a/src/lib/litegraph/test/NodeSlot.test.ts b/src/lib/litegraph/test/NodeSlot.test.ts index 946415321a..63d272d59b 100644 --- a/src/lib/litegraph/test/NodeSlot.test.ts +++ b/src/lib/litegraph/test/NodeSlot.test.ts @@ -1,72 +1,75 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest' -import { INodeInputSlot, INodeOutputSlot } from "@/lib/litegraph/src/interfaces" -import { inputAsSerialisable, outputAsSerialisable } from "@/lib/litegraph/src/node/slotUtils" +import { INodeInputSlot, INodeOutputSlot } from '@/lib/litegraph/src/interfaces' +import { + inputAsSerialisable, + outputAsSerialisable +} from '@/lib/litegraph/src/node/slotUtils' -describe("NodeSlot", () => { - describe("inputAsSerialisable", () => { - it("removes _data from serialized slot", () => { +describe('NodeSlot', () => { + describe('inputAsSerialisable', () => { + it('removes _data from serialized slot', () => { const slot: INodeOutputSlot = { - _data: "test data", - name: "test-id", - type: "STRING", - links: [], + _data: 'test data', + name: 'test-id', + type: 'STRING', + links: [] } const serialized = outputAsSerialisable(slot) - expect(serialized).not.toHaveProperty("_data") + expect(serialized).not.toHaveProperty('_data') }) - it("removes pos from widget input slots", () => { + it('removes pos from widget input slots', () => { const widgetInputSlot: INodeInputSlot = { - name: "test-id", + name: 'test-id', pos: [10, 20], - type: "STRING", + type: 'STRING', link: null, widget: { - name: "test-widget", - type: "combo", - value: "test-value-1", + name: 'test-widget', + type: 'combo', + value: 'test-value-1', options: { - values: ["test-value-1", "test-value-2"], - }, - }, + values: ['test-value-1', 'test-value-2'] + } + } } const serialized = inputAsSerialisable(widgetInputSlot) - expect(serialized).not.toHaveProperty("pos") + expect(serialized).not.toHaveProperty('pos') }) - it("preserves pos for non-widget input slots", () => { + it('preserves pos for non-widget input slots', () => { const normalSlot: INodeInputSlot = { - name: "test-id", - type: "STRING", + name: 'test-id', + type: 'STRING', pos: [10, 20], - link: null, + link: null } const serialized = inputAsSerialisable(normalSlot) - expect(serialized).toHaveProperty("pos") + expect(serialized).toHaveProperty('pos') }) - it("preserves only widget name during serialization", () => { + it('preserves only widget name during serialization', () => { const widgetInputSlot: INodeInputSlot = { - name: "test-id", - type: "STRING", + name: 'test-id', + type: 'STRING', link: null, widget: { - name: "test-widget", - type: "combo", - value: "test-value-1", + name: 'test-widget', + type: 'combo', + value: 'test-value-1', options: { - values: ["test-value-1", "test-value-2"], - }, - }, + values: ['test-value-1', 'test-value-2'] + } + } } const serialized = inputAsSerialisable(widgetInputSlot) - expect(serialized.widget).toEqual({ name: "test-widget" }) - expect(serialized.widget).not.toHaveProperty("type") - expect(serialized.widget).not.toHaveProperty("value") - expect(serialized.widget).not.toHaveProperty("options") + expect(serialized.widget).toEqual({ name: 'test-widget' }) + expect(serialized.widget).not.toHaveProperty('type') + expect(serialized.widget).not.toHaveProperty('value') + expect(serialized.widget).not.toHaveProperty('options') }) }) }) diff --git a/src/lib/litegraph/test/ToOutputRenderLink.test.ts b/src/lib/litegraph/test/ToOutputRenderLink.test.ts index 347e7646ff..cffe764b4e 100644 --- a/src/lib/litegraph/test/ToOutputRenderLink.test.ts +++ b/src/lib/litegraph/test/ToOutputRenderLink.test.ts @@ -1,41 +1,45 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from 'vitest' -import { ToOutputRenderLink } from "@/lib/litegraph/src/canvas/ToOutputRenderLink" -import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums" +import { ToOutputRenderLink } from '@/lib/litegraph/src/canvas/ToOutputRenderLink' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' -describe("ToOutputRenderLink", () => { - describe("connectToOutput", () => { - it("should return early if inputNode is null", () => { +describe('ToOutputRenderLink', () => { + describe('connectToOutput', () => { + it('should return early if inputNode is null', () => { // Setup const mockNetwork = {} const mockFromSlot = {} - const mockNode = { id: "test-id", inputs: [mockFromSlot], getInputPos: vi.fn().mockReturnValue([0, 0]) } + const mockNode = { + id: 'test-id', + inputs: [mockFromSlot], + getInputPos: vi.fn().mockReturnValue([0, 0]) + } const renderLink = new ToOutputRenderLink( mockNetwork as any, mockNode as any, mockFromSlot as any, undefined, - LinkDirection.CENTER, + LinkDirection.CENTER ) // Override the node property to simulate null case - Object.defineProperty(renderLink, "node", { - value: null, + Object.defineProperty(renderLink, 'node', { + value: null }) const mockTargetNode = { - connectSlots: vi.fn(), + connectSlots: vi.fn() } const mockEvents = { - dispatch: vi.fn(), + dispatch: vi.fn() } // Act renderLink.connectToOutput( mockTargetNode as any, {} as any, - mockEvents as any, + mockEvents as any ) // Assert @@ -43,14 +47,14 @@ describe("ToOutputRenderLink", () => { expect(mockEvents.dispatch).not.toHaveBeenCalled() }) - it("should create connection and dispatch event when inputNode exists", () => { + it('should create connection and dispatch event when inputNode exists', () => { // Setup const mockNetwork = {} const mockFromSlot = {} const mockNode = { - id: "test-id", + id: 'test-id', inputs: [mockFromSlot], - getInputPos: vi.fn().mockReturnValue([0, 0]), + getInputPos: vi.fn().mockReturnValue([0, 0]) } const renderLink = new ToOutputRenderLink( @@ -58,22 +62,22 @@ describe("ToOutputRenderLink", () => { mockNode as any, mockFromSlot as any, undefined, - LinkDirection.CENTER, + LinkDirection.CENTER ) - const mockNewLink = { id: "new-link" } + const mockNewLink = { id: 'new-link' } const mockTargetNode = { - connectSlots: vi.fn().mockReturnValue(mockNewLink), + connectSlots: vi.fn().mockReturnValue(mockNewLink) } const mockEvents = { - dispatch: vi.fn(), + dispatch: vi.fn() } // Act renderLink.connectToOutput( mockTargetNode as any, {} as any, - mockEvents as any, + mockEvents as any ) // Assert @@ -81,11 +85,11 @@ describe("ToOutputRenderLink", () => { expect.anything(), mockNode, mockFromSlot, - undefined, + undefined ) expect(mockEvents.dispatch).toHaveBeenCalledWith( - "link-created", - mockNewLink, + 'link-created', + mockNewLink ) }) }) diff --git a/src/lib/litegraph/test/assets/testGraphs.ts b/src/lib/litegraph/test/assets/testGraphs.ts index e9435b1598..ad00cb67fa 100644 --- a/src/lib/litegraph/test/assets/testGraphs.ts +++ b/src/lib/litegraph/test/assets/testGraphs.ts @@ -1,7 +1,10 @@ -import type { ISerialisedGraph, SerialisableGraph } from "@/lib/litegraph/src/litegraph" +import type { + ISerialisedGraph, + SerialisableGraph +} from '@/lib/litegraph/src/litegraph' export const oldSchemaGraph: ISerialisedGraph = { - id: "b4e984f1-b421-4d24-b8b4-ff895793af13", + id: 'b4e984f1-b421-4d24-b8b4-ff895793af13', revision: 0, version: 0.4, config: {}, @@ -11,21 +14,21 @@ export const oldSchemaGraph: ISerialisedGraph = { { id: 123, bounding: [20, 20, 1, 3], - color: "#6029aa", + color: '#6029aa', font_size: 14, - title: "A group to test with", - }, + title: 'A group to test with' + } ], nodes: [ { - id: 1, - }, + id: 1 + } ], - links: [], + links: [] } export const minimalSerialisableGraph: SerialisableGraph = { - id: "d175890f-716a-4ece-ba33-1d17a513b7be", + id: 'd175890f-716a-4ece-ba33-1d17a513b7be', revision: 0, version: 1, config: {}, @@ -33,15 +36,15 @@ export const minimalSerialisableGraph: SerialisableGraph = { lastNodeId: 0, lastLinkId: 0, lastGroupId: 0, - lastRerouteId: 0, + lastRerouteId: 0 }, nodes: [], links: [], - groups: [], + groups: [] } export const basicSerialisableGraph: SerialisableGraph = { - id: "ca9da7d8-fddd-4707-ad32-67be9be13140", + id: 'ca9da7d8-fddd-4707-ad32-67be9be13140', revision: 0, version: 1, config: {}, @@ -49,22 +52,22 @@ export const basicSerialisableGraph: SerialisableGraph = { lastNodeId: 0, lastLinkId: 0, lastGroupId: 0, - lastRerouteId: 0, + lastRerouteId: 0 }, groups: [ { id: 123, bounding: [20, 20, 1, 3], - color: "#6029aa", + color: '#6029aa', font_size: 14, - title: "A group to test with", - }, + title: 'A group to test with' + } ], nodes: [ { id: 1, - type: "mustBeSet", - }, + type: 'mustBeSet' + } ], - links: [], + links: [] } diff --git a/src/lib/litegraph/test/canvas/LinkConnector.test.ts b/src/lib/litegraph/test/canvas/LinkConnector.test.ts index 09416e6699..e5e0e84096 100644 --- a/src/lib/litegraph/test/canvas/LinkConnector.test.ts +++ b/src/lib/litegraph/test/canvas/LinkConnector.test.ts @@ -1,9 +1,8 @@ -import type { INodeInputSlot, LGraphNode } from "@/lib/litegraph/src/litegraph" - -import { beforeEach, describe, expect, test, vi } from "vitest" +import { beforeEach, describe, expect, test, vi } from 'vitest' +import type { INodeInputSlot, LGraphNode } from '@/lib/litegraph/src/litegraph' // We don't strictly need RenderLink interface import for the mock -import { LinkConnector } from "@/lib/litegraph/src/litegraph" +import { LinkConnector } from '@/lib/litegraph/src/litegraph' // Mocks const mockSetConnectingLinks = vi.fn() @@ -11,15 +10,15 @@ const mockSetConnectingLinks = vi.fn() // Mock a structure that has the needed method function mockRenderLinkImpl(canConnect: boolean) { return { - canConnectToInput: vi.fn().mockReturnValue(canConnect), - // Add other properties if they become necessary for tests + canConnectToInput: vi.fn().mockReturnValue(canConnect) + // Add other properties if they become necessary for tests } } const mockNode = {} as LGraphNode const mockInput = {} as INodeInputSlot -describe("LinkConnector", () => { +describe('LinkConnector', () => { let connector: LinkConnector beforeEach(() => { @@ -29,12 +28,12 @@ describe("LinkConnector", () => { vi.clearAllMocks() }) - describe("isInputValidDrop", () => { - test("should return false if there are no render links", () => { + describe('isInputValidDrop', () => { + test('should return false if there are no render links', () => { expect(connector.isInputValidDrop(mockNode, mockInput)).toBe(false) }) - test("should return true if at least one render link can connect", () => { + test('should return true if at least one render link can connect', () => { const link1 = mockRenderLinkImpl(false) const link2 = mockRenderLinkImpl(true) // Cast to any to satisfy the push requirement, as we only need the canConnectToInput method @@ -44,7 +43,7 @@ describe("LinkConnector", () => { expect(link2.canConnectToInput).toHaveBeenCalledWith(mockNode, mockInput) }) - test("should return false if no render links can connect", () => { + test('should return false if no render links can connect', () => { const link1 = mockRenderLinkImpl(false) const link2 = mockRenderLinkImpl(false) connector.renderLinks.push(link1 as any, link2 as any) @@ -53,7 +52,7 @@ describe("LinkConnector", () => { expect(link2.canConnectToInput).toHaveBeenCalledWith(mockNode, mockInput) }) - test("should call canConnectToInput on each render link until one returns true", () => { + test('should call canConnectToInput on each render link until one returns true', () => { const link1 = mockRenderLinkImpl(false) const link2 = mockRenderLinkImpl(true) // This one can connect const link3 = mockRenderLinkImpl(false) @@ -67,65 +66,89 @@ describe("LinkConnector", () => { }) }) - describe("listenUntilReset", () => { - test("should add listener for the specified event and for reset", () => { + describe('listenUntilReset', () => { + test('should add listener for the specified event and for reset', () => { const listener = vi.fn() - const addEventListenerSpy = vi.spyOn(connector.events, "addEventListener") + const addEventListenerSpy = vi.spyOn(connector.events, 'addEventListener') - connector.listenUntilReset("before-drop-links", listener) + connector.listenUntilReset('before-drop-links', listener) - expect(addEventListenerSpy).toHaveBeenCalledWith("before-drop-links", listener, undefined) - expect(addEventListenerSpy).toHaveBeenCalledWith("reset", expect.any(Function), { once: true }) + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'before-drop-links', + listener, + undefined + ) + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'reset', + expect.any(Function), + { once: true } + ) }) - test("should call the listener when the event is dispatched before reset", () => { + test('should call the listener when the event is dispatched before reset', () => { const listener = vi.fn() const eventData = { renderLinks: [], event: {} as any } // Mock event data - connector.listenUntilReset("before-drop-links", listener) + connector.listenUntilReset('before-drop-links', listener) - connector.events.dispatch("before-drop-links", eventData) + connector.events.dispatch('before-drop-links', eventData) expect(listener).toHaveBeenCalledTimes(1) - expect(listener).toHaveBeenCalledWith(new CustomEvent("before-drop-links")) + expect(listener).toHaveBeenCalledWith( + new CustomEvent('before-drop-links') + ) }) - test("should remove the listener when reset is dispatched", () => { + test('should remove the listener when reset is dispatched', () => { const listener = vi.fn() - const removeEventListenerSpy = vi.spyOn(connector.events, "removeEventListener") + const removeEventListenerSpy = vi.spyOn( + connector.events, + 'removeEventListener' + ) - connector.listenUntilReset("before-drop-links", listener) + connector.listenUntilReset('before-drop-links', listener) // Simulate the reset event being dispatched - connector.events.dispatch("reset", false) + connector.events.dispatch('reset', false) // Check if removeEventListener was called correctly for the original listener - expect(removeEventListenerSpy).toHaveBeenCalledWith("before-drop-links", listener) + expect(removeEventListenerSpy).toHaveBeenCalledWith( + 'before-drop-links', + listener + ) }) - test("should not call the listener after reset is dispatched", () => { + test('should not call the listener after reset is dispatched', () => { const listener = vi.fn() const eventData = { renderLinks: [], event: {} as any } - connector.listenUntilReset("before-drop-links", listener) + connector.listenUntilReset('before-drop-links', listener) // Dispatch reset first - connector.events.dispatch("reset", false) + connector.events.dispatch('reset', false) // Then dispatch the original event - connector.events.dispatch("before-drop-links", eventData) + connector.events.dispatch('before-drop-links', eventData) expect(listener).not.toHaveBeenCalled() }) - test("should pass options to addEventListener", () => { + test('should pass options to addEventListener', () => { const listener = vi.fn() const options = { once: true } - const addEventListenerSpy = vi.spyOn(connector.events, "addEventListener") + const addEventListenerSpy = vi.spyOn(connector.events, 'addEventListener') - connector.listenUntilReset("after-drop-links", listener, options) + connector.listenUntilReset('after-drop-links', listener, options) - expect(addEventListenerSpy).toHaveBeenCalledWith("after-drop-links", listener, options) + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'after-drop-links', + listener, + options + ) // Still adds the reset listener - expect(addEventListenerSpy).toHaveBeenCalledWith("reset", expect.any(Function), { once: true }) + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'reset', + expect.any(Function), + { once: true } + ) }) }) }) diff --git a/src/lib/litegraph/test/infrastructure/Rectangle.resize.test.ts b/src/lib/litegraph/test/infrastructure/Rectangle.resize.test.ts index 877963db29..8b3fd1862f 100644 --- a/src/lib/litegraph/test/infrastructure/Rectangle.resize.test.ts +++ b/src/lib/litegraph/test/infrastructure/Rectangle.resize.test.ts @@ -1,8 +1,8 @@ -import { beforeEach, describe, expect, test } from "vitest" +import { beforeEach, describe, expect, test } from 'vitest' -import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" +import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' -describe("Rectangle resize functionality", () => { +describe('Rectangle resize functionality', () => { let rect: Rectangle beforeEach(() => { @@ -10,37 +10,37 @@ describe("Rectangle resize functionality", () => { // So: left=100, top=200, right=400, bottom=600 }) - describe("findContainingCorner", () => { + describe('findContainingCorner', () => { const cornerSize = 15 - test("should detect NW (top-left) corner", () => { - expect(rect.findContainingCorner(100, 200, cornerSize)).toBe("NW") - expect(rect.findContainingCorner(110, 210, cornerSize)).toBe("NW") - expect(rect.findContainingCorner(114, 214, cornerSize)).toBe("NW") + test('should detect NW (top-left) corner', () => { + expect(rect.findContainingCorner(100, 200, cornerSize)).toBe('NW') + expect(rect.findContainingCorner(110, 210, cornerSize)).toBe('NW') + expect(rect.findContainingCorner(114, 214, cornerSize)).toBe('NW') }) - test("should detect NE (top-right) corner", () => { + test('should detect NE (top-right) corner', () => { // Top-right corner starts at (right - cornerSize, top) = (385, 200) - expect(rect.findContainingCorner(385, 200, cornerSize)).toBe("NE") - expect(rect.findContainingCorner(390, 210, cornerSize)).toBe("NE") - expect(rect.findContainingCorner(399, 214, cornerSize)).toBe("NE") + expect(rect.findContainingCorner(385, 200, cornerSize)).toBe('NE') + expect(rect.findContainingCorner(390, 210, cornerSize)).toBe('NE') + expect(rect.findContainingCorner(399, 214, cornerSize)).toBe('NE') }) - test("should detect SW (bottom-left) corner", () => { + test('should detect SW (bottom-left) corner', () => { // Bottom-left corner starts at (left, bottom - cornerSize) = (100, 585) - expect(rect.findContainingCorner(100, 585, cornerSize)).toBe("SW") - expect(rect.findContainingCorner(110, 590, cornerSize)).toBe("SW") - expect(rect.findContainingCorner(114, 599, cornerSize)).toBe("SW") + expect(rect.findContainingCorner(100, 585, cornerSize)).toBe('SW') + expect(rect.findContainingCorner(110, 590, cornerSize)).toBe('SW') + expect(rect.findContainingCorner(114, 599, cornerSize)).toBe('SW') }) - test("should detect SE (bottom-right) corner", () => { + test('should detect SE (bottom-right) corner', () => { // Bottom-right corner starts at (right - cornerSize, bottom - cornerSize) = (385, 585) - expect(rect.findContainingCorner(385, 585, cornerSize)).toBe("SE") - expect(rect.findContainingCorner(390, 590, cornerSize)).toBe("SE") - expect(rect.findContainingCorner(399, 599, cornerSize)).toBe("SE") + expect(rect.findContainingCorner(385, 585, cornerSize)).toBe('SE') + expect(rect.findContainingCorner(390, 590, cornerSize)).toBe('SE') + expect(rect.findContainingCorner(399, 599, cornerSize)).toBe('SE') }) - test("should return undefined when not in any corner", () => { + test('should return undefined when not in any corner', () => { // Middle of rectangle expect(rect.findContainingCorner(250, 400, cornerSize)).toBeUndefined() // On edge but not in corner @@ -51,17 +51,17 @@ describe("Rectangle resize functionality", () => { }) }) - describe("corner detection methods", () => { + describe('corner detection methods', () => { const cornerSize = 20 - describe("isInTopLeftCorner", () => { - test("should return true when point is in top-left corner", () => { + describe('isInTopLeftCorner', () => { + test('should return true when point is in top-left corner', () => { expect(rect.isInTopLeftCorner(100, 200, cornerSize)).toBe(true) expect(rect.isInTopLeftCorner(110, 210, cornerSize)).toBe(true) expect(rect.isInTopLeftCorner(119, 219, cornerSize)).toBe(true) }) - test("should return false when point is outside top-left corner", () => { + test('should return false when point is outside top-left corner', () => { expect(rect.isInTopLeftCorner(120, 200, cornerSize)).toBe(false) expect(rect.isInTopLeftCorner(100, 220, cornerSize)).toBe(false) expect(rect.isInTopLeftCorner(99, 200, cornerSize)).toBe(false) @@ -69,8 +69,8 @@ describe("Rectangle resize functionality", () => { }) }) - describe("isInTopRightCorner", () => { - test("should return true when point is in top-right corner", () => { + describe('isInTopRightCorner', () => { + test('should return true when point is in top-right corner', () => { // Top-right corner area is from (right - cornerSize, top) to (right, top + cornerSize) // That's (380, 200) to (400, 220) expect(rect.isInTopRightCorner(380, 200, cornerSize)).toBe(true) @@ -78,7 +78,7 @@ describe("Rectangle resize functionality", () => { expect(rect.isInTopRightCorner(399, 219, cornerSize)).toBe(true) }) - test("should return false when point is outside top-right corner", () => { + test('should return false when point is outside top-right corner', () => { expect(rect.isInTopRightCorner(379, 200, cornerSize)).toBe(false) expect(rect.isInTopRightCorner(400, 220, cornerSize)).toBe(false) expect(rect.isInTopRightCorner(401, 200, cornerSize)).toBe(false) @@ -86,8 +86,8 @@ describe("Rectangle resize functionality", () => { }) }) - describe("isInBottomLeftCorner", () => { - test("should return true when point is in bottom-left corner", () => { + describe('isInBottomLeftCorner', () => { + test('should return true when point is in bottom-left corner', () => { // Bottom-left corner area is from (left, bottom - cornerSize) to (left + cornerSize, bottom) // That's (100, 580) to (120, 600) expect(rect.isInBottomLeftCorner(100, 580, cornerSize)).toBe(true) @@ -95,7 +95,7 @@ describe("Rectangle resize functionality", () => { expect(rect.isInBottomLeftCorner(119, 599, cornerSize)).toBe(true) }) - test("should return false when point is outside bottom-left corner", () => { + test('should return false when point is outside bottom-left corner', () => { expect(rect.isInBottomLeftCorner(120, 600, cornerSize)).toBe(false) expect(rect.isInBottomLeftCorner(100, 579, cornerSize)).toBe(false) expect(rect.isInBottomLeftCorner(99, 600, cornerSize)).toBe(false) @@ -103,8 +103,8 @@ describe("Rectangle resize functionality", () => { }) }) - describe("isInBottomRightCorner", () => { - test("should return true when point is in bottom-right corner", () => { + describe('isInBottomRightCorner', () => { + test('should return true when point is in bottom-right corner', () => { // Bottom-right corner area is from (right - cornerSize, bottom - cornerSize) to (right, bottom) // That's (380, 580) to (400, 600) expect(rect.isInBottomRightCorner(380, 580, cornerSize)).toBe(true) @@ -112,7 +112,7 @@ describe("Rectangle resize functionality", () => { expect(rect.isInBottomRightCorner(399, 599, cornerSize)).toBe(true) }) - test("should return false when point is outside bottom-right corner", () => { + test('should return false when point is outside bottom-right corner', () => { expect(rect.isInBottomRightCorner(379, 600, cornerSize)).toBe(false) expect(rect.isInBottomRightCorner(400, 579, cornerSize)).toBe(false) expect(rect.isInBottomRightCorner(401, 600, cornerSize)).toBe(false) @@ -121,24 +121,24 @@ describe("Rectangle resize functionality", () => { }) }) - describe("edge cases", () => { - test("should handle zero-sized corner areas", () => { + describe('edge cases', () => { + test('should handle zero-sized corner areas', () => { expect(rect.findContainingCorner(100, 200, 0)).toBeUndefined() expect(rect.isInTopLeftCorner(100, 200, 0)).toBe(false) }) - test("should handle rectangles at origin", () => { + test('should handle rectangles at origin', () => { const originRect = new Rectangle(0, 0, 100, 100) - expect(originRect.findContainingCorner(0, 0, 10)).toBe("NW") + expect(originRect.findContainingCorner(0, 0, 10)).toBe('NW') // Bottom-right corner is at (90, 90) to (100, 100) - expect(originRect.findContainingCorner(90, 90, 10)).toBe("SE") + expect(originRect.findContainingCorner(90, 90, 10)).toBe('SE') }) - test("should handle negative coordinates", () => { + test('should handle negative coordinates', () => { const negRect = new Rectangle(-50, -50, 100, 100) - expect(negRect.findContainingCorner(-50, -50, 10)).toBe("NW") + expect(negRect.findContainingCorner(-50, -50, 10)).toBe('NW') // Bottom-right corner is at (40, 40) to (50, 50) - expect(negRect.findContainingCorner(40, 40, 10)).toBe("SE") + expect(negRect.findContainingCorner(40, 40, 10)).toBe('SE') }) }) }) diff --git a/src/lib/litegraph/test/infrastructure/Rectangle.test.ts b/src/lib/litegraph/test/infrastructure/Rectangle.test.ts index 2590a9fcd5..42cea0f094 100644 --- a/src/lib/litegraph/test/infrastructure/Rectangle.test.ts +++ b/src/lib/litegraph/test/infrastructure/Rectangle.test.ts @@ -1,20 +1,19 @@ -import type { Point, Size } from "@/lib/litegraph/src/interfaces" +import { test as baseTest, describe, expect, vi } from 'vitest' -import { describe, expect, test as baseTest, vi } from "vitest" - -import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle" +import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' +import type { Point, Size } from '@/lib/litegraph/src/interfaces' // TODO: If there's a common test context, use it here // For now, we'll define a simple context for Rectangle tests const test = baseTest.extend<{ rect: Rectangle }>({ rect: async ({}, use) => { await use(new Rectangle()) - }, + } }) -describe("Rectangle", () => { - describe("constructor and basic properties", () => { - test("should create a default rectangle", ({ rect }) => { +describe('Rectangle', () => { + describe('constructor and basic properties', () => { + test('should create a default rectangle', ({ rect }) => { expect(rect.x).toBe(0) expect(rect.y).toBe(0) expect(rect.width).toBe(0) @@ -22,7 +21,7 @@ describe("Rectangle", () => { expect(rect.length).toBe(4) }) - test("should create a rectangle with specified values", () => { + test('should create a rectangle with specified values', () => { const rect = new Rectangle(1, 2, 3, 4) expect(rect.x).toBe(1) expect(rect.y).toBe(2) @@ -30,7 +29,7 @@ describe("Rectangle", () => { expect(rect.height).toBe(4) }) - test("should update the rectangle values", ({ rect }) => { + test('should update the rectangle values', ({ rect }) => { const newValues: [number, number, number, number] = [1, 2, 3, 4] rect.updateTo(newValues) expect(rect.x).toBe(1) @@ -40,8 +39,8 @@ describe("Rectangle", () => { }) }) - describe("array operations", () => { - test("should return a Float64Array representing the subarray", () => { + describe('array operations', () => { + test('should return a Float64Array representing the subarray', () => { const rect = new Rectangle(10, 20, 30, 40) const sub = rect.subarray(1, 3) expect(sub).toBeInstanceOf(Float64Array) @@ -50,7 +49,7 @@ describe("Rectangle", () => { expect(sub[1]).toBe(30) // width }) - test("should return a Float64Array for the entire array if no args", () => { + test('should return a Float64Array for the entire array if no args', () => { const rect = new Rectangle(10, 20, 30, 40) const sub = rect.subarray() expect(sub).toBeInstanceOf(Float64Array) @@ -61,7 +60,7 @@ describe("Rectangle", () => { expect(sub[3]).toBe(40) }) - test("should return an array with [x, y, width, height]", () => { + test('should return an array with [x, y, width, height]', () => { const rect = new Rectangle(1, 2, 3, 4) const arr = rect.toArray() expect(arr).toEqual([1, 2, 3, 4]) @@ -75,8 +74,8 @@ describe("Rectangle", () => { }) }) - describe("position and size properties", () => { - test("should get the position", ({ rect }) => { + describe('position and size properties', () => { + test('should get the position', ({ rect }) => { rect.x = 10 rect.y = 20 const pos = rect.pos @@ -85,14 +84,16 @@ describe("Rectangle", () => { expect(pos.length).toBe(2) }) - test("should set the position", ({ rect }) => { + test('should set the position', ({ rect }) => { const newPos: Point = [5, 15] rect.pos = newPos expect(rect.x).toBe(5) expect(rect.y).toBe(15) }) - test("should update the rectangle when the returned pos object is modified", ({ rect }) => { + test('should update the rectangle when the returned pos object is modified', ({ + rect + }) => { rect.x = 1 rect.y = 2 const pos = rect.pos @@ -102,7 +103,7 @@ describe("Rectangle", () => { expect(rect.y).toBe(200) }) - test("should get the size", ({ rect }) => { + test('should get the size', ({ rect }) => { rect.width = 30 rect.height = 40 const size = rect.size @@ -111,14 +112,16 @@ describe("Rectangle", () => { expect(size.length).toBe(2) }) - test("should set the size", ({ rect }) => { + test('should set the size', ({ rect }) => { const newSize: Size = [35, 45] rect.size = newSize expect(rect.width).toBe(35) expect(rect.height).toBe(45) }) - test("should update the rectangle when the returned size object is modified", ({ rect }) => { + test('should update the rectangle when the returned size object is modified', ({ + rect + }) => { rect.width = 3 rect.height = 4 const size = rect.size @@ -129,74 +132,74 @@ describe("Rectangle", () => { }) }) - describe("edge properties", () => { - test("should get x", ({ rect }) => { + describe('edge properties', () => { + test('should get x', ({ rect }) => { rect[0] = 5 expect(rect.x).toBe(5) }) - test("should set x", ({ rect }) => { + test('should set x', ({ rect }) => { rect.x = 10 expect(rect[0]).toBe(10) }) - test("should get y", ({ rect }) => { + test('should get y', ({ rect }) => { rect[1] = 6 expect(rect.y).toBe(6) }) - test("should set y", ({ rect }) => { + test('should set y', ({ rect }) => { rect.y = 11 expect(rect[1]).toBe(11) }) - test("should get width", ({ rect }) => { + test('should get width', ({ rect }) => { rect[2] = 7 expect(rect.width).toBe(7) }) - test("should set width", ({ rect }) => { + test('should set width', ({ rect }) => { rect.width = 12 expect(rect[2]).toBe(12) }) - test("should get height", ({ rect }) => { + test('should get height', ({ rect }) => { rect[3] = 8 expect(rect.height).toBe(8) }) - test("should set height", ({ rect }) => { + test('should set height', ({ rect }) => { rect.height = 13 expect(rect[3]).toBe(13) }) - test("should get left", ({ rect }) => { + test('should get left', ({ rect }) => { rect[0] = 1 expect(rect.left).toBe(1) }) - test("should set left", ({ rect }) => { + test('should set left', ({ rect }) => { rect.left = 2 expect(rect[0]).toBe(2) }) - test("should get top", ({ rect }) => { + test('should get top', ({ rect }) => { rect[1] = 3 expect(rect.top).toBe(3) }) - test("should set top", ({ rect }) => { + test('should set top', ({ rect }) => { rect.top = 4 expect(rect[1]).toBe(4) }) - test("should get right", ({ rect }) => { + test('should get right', ({ rect }) => { rect[0] = 1 rect[2] = 10 expect(rect.right).toBe(11) }) - test("should set right", ({ rect }) => { + test('should set right', ({ rect }) => { rect.x = 1 rect.width = 10 // right is 11 rect.right = 20 // new right @@ -204,13 +207,13 @@ describe("Rectangle", () => { expect(rect.width).toBe(10) }) - test("should get bottom", ({ rect }) => { + test('should get bottom', ({ rect }) => { rect[1] = 2 rect[3] = 20 expect(rect.bottom).toBe(22) }) - test("should set bottom", ({ rect }) => { + test('should set bottom', ({ rect }) => { rect.y = 2 rect.height = 20 // bottom is 22 rect.bottom = 30 // new bottom @@ -218,7 +221,7 @@ describe("Rectangle", () => { expect(rect.height).toBe(20) }) - test("should get centreX", () => { + test('should get centreX', () => { const rect = new Rectangle(0, 0, 10, 0) expect(rect.centreX).toBe(5) rect.x = 5 @@ -227,7 +230,7 @@ describe("Rectangle", () => { expect(rect.centreX).toBe(15) // 5 + (20 * 0.5) }) - test("should get centreY", () => { + test('should get centreY', () => { const rect = new Rectangle(0, 0, 0, 10) expect(rect.centreY).toBe(5) rect.y = 5 @@ -237,8 +240,8 @@ describe("Rectangle", () => { }) }) - describe("geometric operations", () => { - test("should return the centre point", () => { + describe('geometric operations', () => { + test('should return the centre point', () => { const rect = new Rectangle(10, 20, 30, 40) // centreX = 10 + 15 = 25, centreY = 20 + 20 = 40 const centre = rect.getCentre() expect(centre[0]).toBe(25) @@ -246,17 +249,17 @@ describe("Rectangle", () => { expect(centre).not.toBe(rect.pos) // Should be a new Point }) - test("should return the area", () => { + test('should return the area', () => { expect(new Rectangle(0, 0, 5, 10).getArea()).toBe(50) expect(new Rectangle(1, 1, 0, 10).getArea()).toBe(0) }) - test("should return the perimeter", () => { + test('should return the perimeter', () => { expect(new Rectangle(0, 0, 5, 10).getPerimeter()).toBe(30) // 2 * (5+10) expect(new Rectangle(0, 0, 0, 0).getPerimeter()).toBe(0) }) - test("should return the top-left point", () => { + test('should return the top-left point', () => { const rect = new Rectangle(1, 2, 3, 4) const tl = rect.getTopLeft() expect(tl[0]).toBe(1) @@ -264,14 +267,14 @@ describe("Rectangle", () => { expect(tl).not.toBe(rect.pos) }) - test("should return the bottom-right point", () => { + test('should return the bottom-right point', () => { const rect = new Rectangle(1, 2, 10, 20) // right=11, bottom=22 const br = rect.getBottomRight() expect(br[0]).toBe(11) expect(br[1]).toBe(22) }) - test("should return the size", () => { + test('should return the size', () => { const rect = new Rectangle(1, 2, 30, 40) const s = rect.getSize() expect(s[0]).toBe(30) @@ -279,14 +282,14 @@ describe("Rectangle", () => { expect(s).not.toBe(rect.size) }) - test("should return the offset from top-left to the point", () => { + test('should return the offset from top-left to the point', () => { const rect = new Rectangle(10, 20, 5, 5) const offset = rect.getOffsetTo([12, 23]) expect(offset[0]).toBe(2) // 12 - 10 expect(offset[1]).toBe(3) // 23 - 20 }) - test("should return the offset from the point to the top-left", () => { + test('should return the offset from the point to the top-left', () => { const rect = new Rectangle(10, 20, 5, 5) const offset = rect.getOffsetFrom([12, 23]) expect(offset[0]).toBe(-2) // 10 - 12 @@ -294,7 +297,7 @@ describe("Rectangle", () => { }) }) - describe("containment and overlap", () => { + describe('containment and overlap', () => { const rect = new Rectangle(10, 10, 20, 20) // x: 10, y: 10, right: 30, bottom: 30 test.each([ @@ -306,10 +309,13 @@ describe("Rectangle", () => { [15, 5, false], // outside top [15, 30, false], // outside bottom [10, 29, true], // on bottom edge - [29, 10, true], // on right edge - ])("when checking if (%s, %s) is inside, should return %s", (x, y, expected) => { - expect(rect.containsXy(x, y)).toBe(expected) - }) + [29, 10, true] // on right edge + ])( + 'when checking if (%s, %s) is inside, should return %s', + (x, y, expected) => { + expect(rect.containsXy(x, y)).toBe(expected) + } + ) test.each([ [[0, 0] as Point, true], @@ -318,8 +324,8 @@ describe("Rectangle", () => { [[-1, 5] as Point, false], [[11, 5] as Point, false], [[5, -1] as Point, false], - [[5, 11] as Point, false], - ])("should return %s for point %j", (point: Point, expected: boolean) => { + [[5, 11] as Point, false] + ])('should return %s for point %j', (point: Point, expected: boolean) => { rect.updateTo([0, 0, 10, 10]) expect(rect.containsPoint(point)).toBe(expected) }) @@ -340,18 +346,25 @@ describe("Rectangle", () => { // Outer rectangle is smaller [new Rectangle(0, 0, 5, 5), new Rectangle(0, 0, 10, 10), true], // Same size - [new Rectangle(0, 0, 99, 99), true], - ])("should return %s when checking if %s is inside outer rect", (inner: Rectangle, expectedOrOuter: boolean | Rectangle, expectedIfThreeArgs?: boolean) => { - let testOuter = rect - rect.updateTo([0, 0, 100, 100]) + [new Rectangle(0, 0, 99, 99), true] + ])( + 'should return %s when checking if %s is inside outer rect', + ( + inner: Rectangle, + expectedOrOuter: boolean | Rectangle, + expectedIfThreeArgs?: boolean + ) => { + let testOuter = rect + rect.updateTo([0, 0, 100, 100]) - let testExpected = expectedOrOuter as boolean - if (typeof expectedOrOuter !== "boolean") { - testOuter = expectedOrOuter as Rectangle - testExpected = expectedIfThreeArgs as boolean + let testExpected = expectedOrOuter as boolean + if (typeof expectedOrOuter !== 'boolean') { + testOuter = expectedOrOuter as Rectangle + testExpected = expectedIfThreeArgs as boolean + } + expect(testOuter.containsRect(inner)).toBe(testExpected) } - expect(testOuter.containsRect(inner)).toBe(testExpected) - }) + ) test.each([ // Completely overlapping @@ -372,8 +385,8 @@ describe("Rectangle", () => { [new Rectangle(100, 100, 5, 5), false], // r2 far away [new Rectangle(0, 0, 5, 5), false], // r2 outside top-left // rect1 inside rect2 - [new Rectangle(0, 0, 100, 100), true], - ])("should return %s for overlap with %s", (rect2, expected) => { + [new Rectangle(0, 0, 100, 100), true] + ])('should return %s for overlap with %s', (rect2, expected) => { const rect = new Rectangle(10, 10, 20, 20) // 10,10 to 30,30 expect(rect.overlaps(rect2)).toBe(expected) @@ -382,8 +395,10 @@ describe("Rectangle", () => { }) }) - describe("resize operations", () => { - test("should resize from top-left corner while maintaining bottom-right", ({ rect }) => { + describe('resize operations', () => { + test('should resize from top-left corner while maintaining bottom-right', ({ + rect + }) => { rect.updateTo([10, 10, 20, 20]) // x: 10, y: 10, width: 20, height: 20 rect.resizeTopLeft(5, 5) expect(rect.x).toBe(5) @@ -392,7 +407,9 @@ describe("Rectangle", () => { expect(rect.height).toBe(25) // 20 + (10 - 5) }) - test("should handle negative coordinates for top-left resize", ({ rect }) => { + test('should handle negative coordinates for top-left resize', ({ + rect + }) => { rect.updateTo([10, 10, 20, 20]) rect.resizeTopLeft(-5, -5) expect(rect.x).toBe(-5) @@ -401,7 +418,9 @@ describe("Rectangle", () => { expect(rect.height).toBe(35) // 20 + (10 - (-5)) }) - test("should resize from bottom-left corner while maintaining top-right", ({ rect }) => { + test('should resize from bottom-left corner while maintaining top-right', ({ + rect + }) => { rect.updateTo([10, 10, 20, 20]) rect.resizeBottomLeft(5, 35) expect(rect.x).toBe(5) @@ -410,7 +429,9 @@ describe("Rectangle", () => { expect(rect.height).toBe(25) // 35 - 10 }) - test("should handle negative coordinates for bottom-left resize", ({ rect }) => { + test('should handle negative coordinates for bottom-left resize', ({ + rect + }) => { rect.updateTo([10, 10, 20, 20]) rect.resizeBottomLeft(-5, 35) expect(rect.x).toBe(-5) @@ -419,7 +440,9 @@ describe("Rectangle", () => { expect(rect.height).toBe(25) // 35 - 10 }) - test("should resize from top-right corner while maintaining bottom-left", ({ rect }) => { + test('should resize from top-right corner while maintaining bottom-left', ({ + rect + }) => { rect.updateTo([10, 10, 20, 20]) rect.resizeTopRight(35, 5) expect(rect.x).toBe(10) @@ -428,7 +451,9 @@ describe("Rectangle", () => { expect(rect.height).toBe(25) // 20 + (10 - 5) }) - test("should handle negative coordinates for top-right resize", ({ rect }) => { + test('should handle negative coordinates for top-right resize', ({ + rect + }) => { rect.updateTo([10, 10, 20, 20]) rect.resizeTopRight(35, -5) expect(rect.x).toBe(10) @@ -437,7 +462,9 @@ describe("Rectangle", () => { expect(rect.height).toBe(35) // 20 + (10 - (-5)) }) - test("should resize from bottom-right corner while maintaining top-left", ({ rect }) => { + test('should resize from bottom-right corner while maintaining top-left', ({ + rect + }) => { rect.updateTo([10, 10, 20, 20]) rect.resizeBottomRight(35, 35) expect(rect.x).toBe(10) @@ -446,7 +473,9 @@ describe("Rectangle", () => { expect(rect.height).toBe(25) // 35 - 10 }) - test("should handle negative coordinates for bottom-right resize", ({ rect }) => { + test('should handle negative coordinates for bottom-right resize', ({ + rect + }) => { rect.updateTo([10, 10, 20, 20]) rect.resizeBottomRight(35, -5) expect(rect.x).toBe(10) @@ -455,7 +484,7 @@ describe("Rectangle", () => { expect(rect.height).toBe(-15) // -5 - 10 }) - test("should set width, anchoring the right edge", () => { + test('should set width, anchoring the right edge', () => { const rect = new Rectangle(10, 0, 20, 0) // x:10, width:20 -> right:30 rect.setWidthRightAnchored(15) // new width 15 expect(rect.width).toBe(15) @@ -463,7 +492,7 @@ describe("Rectangle", () => { expect(rect.right).toBe(30) // right should remain 30 (15+15) }) - test("should set height, anchoring the bottom edge", () => { + test('should set height, anchoring the bottom edge', () => { const rect = new Rectangle(0, 10, 0, 20) // y:10, height:20 -> bottom:30 rect.setHeightBottomAnchored(15) // new height 15 expect(rect.height).toBe(15) @@ -472,21 +501,21 @@ describe("Rectangle", () => { }) }) - describe("debug drawing", () => { - test("should call canvas context methods", () => { + describe('debug drawing', () => { + test('should call canvas context methods', () => { const rect = new Rectangle(10, 20, 30, 40) const mockCtx = { - strokeStyle: "black", + strokeStyle: 'black', lineWidth: 1, beginPath: vi.fn(), - strokeRect: vi.fn(), + strokeRect: vi.fn() } as unknown as CanvasRenderingContext2D - rect._drawDebug(mockCtx, "blue") + rect._drawDebug(mockCtx, 'blue') expect(mockCtx.beginPath).toHaveBeenCalledOnce() expect(mockCtx.strokeRect).toHaveBeenCalledWith(10, 20, 30, 40) - expect(mockCtx.strokeStyle).toBe("black") // Restored + expect(mockCtx.strokeStyle).toBe('black') // Restored expect(mockCtx.lineWidth).toBe(1) // Restored // Check if it was set during the call @@ -496,20 +525,20 @@ describe("Rectangle", () => { // A more robust test could involve spying on property assignments if vitest supports it easily. }) - test("should use default color if not provided", () => { + test('should use default color if not provided', () => { const rect = new Rectangle(1, 2, 3, 4) const mockCtx = { - strokeStyle: "black", + strokeStyle: 'black', lineWidth: 1, beginPath: vi.fn(), - strokeRect: vi.fn(), + strokeRect: vi.fn() } as unknown as CanvasRenderingContext2D rect._drawDebug(mockCtx) // Check if strokeStyle was "red" at the time of strokeRect // This requires a more complex mock or observing calls. // A simple check is that it ran without error and values were restored. expect(mockCtx.strokeRect).toHaveBeenCalledWith(1, 2, 3, 4) - expect(mockCtx.strokeStyle).toBe("black") + expect(mockCtx.strokeStyle).toBe('black') }) }) }) diff --git a/src/lib/litegraph/test/litegraph.test.ts b/src/lib/litegraph/test/litegraph.test.ts index d93fe646cd..7987d2822c 100644 --- a/src/lib/litegraph/test/litegraph.test.ts +++ b/src/lib/litegraph/test/litegraph.test.ts @@ -1,36 +1,38 @@ -import { beforeEach, describe, expect, vi } from "vitest" +import { beforeEach, describe, expect, vi } from 'vitest' -import { clamp, LGraphCanvas, LiteGraph } from "@/lib/litegraph/src/litegraph" -import { LiteGraphGlobal } from "@/lib/litegraph/src/LiteGraphGlobal" +import { LiteGraphGlobal } from '@/lib/litegraph/src/LiteGraphGlobal' +import { LGraphCanvas, LiteGraph, clamp } from '@/lib/litegraph/src/litegraph' -import { test } from "./testExtensions" +import { test } from './testExtensions' -describe("Litegraph module", () => { - test("contains a global export", ({ expect }) => { +describe('Litegraph module', () => { + test('contains a global export', ({ expect }) => { expect(LiteGraph).toBeInstanceOf(LiteGraphGlobal) expect(LiteGraph.LGraphCanvas).toBe(LGraphCanvas) }) - test("has the same structure", ({ expect }) => { + test('has the same structure', ({ expect }) => { const lgGlobal = new LiteGraphGlobal() - expect(lgGlobal).toMatchSnapshot("minLGraph") + expect(lgGlobal).toMatchSnapshot('minLGraph') }) - test("clamps values", () => { + test('clamps values', () => { expect(clamp(-1.124, 13, 24)).toStrictEqual(13) expect(clamp(Infinity, 18, 29)).toStrictEqual(29) }) }) -describe("Import order dependency", () => { +describe('Import order dependency', () => { beforeEach(() => { vi.resetModules() }) - test("Imports without error when entry point is imported first", async ({ expect }) => { + test('Imports without error when entry point is imported first', async ({ + expect + }) => { async function importNormally() { - const entryPointImport = await import("@/lib/litegraph/src/litegraph") - const directImport = await import("@/lib/litegraph/src/LGraph") + const entryPointImport = await import('@/lib/litegraph/src/litegraph') + const directImport = await import('@/lib/litegraph/src/LGraph') // Sanity check that imports were cleared. expect(Object.is(LiteGraph, entryPointImport.LiteGraph)).toBe(false) diff --git a/src/lib/litegraph/test/measure.test.ts b/src/lib/litegraph/test/measure.test.ts index 57967d07bc..854365810b 100644 --- a/src/lib/litegraph/test/measure.test.ts +++ b/src/lib/litegraph/test/measure.test.ts @@ -1,7 +1,6 @@ -import type { Point, Rect } from "../src/interfaces" - -import { test as baseTest } from "vitest" +import { test as baseTest } from 'vitest' +import type { Point, Rect } from '../src/interfaces' import { addDirectionalOffset, containsCentre, @@ -17,25 +16,29 @@ import { isPointInRect, overlapBounding, rotateLink, - snapPoint, -} from "../src/measure" -import { LinkDirection } from "../src/types/globalEnums" + snapPoint +} from '../src/measure' +import { LinkDirection } from '../src/types/globalEnums' const test = baseTest.extend({}) -test("distance calculates correct distance between two points", ({ expect }) => { +test('distance calculates correct distance between two points', ({ + expect +}) => { expect(distance([0, 0], [3, 4])).toBe(5) // 3-4-5 triangle expect(distance([1, 1], [4, 5])).toBe(5) // Same triangle, shifted expect(distance([0, 0], [0, 0])).toBe(0) // Same point }) -test("dist2 calculates squared distance between points", ({ expect }) => { +test('dist2 calculates squared distance between points', ({ expect }) => { expect(dist2(0, 0, 3, 4)).toBe(25) // 3-4-5 triangle squared expect(dist2(1, 1, 4, 5)).toBe(25) // Same triangle, shifted expect(dist2(0, 0, 0, 0)).toBe(0) // Same point }) -test("isInRectangle correctly identifies points inside rectangle", ({ expect }) => { +test('isInRectangle correctly identifies points inside rectangle', ({ + expect +}) => { // Test points inside expect(isInRectangle(5, 5, 0, 0, 10, 10)).toBe(true) // Test points on edges (should be true) @@ -46,13 +49,17 @@ test("isInRectangle correctly identifies points inside rectangle", ({ expect }) expect(isInRectangle(11, 5, 0, 0, 10, 10)).toBe(false) }) -test("isPointInRect correctly identifies points inside rectangle", ({ expect }) => { +test('isPointInRect correctly identifies points inside rectangle', ({ + expect +}) => { const rect: Rect = [0, 0, 10, 10] expect(isPointInRect([5, 5], rect)).toBe(true) expect(isPointInRect([-1, 5], rect)).toBe(false) }) -test("overlapBounding correctly identifies overlapping rectangles", ({ expect }) => { +test('overlapBounding correctly identifies overlapping rectangles', ({ + expect +}) => { const rect1: Rect = [0, 0, 10, 10] const rect2: Rect = [5, 5, 10, 10] const rect3: Rect = [20, 20, 10, 10] @@ -61,7 +68,9 @@ test("overlapBounding correctly identifies overlapping rectangles", ({ expect }) expect(overlapBounding(rect1, rect3)).toBe(false) }) -test("containsCentre correctly identifies if rectangle contains center of another", ({ expect }) => { +test('containsCentre correctly identifies if rectangle contains center of another', ({ + expect +}) => { const container: Rect = [0, 0, 20, 20] const inside: Rect = [5, 5, 10, 10] // Center at 10,10 const outside: Rect = [15, 15, 10, 10] // Center at 20,20 @@ -70,7 +79,7 @@ test("containsCentre correctly identifies if rectangle contains center of anothe expect(containsCentre(container, outside)).toBe(false) }) -test("addDirectionalOffset correctly adds offsets", ({ expect }) => { +test('addDirectionalOffset correctly adds offsets', ({ expect }) => { const point: Point = [10, 10] // Test each direction @@ -90,7 +99,7 @@ test("addDirectionalOffset correctly adds offsets", ({ expect }) => { expect(point).toEqual([10, 5]) }) -test("findPointOnCurve correctly interpolates curve points", ({ expect }) => { +test('findPointOnCurve correctly interpolates curve points', ({ expect }) => { const out: Point = [0, 0] const start: Point = [0, 0] const end: Point = [10, 10] @@ -103,7 +112,7 @@ test("findPointOnCurve correctly interpolates curve points", ({ expect }) => { expect(out[1]).toBeCloseTo(5) }) -test("snapPoint correctly snaps points to grid", ({ expect }) => { +test('snapPoint correctly snaps points to grid', ({ expect }) => { const point: Point = [12.3, 18.7] // Snap to 5 @@ -120,10 +129,10 @@ test("snapPoint correctly snaps points to grid", ({ expect }) => { expect(point3).toEqual([20, 20]) }) -test("createBounds correctly creates bounding box", ({ expect }) => { +test('createBounds correctly creates bounding box', ({ expect }) => { const objects = [ { boundingRect: [0, 0, 10, 10] as Rect }, - { boundingRect: [5, 5, 10, 10] as Rect }, + { boundingRect: [5, 5, 10, 10] as Rect } ] const defaultBounds = createBounds(objects) @@ -136,7 +145,9 @@ test("createBounds correctly creates bounding box", ({ expect }) => { expect(createBounds([])).toBe(null) }) -test("isInsideRectangle handles edge cases differently from isInRectangle", ({ expect }) => { +test('isInsideRectangle handles edge cases differently from isInRectangle', ({ + expect +}) => { // isInsideRectangle returns false when point is exactly on left or top edge expect(isInsideRectangle(0, 5, 0, 0, 10, 10)).toBe(false) expect(isInsideRectangle(5, 0, 0, 0, 10, 10)).toBe(false) @@ -153,7 +164,7 @@ test("isInsideRectangle handles edge cases differently from isInRectangle", ({ e expect(isInsideRectangle(11, 5, 0, 0, 10, 10)).toBe(false) }) -test("containsRect correctly identifies nested rectangles", ({ expect }) => { +test('containsRect correctly identifies nested rectangles', ({ expect }) => { const container: Rect = [0, 0, 20, 20] // Fully contained rectangle @@ -177,38 +188,40 @@ test("containsRect correctly identifies nested rectangles", ({ expect }) => { expect(containsRect(container, larger)).toBe(false) }) -test("rotateLink correctly rotates offsets between directions", ({ expect }) => { +test('rotateLink correctly rotates offsets between directions', ({ + expect +}) => { const testCases = [ { offset: [10, 5] as Point, from: LinkDirection.LEFT, to: LinkDirection.RIGHT, - expected: [-10, -5], + expected: [-10, -5] }, { offset: [10, 5] as Point, from: LinkDirection.LEFT, to: LinkDirection.UP, - expected: [5, -10], + expected: [5, -10] }, { offset: [10, 5] as Point, from: LinkDirection.LEFT, to: LinkDirection.DOWN, - expected: [-5, 10], + expected: [-5, 10] }, { offset: [10, 5] as Point, from: LinkDirection.RIGHT, to: LinkDirection.LEFT, - expected: [-10, -5], + expected: [-10, -5] }, { offset: [10, 5] as Point, from: LinkDirection.UP, to: LinkDirection.DOWN, - expected: [-10, -5], - }, + expected: [-10, -5] + } ] for (const { offset, from, to, expected } of testCases) { @@ -232,7 +245,9 @@ test("rotateLink correctly rotates offsets between directions", ({ expect }) => expect(noneCase).toEqual([10, 5]) }) -test("getOrientation correctly determines point position relative to line", ({ expect }) => { +test('getOrientation correctly determines point position relative to line', ({ + expect +}) => { const lineStart: Point = [0, 0] const lineEnd: Point = [10, 10] @@ -256,7 +271,9 @@ test("getOrientation correctly determines point position relative to line", ({ e expect(getOrientation(lineStart, vLineEnd, -5, 5)).toBeLessThan(0) // Left of line }) -test("isInRect correctly identifies if point coordinates are inside rectangle", ({ expect }) => { +test('isInRect correctly identifies if point coordinates are inside rectangle', ({ + expect +}) => { const rect: Rect = [0, 0, 10, 10] // Points inside diff --git a/src/lib/litegraph/test/serialise.test.ts b/src/lib/litegraph/test/serialise.test.ts index 807053dd4b..cd55eabc10 100644 --- a/src/lib/litegraph/test/serialise.test.ts +++ b/src/lib/litegraph/test/serialise.test.ts @@ -1,15 +1,14 @@ -import type { ISerialisedGraph } from "@/lib/litegraph/src/types/serialisation" +import { describe } from 'vitest' -import { describe } from "vitest" +import { LGraph, LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation' -import { LGraph, LGraphGroup, LGraphNode } from "@/lib/litegraph/src/litegraph" +import { test } from './testExtensions' -import { test } from "./testExtensions" - -describe("LGraph Serialisation", () => { - test("can (de)serialise node / group titles", ({ expect, minimalGraph }) => { - const nodeTitle = "Test Node" - const groupTitle = "Test Group" +describe('LGraph Serialisation', () => { + test('can (de)serialise node / group titles', ({ expect, minimalGraph }) => { + const nodeTitle = 'Test Node' + const groupTitle = 'Test Group' minimalGraph.add(new LGraphNode(nodeTitle)) minimalGraph.add(new LGraphGroup(groupTitle)) diff --git a/src/lib/litegraph/test/subgraph/ExecutableNodeDTO.test.ts b/src/lib/litegraph/test/subgraph/ExecutableNodeDTO.test.ts index 0a32e82a71..5c05db4cdf 100644 --- a/src/lib/litegraph/test/subgraph/ExecutableNodeDTO.test.ts +++ b/src/lib/litegraph/test/subgraph/ExecutableNodeDTO.test.ts @@ -1,20 +1,20 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from 'vitest' -import { LGraph, LGraphNode } from "@/lib/litegraph/src/litegraph" -import { ExecutableNodeDTO } from "@/lib/litegraph/src/subgraph/ExecutableNodeDTO" +import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph' +import { ExecutableNodeDTO } from '@/lib/litegraph/src/subgraph/ExecutableNodeDTO' import { createNestedSubgraphs, createTestSubgraph, - createTestSubgraphNode, -} from "./fixtures/subgraphHelpers" + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("ExecutableNodeDTO Creation", () => { - it("should create DTO from regular node", () => { +describe('ExecutableNodeDTO Creation', () => { + it('should create DTO from regular node', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") - node.addInput("in", "number") - node.addOutput("out", "string") + const node = new LGraphNode('Test Node') + node.addInput('in', 'number') + node.addOutput('out', 'string') graph.add(node) const executableNodes = new Map() @@ -26,44 +26,44 @@ describe("ExecutableNodeDTO Creation", () => { expect(dto.id).toBe(node.id.toString()) }) - it("should create DTO with subgraph path", () => { + it('should create DTO with subgraph path', () => { const graph = new LGraph() - const node = new LGraphNode("Inner Node") + const node = new LGraphNode('Inner Node') node.id = 42 graph.add(node) - const subgraphPath = ["10", "20"] as const + const subgraphPath = ['10', '20'] as const const dto = new ExecutableNodeDTO(node, subgraphPath, new Map(), undefined) expect(dto.subgraphNodePath).toBe(subgraphPath) - expect(dto.id).toBe("10:20:42") + expect(dto.id).toBe('10:20:42') }) - it("should clone input slot data", () => { + it('should clone input slot data', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") - node.addInput("input1", "number") - node.addInput("input2", "string") + const node = new LGraphNode('Test Node') + node.addInput('input1', 'number') + node.addInput('input2', 'string') node.inputs[0].link = 123 // Simulate connected input graph.add(node) const dto = new ExecutableNodeDTO(node, [], new Map(), undefined) expect(dto.inputs).toHaveLength(2) - expect(dto.inputs[0].name).toBe("input1") - expect(dto.inputs[0].type).toBe("number") + expect(dto.inputs[0].name).toBe('input1') + expect(dto.inputs[0].type).toBe('number') expect(dto.inputs[0].linkId).toBe(123) - expect(dto.inputs[1].name).toBe("input2") - expect(dto.inputs[1].type).toBe("string") + expect(dto.inputs[1].name).toBe('input2') + expect(dto.inputs[1].type).toBe('string') expect(dto.inputs[1].linkId).toBeNull() // Should be a copy, not reference expect(dto.inputs).not.toBe(node.inputs) }) - it("should inherit graph reference", () => { + it('should inherit graph reference', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") + const node = new LGraphNode('Test Node') graph.add(node) const dto = new ExecutableNodeDTO(node, [], new Map(), undefined) @@ -71,9 +71,9 @@ describe("ExecutableNodeDTO Creation", () => { expect(dto.graph).toBe(graph) }) - it("should wrap applyToGraph method if present", () => { + it('should wrap applyToGraph method if present', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") + const node = new LGraphNode('Test Node') const mockApplyToGraph = vi.fn() Object.assign(node, { applyToGraph: mockApplyToGraph }) graph.add(node) @@ -83,7 +83,7 @@ describe("ExecutableNodeDTO Creation", () => { expect(dto.applyToGraph).toBeDefined() // Test that wrapper calls original method - const args = ["arg1", "arg2"] + const args = ['arg1', 'arg2'] dto.applyToGraph!(args[0], args[1]) expect(mockApplyToGraph).toHaveBeenCalledWith(args[0], args[1]) @@ -91,7 +91,7 @@ describe("ExecutableNodeDTO Creation", () => { it("should not create applyToGraph wrapper if method doesn't exist", () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") + const node = new LGraphNode('Test Node') graph.add(node) const dto = new ExecutableNodeDTO(node, [], new Map(), undefined) @@ -100,65 +100,65 @@ describe("ExecutableNodeDTO Creation", () => { }) }) -describe("ExecutableNodeDTO Path-Based IDs", () => { - it("should generate simple ID for root node", () => { +describe('ExecutableNodeDTO Path-Based IDs', () => { + it('should generate simple ID for root node', () => { const graph = new LGraph() - const node = new LGraphNode("Root Node") + const node = new LGraphNode('Root Node') node.id = 5 graph.add(node) const dto = new ExecutableNodeDTO(node, [], new Map(), undefined) - expect(dto.id).toBe("5") + expect(dto.id).toBe('5') }) - it("should generate path-based ID for nested node", () => { + it('should generate path-based ID for nested node', () => { const graph = new LGraph() - const node = new LGraphNode("Nested Node") + const node = new LGraphNode('Nested Node') node.id = 3 graph.add(node) - const path = ["1", "2"] as const + const path = ['1', '2'] as const const dto = new ExecutableNodeDTO(node, path, new Map(), undefined) - expect(dto.id).toBe("1:2:3") + expect(dto.id).toBe('1:2:3') }) - it("should handle deep nesting paths", () => { + it('should handle deep nesting paths', () => { const graph = new LGraph() - const node = new LGraphNode("Deep Node") + const node = new LGraphNode('Deep Node') node.id = 99 graph.add(node) - const path = ["1", "2", "3", "4", "5"] as const + const path = ['1', '2', '3', '4', '5'] as const const dto = new ExecutableNodeDTO(node, path, new Map(), undefined) - expect(dto.id).toBe("1:2:3:4:5:99") + expect(dto.id).toBe('1:2:3:4:5:99') }) - it("should handle string and number IDs consistently", () => { + it('should handle string and number IDs consistently', () => { const graph = new LGraph() - const node1 = new LGraphNode("Node 1") + const node1 = new LGraphNode('Node 1') node1.id = 10 graph.add(node1) - const node2 = new LGraphNode("Node 2") + const node2 = new LGraphNode('Node 2') node2.id = 20 graph.add(node2) - const dto1 = new ExecutableNodeDTO(node1, ["5"], new Map(), undefined) - const dto2 = new ExecutableNodeDTO(node2, ["5"], new Map(), undefined) + const dto1 = new ExecutableNodeDTO(node1, ['5'], new Map(), undefined) + const dto2 = new ExecutableNodeDTO(node2, ['5'], new Map(), undefined) - expect(dto1.id).toBe("5:10") - expect(dto2.id).toBe("5:20") + expect(dto1.id).toBe('5:10') + expect(dto2.id).toBe('5:20') }) }) -describe("ExecutableNodeDTO Input Resolution", () => { - it("should return undefined for unconnected inputs", () => { +describe('ExecutableNodeDTO Input Resolution', () => { + it('should return undefined for unconnected inputs', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") - node.addInput("in", "number") + const node = new LGraphNode('Test Node') + node.addInput('in', 'number') graph.add(node) const dto = new ExecutableNodeDTO(node, [], new Map(), undefined) @@ -168,27 +168,27 @@ describe("ExecutableNodeDTO Input Resolution", () => { expect(resolved).toBeUndefined() }) - it("should throw for non-existent input slots", () => { + it('should throw for non-existent input slots', () => { const graph = new LGraph() - const node = new LGraphNode("No Input Node") + const node = new LGraphNode('No Input Node') graph.add(node) const dto = new ExecutableNodeDTO(node, [], new Map(), undefined) // Should throw SlotIndexError for non-existent input - expect(() => dto.resolveInput(0)).toThrow("No input found for flattened id") + expect(() => dto.resolveInput(0)).toThrow('No input found for flattened id') }) - it("should handle subgraph boundary inputs", () => { + it('should handle subgraph boundary inputs', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input1", type: "number" }], - nodeCount: 1, + inputs: [{ name: 'input1', type: 'number' }], + nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph) // Get the inner node and create DTO const innerNode = subgraph.nodes[0] - const dto = new ExecutableNodeDTO(innerNode, ["1"], new Map(), subgraphNode) + const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode) // Should return undefined for unconnected input const resolved = dto.resolveInput(0) @@ -196,17 +196,17 @@ describe("ExecutableNodeDTO Input Resolution", () => { }) }) -describe("ExecutableNodeDTO Output Resolution", () => { - it("should resolve outputs for simple nodes", () => { +describe('ExecutableNodeDTO Output Resolution', () => { + it('should resolve outputs for simple nodes', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") - node.addOutput("out", "string") + const node = new LGraphNode('Test Node') + node.addOutput('out', 'string') graph.add(node) const dto = new ExecutableNodeDTO(node, [], new Map(), undefined) // resolveOutput requires type and visited parameters - const resolved = dto.resolveOutput(0, "string", new Set()) + const resolved = dto.resolveOutput(0, 'string', new Set()) expect(resolved).toBeDefined() expect(resolved?.node).toBe(dto) @@ -214,60 +214,60 @@ describe("ExecutableNodeDTO Output Resolution", () => { expect(resolved?.origin_slot).toBe(0) }) - it("should resolve cross-boundary outputs in subgraphs", () => { + it('should resolve cross-boundary outputs in subgraphs', () => { const subgraph = createTestSubgraph({ - outputs: [{ name: "output1", type: "string" }], - nodeCount: 1, + outputs: [{ name: 'output1', type: 'string' }], + nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph) // Get the inner node and create DTO const innerNode = subgraph.nodes[0] - const dto = new ExecutableNodeDTO(innerNode, ["1"], new Map(), subgraphNode) + const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode) - const resolved = dto.resolveOutput(0, "string", new Set()) + const resolved = dto.resolveOutput(0, 'string', new Set()) expect(resolved).toBeDefined() }) - it("should handle nodes with no outputs", () => { + it('should handle nodes with no outputs', () => { const graph = new LGraph() - const node = new LGraphNode("No Output Node") + const node = new LGraphNode('No Output Node') graph.add(node) const dto = new ExecutableNodeDTO(node, [], new Map(), undefined) // For regular nodes, resolveOutput returns the node itself even if no outputs // This tests the current implementation behavior - const resolved = dto.resolveOutput(0, "string", new Set()) + const resolved = dto.resolveOutput(0, 'string', new Set()) expect(resolved).toBeDefined() expect(resolved?.node).toBe(dto) expect(resolved?.origin_slot).toBe(0) }) }) -describe("ExecutableNodeDTO Properties", () => { - it("should provide access to basic properties", () => { +describe('ExecutableNodeDTO Properties', () => { + it('should provide access to basic properties', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") + const node = new LGraphNode('Test Node') node.id = 42 - node.addInput("input", "number") - node.addOutput("output", "string") + node.addInput('input', 'number') + node.addOutput('output', 'string') graph.add(node) - const dto = new ExecutableNodeDTO(node, ["1", "2"], new Map(), undefined) + const dto = new ExecutableNodeDTO(node, ['1', '2'], new Map(), undefined) - expect(dto.id).toBe("1:2:42") + expect(dto.id).toBe('1:2:42') expect(dto.type).toBe(node.type) expect(dto.title).toBe(node.title) expect(dto.mode).toBe(node.mode) expect(dto.isVirtualNode).toBe(node.isVirtualNode) }) - it("should provide access to input information", () => { + it('should provide access to input information', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") - node.addInput("testInput", "number") + const node = new LGraphNode('Test Node') + node.addInput('testInput', 'number') node.inputs[0].link = 999 // Simulate connection graph.add(node) @@ -275,35 +275,35 @@ describe("ExecutableNodeDTO Properties", () => { expect(dto.inputs).toBeDefined() expect(dto.inputs).toHaveLength(1) - expect(dto.inputs[0].name).toBe("testInput") - expect(dto.inputs[0].type).toBe("number") + expect(dto.inputs[0].name).toBe('testInput') + expect(dto.inputs[0].type).toBe('number') expect(dto.inputs[0].linkId).toBe(999) }) }) -describe("ExecutableNodeDTO Memory Efficiency", () => { - it("should create lightweight objects", () => { +describe('ExecutableNodeDTO Memory Efficiency', () => { + it('should create lightweight objects', () => { const graph = new LGraph() - const node = new LGraphNode("Test Node") - node.addInput("in1", "number") - node.addInput("in2", "string") - node.addOutput("out1", "number") - node.addOutput("out2", "string") + const node = new LGraphNode('Test Node') + node.addInput('in1', 'number') + node.addInput('in2', 'string') + node.addOutput('out1', 'number') + node.addOutput('out2', 'string') graph.add(node) - const dto = new ExecutableNodeDTO(node, ["1"], new Map(), undefined) + const dto = new ExecutableNodeDTO(node, ['1'], new Map(), undefined) // DTO should be lightweight - only essential properties expect(dto.node).toBe(node) // Reference, not copy - expect(dto.subgraphNodePath).toEqual(["1"]) // Reference to path + expect(dto.subgraphNodePath).toEqual(['1']) // Reference to path expect(dto.inputs).toHaveLength(2) // Copied input data only // Should not duplicate heavy node data - expect(dto.hasOwnProperty("outputs")).toBe(false) // Outputs not copied - expect(dto.hasOwnProperty("widgets")).toBe(false) // Widgets not copied + expect(dto.hasOwnProperty('outputs')).toBe(false) // Outputs not copied + expect(dto.hasOwnProperty('widgets')).toBe(false) // Widgets not copied }) - it("should handle disposal without memory leaks", () => { + it('should handle disposal without memory leaks', () => { const graph = new LGraph() const nodes: ExecutableNodeDTO[] = [] @@ -312,7 +312,7 @@ describe("ExecutableNodeDTO Memory Efficiency", () => { const node = new LGraphNode(`Node ${i}`) node.id = i graph.add(node) - const dto = new ExecutableNodeDTO(node, ["parent"], new Map(), undefined) + const dto = new ExecutableNodeDTO(node, ['parent'], new Map(), undefined) nodes.push(dto) } @@ -326,12 +326,12 @@ describe("ExecutableNodeDTO Memory Efficiency", () => { expect(nodes).toHaveLength(0) }) - it("should not retain unnecessary references", () => { + it('should not retain unnecessary references', () => { const subgraph = createTestSubgraph({ nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph) const innerNode = subgraph.nodes[0] - const dto = new ExecutableNodeDTO(innerNode, ["1"], new Map(), subgraphNode) + const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode) // Should hold necessary references expect(dto.node).toBe(innerNode) @@ -339,13 +339,13 @@ describe("ExecutableNodeDTO Memory Efficiency", () => { expect(dto.graph).toBe(innerNode.graph) // Should not hold heavy references that prevent GC - expect(dto.hasOwnProperty("parentGraph")).toBe(false) - expect(dto.hasOwnProperty("rootGraph")).toBe(false) + expect(dto.hasOwnProperty('parentGraph')).toBe(false) + expect(dto.hasOwnProperty('rootGraph')).toBe(false) }) }) -describe("ExecutableNodeDTO Integration", () => { - it("should work with SubgraphNode flattening", () => { +describe('ExecutableNodeDTO Integration', () => { + it('should work with SubgraphNode flattening', () => { const subgraph = createTestSubgraph({ nodeCount: 3 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -356,13 +356,13 @@ describe("ExecutableNodeDTO Integration", () => { expect(flattened[0].id).toMatch(/^1:\d+$/) }) - it.skip("should handle nested subgraph flattening", () => { + it.skip('should handle nested subgraph flattening', () => { // FIXME: Complex nested structure requires proper parent graph setup // This test needs investigation of how resolveSubgraphIdPath works // Skip for now - will implement in edge cases test file const nested = createNestedSubgraphs({ depth: 2, - nodesPerLevel: 1, + nodesPerLevel: 1 }) const rootSubgraphNode = nested.subgraphNodes[0] @@ -370,19 +370,24 @@ describe("ExecutableNodeDTO Integration", () => { const flattened = rootSubgraphNode.getInnerNodes(executableNodes) expect(flattened.length).toBeGreaterThan(0) - const hierarchicalIds = flattened.filter(dto => dto.id.includes(":")) + const hierarchicalIds = flattened.filter((dto) => dto.id.includes(':')) expect(hierarchicalIds.length).toBeGreaterThan(0) }) - it("should preserve original node properties through DTO", () => { + it('should preserve original node properties through DTO', () => { const graph = new LGraph() - const originalNode = new LGraphNode("Original") + const originalNode = new LGraphNode('Original') originalNode.id = 123 - originalNode.addInput("test", "number") + originalNode.addInput('test', 'number') originalNode.properties = { value: 42 } graph.add(originalNode) - const dto = new ExecutableNodeDTO(originalNode, ["parent"], new Map(), undefined) + const dto = new ExecutableNodeDTO( + originalNode, + ['parent'], + new Map(), + undefined + ) // DTO should provide access to original node properties expect(dto.node.id).toBe(123) @@ -390,26 +395,31 @@ describe("ExecutableNodeDTO Integration", () => { expect(dto.node.properties.value).toBe(42) // But DTO ID should be path-based - expect(dto.id).toBe("parent:123") + expect(dto.id).toBe('parent:123') }) - it("should handle execution context correctly", () => { + it('should handle execution context correctly', () => { const subgraph = createTestSubgraph({ nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph, { id: 99 }) const innerNode = subgraph.nodes[0] innerNode.id = 55 - const dto = new ExecutableNodeDTO(innerNode, ["99"], new Map(), subgraphNode) + const dto = new ExecutableNodeDTO( + innerNode, + ['99'], + new Map(), + subgraphNode + ) // DTO provides execution context - expect(dto.id).toBe("99:55") // Path-based execution ID + expect(dto.id).toBe('99:55') // Path-based execution ID expect(dto.node.id).toBe(55) // Original node ID preserved expect(dto.subgraphNode?.id).toBe(99) // Subgraph context }) }) -describe("ExecutableNodeDTO Scale Testing", () => { - it("should create DTOs at scale", () => { +describe('ExecutableNodeDTO Scale Testing', () => { + it('should create DTOs at scale', () => { const graph = new LGraph() const startTime = performance.now() const dtos: ExecutableNodeDTO[] = [] @@ -418,10 +428,10 @@ describe("ExecutableNodeDTO Scale Testing", () => { for (let i = 0; i < 1000; i++) { const node = new LGraphNode(`Node ${i}`) node.id = i - node.addInput("in", "number") + node.addInput('in', 'number') graph.add(node) - const dto = new ExecutableNodeDTO(node, ["parent"], new Map(), undefined) + const dto = new ExecutableNodeDTO(node, ['parent'], new Map(), undefined) dtos.push(dto) } @@ -430,29 +440,31 @@ describe("ExecutableNodeDTO Scale Testing", () => { expect(dtos).toHaveLength(1000) // Test deterministic properties instead of flaky timing - expect(dtos[0].id).toBe("parent:0") - expect(dtos[999].id).toBe("parent:999") + expect(dtos[0].id).toBe('parent:0') + expect(dtos[999].id).toBe('parent:999') expect(dtos.every((dto, i) => dto.id === `parent:${i}`)).toBe(true) console.log(`Created 1000 DTOs in ${duration.toFixed(2)}ms`) }) - it("should handle complex path generation correctly", () => { + it('should handle complex path generation correctly', () => { const graph = new LGraph() - const node = new LGraphNode("Deep Node") + const node = new LGraphNode('Deep Node') node.id = 999 graph.add(node) // Test deterministic path generation behavior const testCases = [ - { depth: 1, expectedId: "1:999" }, - { depth: 3, expectedId: "1:2:3:999" }, - { depth: 5, expectedId: "1:2:3:4:5:999" }, - { depth: 10, expectedId: "1:2:3:4:5:6:7:8:9:10:999" }, + { depth: 1, expectedId: '1:999' }, + { depth: 3, expectedId: '1:2:3:999' }, + { depth: 5, expectedId: '1:2:3:4:5:999' }, + { depth: 10, expectedId: '1:2:3:4:5:6:7:8:9:10:999' } ] for (const testCase of testCases) { - const path = Array.from({ length: testCase.depth }, (_, i) => (i + 1).toString()) + const path = Array.from({ length: testCase.depth }, (_, i) => + (i + 1).toString() + ) const dto = new ExecutableNodeDTO(node, path, new Map(), undefined) expect(dto.id).toBe(testCase.expectedId) } diff --git a/src/lib/litegraph/test/subgraph/Subgraph.test.ts b/src/lib/litegraph/test/subgraph/Subgraph.test.ts index 907fddbbcb..e51fda63a2 100644 --- a/src/lib/litegraph/test/subgraph/Subgraph.test.ts +++ b/src/lib/litegraph/test/subgraph/Subgraph.test.ts @@ -5,61 +5,62 @@ * patterns for the rest of the testing team. These tests cover construction, * basic I/O management, and known issues. */ +import { describe, expect, it } from 'vitest' -import { describe, expect, it } from "vitest" +import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError' +import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph' +import { createUuidv4 } from '@/lib/litegraph/src/utils/uuid' -import { RecursionError } from "@/lib/litegraph/src/infrastructure/RecursionError" -import { LGraph, Subgraph } from "@/lib/litegraph/src/litegraph" -import { createUuidv4 } from "@/lib/litegraph/src/utils/uuid" - -import { subgraphTest } from "./fixtures/subgraphFixtures" +import { subgraphTest } from './fixtures/subgraphFixtures' import { assertSubgraphStructure, createTestSubgraph, - createTestSubgraphData, -} from "./fixtures/subgraphHelpers" + createTestSubgraphData +} from './fixtures/subgraphHelpers' -describe("Subgraph Construction", () => { - it("should create a subgraph with minimal data", () => { +describe('Subgraph Construction', () => { + it('should create a subgraph with minimal data', () => { const subgraph = createTestSubgraph() assertSubgraphStructure(subgraph, { inputCount: 0, outputCount: 0, nodeCount: 0, - name: "Test Subgraph", + name: 'Test Subgraph' }) - expect(subgraph.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + expect(subgraph.id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ) expect(subgraph.inputNode).toBeDefined() expect(subgraph.outputNode).toBeDefined() expect(subgraph.inputNode.id).toBe(-10) expect(subgraph.outputNode.id).toBe(-20) }) - it("should require a root graph", () => { + it('should require a root graph', () => { const subgraphData = createTestSubgraphData() expect(() => { // @ts-expect-error Testing invalid null parameter new Subgraph(null, subgraphData) - }).toThrow("Root graph is required") + }).toThrow('Root graph is required') }) - it("should accept custom name and ID", () => { + it('should accept custom name and ID', () => { const customId = createUuidv4() - const customName = "My Custom Subgraph" + const customName = 'My Custom Subgraph' const subgraph = createTestSubgraph({ id: customId, - name: customName, + name: customName }) expect(subgraph.id).toBe(customId) expect(subgraph.name).toBe(customName) }) - it("should initialize with empty inputs and outputs", () => { + it('should initialize with empty inputs and outputs', () => { const subgraph = createTestSubgraph() expect(subgraph.inputs).toHaveLength(0) @@ -67,7 +68,7 @@ describe("Subgraph Construction", () => { expect(subgraph.widgets).toHaveLength(0) }) - it("should have properly configured input and output nodes", () => { + it('should have properly configured input and output nodes', () => { const subgraph = createTestSubgraph() // Input node should be positioned on the left @@ -82,50 +83,56 @@ describe("Subgraph Construction", () => { }) }) -describe("Subgraph Input/Output Management", () => { - subgraphTest("should add a single input", ({ emptySubgraph }) => { - const input = emptySubgraph.addInput("test_input", "number") +describe('Subgraph Input/Output Management', () => { + subgraphTest('should add a single input', ({ emptySubgraph }) => { + const input = emptySubgraph.addInput('test_input', 'number') expect(emptySubgraph.inputs).toHaveLength(1) - expect(input.name).toBe("test_input") - expect(input.type).toBe("number") + expect(input.name).toBe('test_input') + expect(input.type).toBe('number') expect(emptySubgraph.inputs.indexOf(input)).toBe(0) }) - subgraphTest("should add a single output", ({ emptySubgraph }) => { - const output = emptySubgraph.addOutput("test_output", "string") + subgraphTest('should add a single output', ({ emptySubgraph }) => { + const output = emptySubgraph.addOutput('test_output', 'string') expect(emptySubgraph.outputs).toHaveLength(1) - expect(output.name).toBe("test_output") - expect(output.type).toBe("string") + expect(output.name).toBe('test_output') + expect(output.type).toBe('string') expect(emptySubgraph.outputs.indexOf(output)).toBe(0) }) - subgraphTest("should maintain correct indices when adding multiple inputs", ({ emptySubgraph }) => { - const input1 = emptySubgraph.addInput("input_1", "number") - const input2 = emptySubgraph.addInput("input_2", "string") - const input3 = emptySubgraph.addInput("input_3", "boolean") + subgraphTest( + 'should maintain correct indices when adding multiple inputs', + ({ emptySubgraph }) => { + const input1 = emptySubgraph.addInput('input_1', 'number') + const input2 = emptySubgraph.addInput('input_2', 'string') + const input3 = emptySubgraph.addInput('input_3', 'boolean') - expect(emptySubgraph.inputs.indexOf(input1)).toBe(0) - expect(emptySubgraph.inputs.indexOf(input2)).toBe(1) - expect(emptySubgraph.inputs.indexOf(input3)).toBe(2) - expect(emptySubgraph.inputs).toHaveLength(3) - }) + expect(emptySubgraph.inputs.indexOf(input1)).toBe(0) + expect(emptySubgraph.inputs.indexOf(input2)).toBe(1) + expect(emptySubgraph.inputs.indexOf(input3)).toBe(2) + expect(emptySubgraph.inputs).toHaveLength(3) + } + ) - subgraphTest("should maintain correct indices when adding multiple outputs", ({ emptySubgraph }) => { - const output1 = emptySubgraph.addOutput("output_1", "number") - const output2 = emptySubgraph.addOutput("output_2", "string") - const output3 = emptySubgraph.addOutput("output_3", "boolean") + subgraphTest( + 'should maintain correct indices when adding multiple outputs', + ({ emptySubgraph }) => { + const output1 = emptySubgraph.addOutput('output_1', 'number') + const output2 = emptySubgraph.addOutput('output_2', 'string') + const output3 = emptySubgraph.addOutput('output_3', 'boolean') - expect(emptySubgraph.outputs.indexOf(output1)).toBe(0) - expect(emptySubgraph.outputs.indexOf(output2)).toBe(1) - expect(emptySubgraph.outputs.indexOf(output3)).toBe(2) - expect(emptySubgraph.outputs).toHaveLength(3) - }) + expect(emptySubgraph.outputs.indexOf(output1)).toBe(0) + expect(emptySubgraph.outputs.indexOf(output2)).toBe(1) + expect(emptySubgraph.outputs.indexOf(output3)).toBe(2) + expect(emptySubgraph.outputs).toHaveLength(3) + } + ) - subgraphTest("should remove inputs correctly", ({ simpleSubgraph }) => { + subgraphTest('should remove inputs correctly', ({ simpleSubgraph }) => { // Add a second input first - simpleSubgraph.addInput("second_input", "string") + simpleSubgraph.addInput('second_input', 'string') expect(simpleSubgraph.inputs).toHaveLength(2) // Remove the first input @@ -133,14 +140,14 @@ describe("Subgraph Input/Output Management", () => { simpleSubgraph.removeInput(firstInput) expect(simpleSubgraph.inputs).toHaveLength(1) - expect(simpleSubgraph.inputs[0].name).toBe("second_input") + expect(simpleSubgraph.inputs[0].name).toBe('second_input') // Verify it's at index 0 in the array expect(simpleSubgraph.inputs.indexOf(simpleSubgraph.inputs[0])).toBe(0) }) - subgraphTest("should remove outputs correctly", ({ simpleSubgraph }) => { + subgraphTest('should remove outputs correctly', ({ simpleSubgraph }) => { // Add a second output first - simpleSubgraph.addOutput("second_output", "string") + simpleSubgraph.addOutput('second_output', 'string') expect(simpleSubgraph.outputs).toHaveLength(2) // Remove the first output @@ -148,48 +155,54 @@ describe("Subgraph Input/Output Management", () => { simpleSubgraph.removeOutput(firstOutput) expect(simpleSubgraph.outputs).toHaveLength(1) - expect(simpleSubgraph.outputs[0].name).toBe("second_output") + expect(simpleSubgraph.outputs[0].name).toBe('second_output') // Verify it's at index 0 in the array expect(simpleSubgraph.outputs.indexOf(simpleSubgraph.outputs[0])).toBe(0) }) }) -describe("Subgraph Serialization", () => { - subgraphTest("should serialize empty subgraph", ({ emptySubgraph }) => { +describe('Subgraph Serialization', () => { + subgraphTest('should serialize empty subgraph', ({ emptySubgraph }) => { const serialized = emptySubgraph.asSerialisable() expect(serialized.version).toBe(1) expect(serialized.id).toBeTruthy() - expect(serialized.name).toBe("Empty Test Subgraph") + expect(serialized.name).toBe('Empty Test Subgraph') expect(serialized.inputs).toHaveLength(0) expect(serialized.outputs).toHaveLength(0) expect(serialized.nodes).toHaveLength(0) - expect(typeof serialized.links).toBe("object") + expect(typeof serialized.links).toBe('object') }) - subgraphTest("should serialize subgraph with inputs and outputs", ({ simpleSubgraph }) => { - const serialized = simpleSubgraph.asSerialisable() + subgraphTest( + 'should serialize subgraph with inputs and outputs', + ({ simpleSubgraph }) => { + const serialized = simpleSubgraph.asSerialisable() - expect(serialized.inputs).toHaveLength(1) - expect(serialized.outputs).toHaveLength(1) - expect(serialized.inputs[0].name).toBe("input") - expect(serialized.inputs[0].type).toBe("number") - expect(serialized.outputs[0].name).toBe("output") - expect(serialized.outputs[0].type).toBe("number") - }) + expect(serialized.inputs).toHaveLength(1) + expect(serialized.outputs).toHaveLength(1) + expect(serialized.inputs[0].name).toBe('input') + expect(serialized.inputs[0].type).toBe('number') + expect(serialized.outputs[0].name).toBe('output') + expect(serialized.outputs[0].type).toBe('number') + } + ) - subgraphTest("should include input and output nodes in serialization", ({ emptySubgraph }) => { - const serialized = emptySubgraph.asSerialisable() + subgraphTest( + 'should include input and output nodes in serialization', + ({ emptySubgraph }) => { + const serialized = emptySubgraph.asSerialisable() - expect(serialized.inputNode).toBeDefined() - expect(serialized.outputNode).toBeDefined() - expect(serialized.inputNode.id).toBe(-10) - expect(serialized.outputNode.id).toBe(-20) - }) + expect(serialized.inputNode).toBeDefined() + expect(serialized.outputNode).toBeDefined() + expect(serialized.inputNode.id).toBe(-10) + expect(serialized.outputNode.id).toBe(-20) + } + ) }) -describe("Subgraph Known Issues", () => { - it.todo("should enforce MAX_NESTED_SUBGRAPHS limit", () => { +describe('Subgraph Known Issues', () => { + it.todo('should enforce MAX_NESTED_SUBGRAPHS limit', () => { // This test documents that MAX_NESTED_SUBGRAPHS = 1000 is defined // but not actually enforced anywhere in the code. // @@ -199,24 +212,24 @@ describe("Subgraph Known Issues", () => { // This safety limit should be implemented to prevent runaway recursion. }) - it("should provide MAX_NESTED_SUBGRAPHS constant", () => { + it('should provide MAX_NESTED_SUBGRAPHS constant', () => { expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBe(1000) }) - it("should have recursion detection in place", () => { + it('should have recursion detection in place', () => { // Verify that RecursionError is available and can be thrown expect(() => { - throw new RecursionError("test recursion") + throw new RecursionError('test recursion') }).toThrow(RecursionError) expect(() => { - throw new RecursionError("test recursion") - }).toThrow("test recursion") + throw new RecursionError('test recursion') + }).toThrow('test recursion') }) }) -describe("Subgraph Root Graph Relationship", () => { - it("should maintain reference to root graph", () => { +describe('Subgraph Root Graph Relationship', () => { + it('should maintain reference to root graph', () => { const rootGraph = new LGraph() const subgraphData = createTestSubgraphData() const subgraph = new Subgraph(rootGraph, subgraphData) @@ -224,16 +237,16 @@ describe("Subgraph Root Graph Relationship", () => { expect(subgraph.rootGraph).toBe(rootGraph) }) - it("should inherit root graph in nested subgraphs", () => { + it('should inherit root graph in nested subgraphs', () => { const rootGraph = new LGraph() const parentData = createTestSubgraphData({ - name: "Parent Subgraph", + name: 'Parent Subgraph' }) const parentSubgraph = new Subgraph(rootGraph, parentData) // Create a nested subgraph const nestedData = createTestSubgraphData({ - name: "Nested Subgraph", + name: 'Nested Subgraph' }) const nestedSubgraph = new Subgraph(rootGraph, nestedData) @@ -242,34 +255,40 @@ describe("Subgraph Root Graph Relationship", () => { }) }) -describe("Subgraph Error Handling", () => { - subgraphTest("should handle removing non-existent input gracefully", ({ emptySubgraph }) => { - // Create a fake input that doesn't belong to this subgraph - const fakeInput = emptySubgraph.addInput("temp", "number") - emptySubgraph.removeInput(fakeInput) // Remove it first +describe('Subgraph Error Handling', () => { + subgraphTest( + 'should handle removing non-existent input gracefully', + ({ emptySubgraph }) => { + // Create a fake input that doesn't belong to this subgraph + const fakeInput = emptySubgraph.addInput('temp', 'number') + emptySubgraph.removeInput(fakeInput) // Remove it first - // Now try to remove it again - expect(() => { - emptySubgraph.removeInput(fakeInput) - }).toThrow("Input not found") - }) + // Now try to remove it again + expect(() => { + emptySubgraph.removeInput(fakeInput) + }).toThrow('Input not found') + } + ) - subgraphTest("should handle removing non-existent output gracefully", ({ emptySubgraph }) => { - // Create a fake output that doesn't belong to this subgraph - const fakeOutput = emptySubgraph.addOutput("temp", "number") - emptySubgraph.removeOutput(fakeOutput) // Remove it first + subgraphTest( + 'should handle removing non-existent output gracefully', + ({ emptySubgraph }) => { + // Create a fake output that doesn't belong to this subgraph + const fakeOutput = emptySubgraph.addOutput('temp', 'number') + emptySubgraph.removeOutput(fakeOutput) // Remove it first - // Now try to remove it again - expect(() => { - emptySubgraph.removeOutput(fakeOutput) - }).toThrow("Output not found") - }) + // Now try to remove it again + expect(() => { + emptySubgraph.removeOutput(fakeOutput) + }).toThrow('Output not found') + } + ) }) -describe("Subgraph Integration", () => { +describe('Subgraph Integration', () => { it("should work with LGraph's node management", () => { const subgraph = createTestSubgraph({ - nodeCount: 3, + nodeCount: 3 }) // Verify nodes were added to the subgraph @@ -278,12 +297,12 @@ describe("Subgraph Integration", () => { // Verify we can access nodes by ID const firstNode = subgraph.getNodeById(1) expect(firstNode).toBeDefined() - expect(firstNode?.title).toContain("Test Node") + expect(firstNode?.title).toContain('Test Node') }) - it("should maintain link integrity", () => { + it('should maintain link integrity', () => { const subgraph = createTestSubgraph({ - nodeCount: 2, + nodeCount: 2 }) const node1 = subgraph.nodes[0] diff --git a/src/lib/litegraph/test/subgraph/SubgraphEdgeCases.test.ts b/src/lib/litegraph/test/subgraph/SubgraphEdgeCases.test.ts index 85ee9748e2..c4b86cee42 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphEdgeCases.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphEdgeCases.test.ts @@ -4,21 +4,20 @@ * Tests for edge cases, error handling, and boundary conditions in the subgraph system. * This covers unusual scenarios, invalid states, and stress testing. */ +import { describe, expect, it } from 'vitest' -import { describe, expect, it } from "vitest" - -import { LGraph, LGraphNode, Subgraph } from "@/lib/litegraph/src/litegraph" +import { LGraph, LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph' import { createNestedSubgraphs, createTestSubgraph, - createTestSubgraphNode, -} from "./fixtures/subgraphHelpers" + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("SubgraphEdgeCases - Recursion Detection", () => { - it("should handle circular subgraph references without crashing", () => { - const sub1 = createTestSubgraph({ name: "Sub1" }) - const sub2 = createTestSubgraph({ name: "Sub2" }) +describe('SubgraphEdgeCases - Recursion Detection', () => { + it('should handle circular subgraph references without crashing', () => { + const sub1 = createTestSubgraph({ name: 'Sub1' }) + const sub2 = createTestSubgraph({ name: 'Sub2' }) // Create circular reference const node1 = createTestSubgraphNode(sub1, { id: 1 }) @@ -34,7 +33,7 @@ describe("SubgraphEdgeCases - Recursion Detection", () => { }).toThrow(/Node \[\d+\] not found/) // Current behavior: path resolution fails }) - it("should handle deep nesting scenarios", () => { + it('should handle deep nesting scenarios', () => { // Test with reasonable depth to avoid timeout const nested = createNestedSubgraphs({ depth: 10, nodesPerLevel: 1 }) @@ -48,7 +47,7 @@ describe("SubgraphEdgeCases - Recursion Detection", () => { expect(firstLevel.isSubgraphNode()).toBe(true) }) - it.todo("should use WeakSet for cycle detection", () => { + it.todo('should use WeakSet for cycle detection', () => { // TODO: This test is currently skipped because cycle detection has a bug // The fix is to pass 'visited' directly instead of 'new Set(visited)' in SubgraphNode.ts:299 const subgraph = createTestSubgraph({ nodeCount: 1 }) @@ -64,10 +63,10 @@ describe("SubgraphEdgeCases - Recursion Detection", () => { }).toThrow(/while flattening subgraph/i) }) - it("should respect MAX_NESTED_SUBGRAPHS constant", () => { + it('should respect MAX_NESTED_SUBGRAPHS constant', () => { // Verify the constant exists and is a reasonable positive number expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeDefined() - expect(typeof Subgraph.MAX_NESTED_SUBGRAPHS).toBe("number") + expect(typeof Subgraph.MAX_NESTED_SUBGRAPHS).toBe('number') expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeGreaterThan(0) expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBeLessThanOrEqual(10_000) // Reasonable upper bound @@ -76,10 +75,14 @@ describe("SubgraphEdgeCases - Recursion Detection", () => { }) }) -describe("SubgraphEdgeCases - Invalid States", () => { - it("should handle removing non-existent inputs gracefully", () => { +describe('SubgraphEdgeCases - Invalid States', () => { + it('should handle removing non-existent inputs gracefully', () => { const subgraph = createTestSubgraph() - const fakeInput = { name: "fake", type: "number", disconnect: () => {} } as any + const fakeInput = { + name: 'fake', + type: 'number', + disconnect: () => {} + } as any // Should throw appropriate error for non-existent input expect(() => { @@ -87,92 +90,96 @@ describe("SubgraphEdgeCases - Invalid States", () => { }).toThrow(/Input not found/) // Expected error }) - it("should handle removing non-existent outputs gracefully", () => { + it('should handle removing non-existent outputs gracefully', () => { const subgraph = createTestSubgraph() - const fakeOutput = { name: "fake", type: "number", disconnect: () => {} } as any + const fakeOutput = { + name: 'fake', + type: 'number', + disconnect: () => {} + } as any expect(() => { subgraph.removeOutput(fakeOutput) }).toThrow(/Output not found/) // Expected error }) - it("should handle null/undefined input names", () => { + it('should handle null/undefined input names', () => { const subgraph = createTestSubgraph() // ISSUE: Current implementation allows null/undefined names which may cause runtime errors // TODO: Consider adding validation to prevent null/undefined names // This test documents the current permissive behavior expect(() => { - subgraph.addInput(null as any, "number") + subgraph.addInput(null as any, 'number') }).not.toThrow() // Current behavior: allows null expect(() => { - subgraph.addInput(undefined as any, "number") + subgraph.addInput(undefined as any, 'number') }).not.toThrow() // Current behavior: allows undefined }) - it("should handle null/undefined output names", () => { + it('should handle null/undefined output names', () => { const subgraph = createTestSubgraph() // ISSUE: Current implementation allows null/undefined names which may cause runtime errors // TODO: Consider adding validation to prevent null/undefined names // This test documents the current permissive behavior expect(() => { - subgraph.addOutput(null as any, "number") + subgraph.addOutput(null as any, 'number') }).not.toThrow() // Current behavior: allows null expect(() => { - subgraph.addOutput(undefined as any, "number") + subgraph.addOutput(undefined as any, 'number') }).not.toThrow() // Current behavior: allows undefined }) - it("should handle empty string names", () => { + it('should handle empty string names', () => { const subgraph = createTestSubgraph() // Current implementation may allow empty strings // Document the actual behavior expect(() => { - subgraph.addInput("", "number") + subgraph.addInput('', 'number') }).not.toThrow() // Current behavior: allows empty strings expect(() => { - subgraph.addOutput("", "number") + subgraph.addOutput('', 'number') }).not.toThrow() // Current behavior: allows empty strings }) - it("should handle undefined types gracefully", () => { + it('should handle undefined types gracefully', () => { const subgraph = createTestSubgraph() // Undefined type should not crash but may have default behavior expect(() => { - subgraph.addInput("test", undefined as any) + subgraph.addInput('test', undefined as any) }).not.toThrow() expect(() => { - subgraph.addOutput("test", undefined as any) + subgraph.addOutput('test', undefined as any) }).not.toThrow() }) - it("should handle duplicate slot names", () => { + it('should handle duplicate slot names', () => { const subgraph = createTestSubgraph() // Add first input - subgraph.addInput("duplicate", "number") + subgraph.addInput('duplicate', 'number') // Adding duplicate should not crash (current behavior allows it) expect(() => { - subgraph.addInput("duplicate", "string") + subgraph.addInput('duplicate', 'string') }).not.toThrow() // Should now have 2 inputs with same name expect(subgraph.inputs.length).toBe(2) - expect(subgraph.inputs[0].name).toBe("duplicate") - expect(subgraph.inputs[1].name).toBe("duplicate") + expect(subgraph.inputs[0].name).toBe('duplicate') + expect(subgraph.inputs[1].name).toBe('duplicate') }) }) -describe("SubgraphEdgeCases - Boundary Conditions", () => { - it("should handle empty subgraphs (no nodes, no IO)", () => { +describe('SubgraphEdgeCases - Boundary Conditions', () => { + it('should handle empty subgraphs (no nodes, no IO)', () => { const subgraph = createTestSubgraph({ nodeCount: 0 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -185,32 +192,32 @@ describe("SubgraphEdgeCases - Boundary Conditions", () => { expect(subgraph.outputs).toHaveLength(0) }) - it("should handle single input/output subgraphs", () => { + it('should handle single input/output subgraphs', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "single_in", type: "number" }], - outputs: [{ name: "single_out", type: "number" }], - nodeCount: 1, + inputs: [{ name: 'single_in', type: 'number' }], + outputs: [{ name: 'single_out', type: 'number' }], + nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph) expect(subgraphNode.inputs).toHaveLength(1) expect(subgraphNode.outputs).toHaveLength(1) - expect(subgraphNode.inputs[0].name).toBe("single_in") - expect(subgraphNode.outputs[0].name).toBe("single_out") + expect(subgraphNode.inputs[0].name).toBe('single_in') + expect(subgraphNode.outputs[0].name).toBe('single_out') }) - it("should handle subgraphs with many slots", () => { + it('should handle subgraphs with many slots', () => { const subgraph = createTestSubgraph({ nodeCount: 1 }) // Add many inputs (test with 20 to keep test fast) for (let i = 0; i < 20; i++) { - subgraph.addInput(`input_${i}`, "number") + subgraph.addInput(`input_${i}`, 'number') } // Add many outputs for (let i = 0; i < 20; i++) { - subgraph.addOutput(`output_${i}`, "number") + subgraph.addOutput(`output_${i}`, 'number') } const subgraphNode = createTestSubgraphNode(subgraph) @@ -226,26 +233,26 @@ describe("SubgraphEdgeCases - Boundary Conditions", () => { expect(flattened).toHaveLength(1) // Original node count }) - it("should handle very long slot names", () => { + it('should handle very long slot names', () => { const subgraph = createTestSubgraph() - const longName = "a".repeat(1000) // 1000 character name + const longName = 'a'.repeat(1000) // 1000 character name expect(() => { - subgraph.addInput(longName, "number") - subgraph.addOutput(longName, "string") + subgraph.addInput(longName, 'number') + subgraph.addOutput(longName, 'string') }).not.toThrow() expect(subgraph.inputs[0].name).toBe(longName) expect(subgraph.outputs[0].name).toBe(longName) }) - it("should handle Unicode characters in names", () => { + it('should handle Unicode characters in names', () => { const subgraph = createTestSubgraph() - const unicodeName = "测试_🚀_تست_тест" + const unicodeName = '测试_🚀_تست_тест' expect(() => { - subgraph.addInput(unicodeName, "number") - subgraph.addOutput(unicodeName, "string") + subgraph.addInput(unicodeName, 'number') + subgraph.addOutput(unicodeName, 'string') }).not.toThrow() expect(subgraph.inputs[0].name).toBe(unicodeName) @@ -253,17 +260,17 @@ describe("SubgraphEdgeCases - Boundary Conditions", () => { }) }) -describe("SubgraphEdgeCases - Type Validation", () => { - it("should allow connecting mismatched types (no validation currently)", () => { +describe('SubgraphEdgeCases - Type Validation', () => { + it('should allow connecting mismatched types (no validation currently)', () => { const rootGraph = new LGraph() const subgraph = createTestSubgraph() - subgraph.addInput("num", "number") - subgraph.addOutput("str", "string") + subgraph.addInput('num', 'number') + subgraph.addOutput('str', 'string') // Create a basic node manually since createNode is not available - const numberNode = new LGraphNode("basic/const") - numberNode.addOutput("value", "number") + const numberNode = new LGraphNode('basic/const') + numberNode.addOutput('value', 'number') rootGraph.add(numberNode) const subgraphNode = createTestSubgraphNode(subgraph) @@ -275,36 +282,36 @@ describe("SubgraphEdgeCases - Type Validation", () => { }).not.toThrow() }) - it("should handle invalid type strings", () => { + it('should handle invalid type strings', () => { const subgraph = createTestSubgraph() // These should not crash (current behavior) expect(() => { - subgraph.addInput("test1", "invalid_type") - subgraph.addInput("test2", "") - subgraph.addInput("test3", "123") - subgraph.addInput("test4", "special!@#$%") + subgraph.addInput('test1', 'invalid_type') + subgraph.addInput('test2', '') + subgraph.addInput('test3', '123') + subgraph.addInput('test4', 'special!@#$%') }).not.toThrow() }) - it("should handle complex type strings", () => { + it('should handle complex type strings', () => { const subgraph = createTestSubgraph() expect(() => { - subgraph.addInput("array", "array") - subgraph.addInput("object", "object<{x: number, y: string}>") - subgraph.addInput("union", "number|string") + subgraph.addInput('array', 'array') + subgraph.addInput('object', 'object<{x: number, y: string}>') + subgraph.addInput('union', 'number|string') }).not.toThrow() expect(subgraph.inputs).toHaveLength(3) - expect(subgraph.inputs[0].type).toBe("array") - expect(subgraph.inputs[1].type).toBe("object<{x: number, y: string}>") - expect(subgraph.inputs[2].type).toBe("number|string") + expect(subgraph.inputs[0].type).toBe('array') + expect(subgraph.inputs[1].type).toBe('object<{x: number, y: string}>') + expect(subgraph.inputs[2].type).toBe('number|string') }) }) -describe("SubgraphEdgeCases - Performance and Scale", () => { - it("should handle large numbers of nodes in subgraph", () => { +describe('SubgraphEdgeCases - Performance and Scale', () => { + it('should handle large numbers of nodes in subgraph', () => { // Create subgraph with many nodes (keep reasonable for test speed) const subgraph = createTestSubgraph({ nodeCount: 50 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -317,13 +324,13 @@ describe("SubgraphEdgeCases - Performance and Scale", () => { // Performance is acceptable for 50 nodes (typically < 1ms) }) - it("should handle rapid IO changes", () => { + it('should handle rapid IO changes', () => { const subgraph = createTestSubgraph() // Rapidly add and remove inputs/outputs for (let i = 0; i < 10; i++) { - const input = subgraph.addInput(`rapid_${i}`, "number") - const output = subgraph.addOutput(`rapid_${i}`, "number") + const input = subgraph.addInput(`rapid_${i}`, 'number') + const output = subgraph.addOutput(`rapid_${i}`, 'number') // Remove them immediately subgraph.removeInput(input) @@ -335,7 +342,7 @@ describe("SubgraphEdgeCases - Performance and Scale", () => { expect(subgraph.outputs).toHaveLength(0) }) - it("should handle concurrent modifications safely", () => { + it('should handle concurrent modifications safely', () => { // This test ensures the system doesn't crash under concurrent access // Note: JavaScript is single-threaded, so this tests rapid sequential access const subgraph = createTestSubgraph({ nodeCount: 5 }) @@ -344,16 +351,20 @@ describe("SubgraphEdgeCases - Performance and Scale", () => { // Simulate concurrent operations const operations = [] for (let i = 0; i < 20; i++) { - operations.push(() => { - const executableNodes = new Map() - subgraphNode.getInnerNodes(executableNodes) - }, () => { - subgraph.addInput(`concurrent_${i}`, "number") - }, () => { - if (subgraph.inputs.length > 0) { - subgraph.removeInput(subgraph.inputs[0]) + operations.push( + () => { + const executableNodes = new Map() + subgraphNode.getInnerNodes(executableNodes) + }, + () => { + subgraph.addInput(`concurrent_${i}`, 'number') + }, + () => { + if (subgraph.inputs.length > 0) { + subgraph.removeInput(subgraph.inputs[0]) + } } - }) + ) } // Execute all operations - should not crash diff --git a/src/lib/litegraph/test/subgraph/SubgraphEvents.test.ts b/src/lib/litegraph/test/subgraph/SubgraphEvents.test.ts index ef73ec32d5..46f50e2b0f 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphEvents.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphEvents.test.ts @@ -1,389 +1,440 @@ -import { describe, expect, vi } from "vitest" +import { describe, expect, vi } from 'vitest' -import { subgraphTest } from "./fixtures/subgraphFixtures" -import { verifyEventSequence } from "./fixtures/subgraphHelpers" +import { subgraphTest } from './fixtures/subgraphFixtures' +import { verifyEventSequence } from './fixtures/subgraphHelpers' -describe("SubgraphEvents - Event Payload Verification", () => { - subgraphTest("dispatches input-added with correct payload", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture +describe('SubgraphEvents - Event Payload Verification', () => { + subgraphTest( + 'dispatches input-added with correct payload', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - const input = subgraph.addInput("test_input", "number") + const input = subgraph.addInput('test_input', 'number') - const addedEvents = capture.getEventsByType("input-added") - expect(addedEvents).toHaveLength(1) + const addedEvents = capture.getEventsByType('input-added') + expect(addedEvents).toHaveLength(1) - expect(addedEvents[0].detail).toEqual({ - input: expect.objectContaining({ - name: "test_input", - type: "number", - }), - }) + expect(addedEvents[0].detail).toEqual({ + input: expect.objectContaining({ + name: 'test_input', + type: 'number' + }) + }) - expect(addedEvents[0].detail.input).toBe(input) - }) + expect(addedEvents[0].detail.input).toBe(input) + } + ) - subgraphTest("dispatches output-added with correct payload", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'dispatches output-added with correct payload', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - const output = subgraph.addOutput("test_output", "string") + const output = subgraph.addOutput('test_output', 'string') - const addedEvents = capture.getEventsByType("output-added") - expect(addedEvents).toHaveLength(1) + const addedEvents = capture.getEventsByType('output-added') + expect(addedEvents).toHaveLength(1) - expect(addedEvents[0].detail).toEqual({ - output: expect.objectContaining({ - name: "test_output", - type: "string", - }), - }) + expect(addedEvents[0].detail).toEqual({ + output: expect.objectContaining({ + name: 'test_output', + type: 'string' + }) + }) - expect(addedEvents[0].detail.output).toBe(output) - }) + expect(addedEvents[0].detail.output).toBe(output) + } + ) - subgraphTest("dispatches removing-input with correct payload", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'dispatches removing-input with correct payload', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - const input = subgraph.addInput("to_remove", "boolean") + const input = subgraph.addInput('to_remove', 'boolean') - capture.clear() + capture.clear() - subgraph.removeInput(input) + subgraph.removeInput(input) - const removingEvents = capture.getEventsByType("removing-input") - expect(removingEvents).toHaveLength(1) + const removingEvents = capture.getEventsByType('removing-input') + expect(removingEvents).toHaveLength(1) - expect(removingEvents[0].detail).toEqual({ - input: expect.objectContaining({ - name: "to_remove", - type: "boolean", - }), - index: 0, - }) + expect(removingEvents[0].detail).toEqual({ + input: expect.objectContaining({ + name: 'to_remove', + type: 'boolean' + }), + index: 0 + }) - expect(removingEvents[0].detail.input).toBe(input) - }) + expect(removingEvents[0].detail.input).toBe(input) + } + ) - subgraphTest("dispatches removing-output with correct payload", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'dispatches removing-output with correct payload', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - const output = subgraph.addOutput("to_remove", "number") + const output = subgraph.addOutput('to_remove', 'number') - capture.clear() + capture.clear() - subgraph.removeOutput(output) + subgraph.removeOutput(output) - const removingEvents = capture.getEventsByType("removing-output") - expect(removingEvents).toHaveLength(1) + const removingEvents = capture.getEventsByType('removing-output') + expect(removingEvents).toHaveLength(1) - expect(removingEvents[0].detail).toEqual({ - output: expect.objectContaining({ - name: "to_remove", - type: "number", - }), - index: 0, - }) + expect(removingEvents[0].detail).toEqual({ + output: expect.objectContaining({ + name: 'to_remove', + type: 'number' + }), + index: 0 + }) - expect(removingEvents[0].detail.output).toBe(output) - }) + expect(removingEvents[0].detail.output).toBe(output) + } + ) - subgraphTest("dispatches renaming-input with correct payload", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'dispatches renaming-input with correct payload', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - const input = subgraph.addInput("old_name", "string") + const input = subgraph.addInput('old_name', 'string') - capture.clear() + capture.clear() - subgraph.renameInput(input, "new_name") + subgraph.renameInput(input, 'new_name') - const renamingEvents = capture.getEventsByType("renaming-input") - expect(renamingEvents).toHaveLength(1) + const renamingEvents = capture.getEventsByType('renaming-input') + expect(renamingEvents).toHaveLength(1) - expect(renamingEvents[0].detail).toEqual({ - input: expect.objectContaining({ - type: "string", - }), - index: 0, - oldName: "old_name", - newName: "new_name", - }) + expect(renamingEvents[0].detail).toEqual({ + input: expect.objectContaining({ + type: 'string' + }), + index: 0, + oldName: 'old_name', + newName: 'new_name' + }) - expect(renamingEvents[0].detail.input).toBe(input) + expect(renamingEvents[0].detail.input).toBe(input) - // Verify the label was updated after the event (renameInput sets label, not name) - expect(input.label).toBe("new_name") - expect(input.displayName).toBe("new_name") - expect(input.name).toBe("old_name") - }) + // Verify the label was updated after the event (renameInput sets label, not name) + expect(input.label).toBe('new_name') + expect(input.displayName).toBe('new_name') + expect(input.name).toBe('old_name') + } + ) - subgraphTest("dispatches renaming-output with correct payload", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'dispatches renaming-output with correct payload', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - const output = subgraph.addOutput("old_name", "number") + const output = subgraph.addOutput('old_name', 'number') - capture.clear() + capture.clear() - subgraph.renameOutput(output, "new_name") + subgraph.renameOutput(output, 'new_name') - const renamingEvents = capture.getEventsByType("renaming-output") - expect(renamingEvents).toHaveLength(1) + const renamingEvents = capture.getEventsByType('renaming-output') + expect(renamingEvents).toHaveLength(1) - expect(renamingEvents[0].detail).toEqual({ - output: expect.objectContaining({ - name: "old_name", // Should still have the old name when event is dispatched - type: "number", - }), - index: 0, - oldName: "old_name", - newName: "new_name", - }) + expect(renamingEvents[0].detail).toEqual({ + output: expect.objectContaining({ + name: 'old_name', // Should still have the old name when event is dispatched + type: 'number' + }), + index: 0, + oldName: 'old_name', + newName: 'new_name' + }) - expect(renamingEvents[0].detail.output).toBe(output) + expect(renamingEvents[0].detail.output).toBe(output) - // Verify the label was updated after the event - expect(output.label).toBe("new_name") - expect(output.displayName).toBe("new_name") - expect(output.name).toBe("old_name") - }) + // Verify the label was updated after the event + expect(output.label).toBe('new_name') + expect(output.displayName).toBe('new_name') + expect(output.name).toBe('old_name') + } + ) - subgraphTest("dispatches adding-input with correct payload", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'dispatches adding-input with correct payload', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - subgraph.addInput("test_input", "number") + subgraph.addInput('test_input', 'number') - const addingEvents = capture.getEventsByType("adding-input") - expect(addingEvents).toHaveLength(1) + const addingEvents = capture.getEventsByType('adding-input') + expect(addingEvents).toHaveLength(1) - expect(addingEvents[0].detail).toEqual({ - name: "test_input", - type: "number", - }) - }) + expect(addingEvents[0].detail).toEqual({ + name: 'test_input', + type: 'number' + }) + } + ) - subgraphTest("dispatches adding-output with correct payload", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'dispatches adding-output with correct payload', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - subgraph.addOutput("test_output", "string") + subgraph.addOutput('test_output', 'string') - const addingEvents = capture.getEventsByType("adding-output") - expect(addingEvents).toHaveLength(1) + const addingEvents = capture.getEventsByType('adding-output') + expect(addingEvents).toHaveLength(1) - expect(addingEvents[0].detail).toEqual({ - name: "test_output", - type: "string", - }) - }) + expect(addingEvents[0].detail).toEqual({ + name: 'test_output', + type: 'string' + }) + } + ) }) -describe("SubgraphEvents - Event Handler Isolation", () => { - subgraphTest("continues dispatching if handler throws", ({ emptySubgraph }) => { - const handler1 = vi.fn(() => { - throw new Error("Handler 1 error") - }) - const handler2 = vi.fn() - const handler3 = vi.fn() +describe('SubgraphEvents - Event Handler Isolation', () => { + subgraphTest( + 'continues dispatching if handler throws', + ({ emptySubgraph }) => { + const handler1 = vi.fn(() => { + throw new Error('Handler 1 error') + }) + const handler2 = vi.fn() + const handler3 = vi.fn() - emptySubgraph.events.addEventListener("input-added", handler1) - emptySubgraph.events.addEventListener("input-added", handler2) - emptySubgraph.events.addEventListener("input-added", handler3) + emptySubgraph.events.addEventListener('input-added', handler1) + emptySubgraph.events.addEventListener('input-added', handler2) + emptySubgraph.events.addEventListener('input-added', handler3) - // The operation itself should not throw (error is isolated) - expect(() => { - emptySubgraph.addInput("test", "number") - }).not.toThrow() + // The operation itself should not throw (error is isolated) + expect(() => { + emptySubgraph.addInput('test', 'number') + }).not.toThrow() - // Verify all handlers were called despite the first one throwing - expect(handler1).toHaveBeenCalled() - expect(handler2).toHaveBeenCalled() - expect(handler3).toHaveBeenCalled() + // Verify all handlers were called despite the first one throwing + expect(handler1).toHaveBeenCalled() + expect(handler2).toHaveBeenCalled() + expect(handler3).toHaveBeenCalled() - // Verify the throwing handler actually received the event - expect(handler1).toHaveBeenCalledWith(expect.objectContaining({ - type: "input-added", - })) + // Verify the throwing handler actually received the event + expect(handler1).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'input-added' + }) + ) - // Verify other handlers received correct event data - expect(handler2).toHaveBeenCalledWith(expect.objectContaining({ - type: "input-added", - detail: expect.objectContaining({ - input: expect.objectContaining({ - name: "test", - type: "number", - }), - }), - })) - expect(handler3).toHaveBeenCalledWith(expect.objectContaining({ - type: "input-added", - })) - }) + // Verify other handlers received correct event data + expect(handler2).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'input-added', + detail: expect.objectContaining({ + input: expect.objectContaining({ + name: 'test', + type: 'number' + }) + }) + }) + ) + expect(handler3).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'input-added' + }) + ) + } + ) - subgraphTest("maintains handler execution order", ({ emptySubgraph }) => { + subgraphTest('maintains handler execution order', ({ emptySubgraph }) => { const executionOrder: number[] = [] const handler1 = vi.fn(() => executionOrder.push(1)) const handler2 = vi.fn(() => executionOrder.push(2)) const handler3 = vi.fn(() => executionOrder.push(3)) - emptySubgraph.events.addEventListener("input-added", handler1) - emptySubgraph.events.addEventListener("input-added", handler2) - emptySubgraph.events.addEventListener("input-added", handler3) + emptySubgraph.events.addEventListener('input-added', handler1) + emptySubgraph.events.addEventListener('input-added', handler2) + emptySubgraph.events.addEventListener('input-added', handler3) - emptySubgraph.addInput("test", "number") + emptySubgraph.addInput('test', 'number') expect(executionOrder).toEqual([1, 2, 3]) }) - subgraphTest("prevents handler accumulation with proper cleanup", ({ emptySubgraph }) => { - const handler = vi.fn() + subgraphTest( + 'prevents handler accumulation with proper cleanup', + ({ emptySubgraph }) => { + const handler = vi.fn() - for (let i = 0; i < 5; i++) { - emptySubgraph.events.addEventListener("input-added", handler) - emptySubgraph.events.removeEventListener("input-added", handler) + for (let i = 0; i < 5; i++) { + emptySubgraph.events.addEventListener('input-added', handler) + emptySubgraph.events.removeEventListener('input-added', handler) + } + + emptySubgraph.events.addEventListener('input-added', handler) + + emptySubgraph.addInput('test', 'number') + + expect(handler).toHaveBeenCalledTimes(1) } + ) - emptySubgraph.events.addEventListener("input-added", handler) + subgraphTest( + 'supports AbortController cleanup patterns', + ({ emptySubgraph }) => { + const abortController = new AbortController() + const { signal } = abortController - emptySubgraph.addInput("test", "number") + const handler = vi.fn() - expect(handler).toHaveBeenCalledTimes(1) - }) + emptySubgraph.events.addEventListener('input-added', handler, { signal }) - subgraphTest("supports AbortController cleanup patterns", ({ emptySubgraph }) => { - const abortController = new AbortController() - const { signal } = abortController + emptySubgraph.addInput('test1', 'number') + expect(handler).toHaveBeenCalledTimes(1) - const handler = vi.fn() + abortController.abort() - emptySubgraph.events.addEventListener("input-added", handler, { signal }) - - emptySubgraph.addInput("test1", "number") - expect(handler).toHaveBeenCalledTimes(1) - - abortController.abort() - - emptySubgraph.addInput("test2", "number") - expect(handler).toHaveBeenCalledTimes(1) - }) + emptySubgraph.addInput('test2', 'number') + expect(handler).toHaveBeenCalledTimes(1) + } + ) }) -describe("SubgraphEvents - Event Sequence Testing", () => { - subgraphTest("maintains correct event sequence for inputs", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture +describe('SubgraphEvents - Event Sequence Testing', () => { + subgraphTest( + 'maintains correct event sequence for inputs', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - subgraph.addInput("input1", "number") + subgraph.addInput('input1', 'number') - verifyEventSequence(capture.events, [ - "adding-input", - "input-added", - ]) - }) + verifyEventSequence(capture.events, ['adding-input', 'input-added']) + } + ) - subgraphTest("maintains correct event sequence for outputs", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'maintains correct event sequence for outputs', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - subgraph.addOutput("output1", "string") + subgraph.addOutput('output1', 'string') - verifyEventSequence(capture.events, [ - "adding-output", - "output-added", - ]) - }) + verifyEventSequence(capture.events, ['adding-output', 'output-added']) + } + ) - subgraphTest("maintains correct event sequence for rapid operations", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'maintains correct event sequence for rapid operations', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - subgraph.addInput("input1", "number") - subgraph.addInput("input2", "string") - subgraph.addOutput("output1", "boolean") - subgraph.addOutput("output2", "number") + subgraph.addInput('input1', 'number') + subgraph.addInput('input2', 'string') + subgraph.addOutput('output1', 'boolean') + subgraph.addOutput('output2', 'number') - verifyEventSequence(capture.events, [ - "adding-input", - "input-added", - "adding-input", - "input-added", - "adding-output", - "output-added", - "adding-output", - "output-added", - ]) - }) + verifyEventSequence(capture.events, [ + 'adding-input', + 'input-added', + 'adding-input', + 'input-added', + 'adding-output', + 'output-added', + 'adding-output', + 'output-added' + ]) + } + ) - subgraphTest("handles concurrent event handling", ({ eventCapture }) => { + subgraphTest('handles concurrent event handling', ({ eventCapture }) => { const { subgraph, capture } = eventCapture const handler1 = vi.fn(() => { - return new Promise(resolve => setTimeout(resolve, 1)) + return new Promise((resolve) => setTimeout(resolve, 1)) }) const handler2 = vi.fn() const handler3 = vi.fn() - subgraph.events.addEventListener("input-added", handler1) - subgraph.events.addEventListener("input-added", handler2) - subgraph.events.addEventListener("input-added", handler3) + subgraph.events.addEventListener('input-added', handler1) + subgraph.events.addEventListener('input-added', handler2) + subgraph.events.addEventListener('input-added', handler3) - subgraph.addInput("test", "number") + subgraph.addInput('test', 'number') expect(handler1).toHaveBeenCalled() expect(handler2).toHaveBeenCalled() expect(handler3).toHaveBeenCalled() - const addedEvents = capture.getEventsByType("input-added") + const addedEvents = capture.getEventsByType('input-added') expect(addedEvents).toHaveLength(1) }) - subgraphTest("validates event timestamps are properly ordered", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture + subgraphTest( + 'validates event timestamps are properly ordered', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - subgraph.addInput("input1", "number") - subgraph.addInput("input2", "string") - subgraph.addOutput("output1", "boolean") + subgraph.addInput('input1', 'number') + subgraph.addInput('input2', 'string') + subgraph.addOutput('output1', 'boolean') - for (let i = 1; i < capture.events.length; i++) { - expect(capture.events[i].timestamp).toBeGreaterThanOrEqual( - capture.events[i - 1].timestamp, - ) + for (let i = 1; i < capture.events.length; i++) { + expect(capture.events[i].timestamp).toBeGreaterThanOrEqual( + capture.events[i - 1].timestamp + ) + } } - }) + ) }) -describe("SubgraphEvents - Event Cancellation", () => { - subgraphTest("supports preventDefault() for cancellable events", ({ emptySubgraph }) => { - const preventHandler = vi.fn((event: Event) => { - event.preventDefault() - }) +describe('SubgraphEvents - Event Cancellation', () => { + subgraphTest( + 'supports preventDefault() for cancellable events', + ({ emptySubgraph }) => { + const preventHandler = vi.fn((event: Event) => { + event.preventDefault() + }) - emptySubgraph.events.addEventListener("removing-input", preventHandler) + emptySubgraph.events.addEventListener('removing-input', preventHandler) - const input = emptySubgraph.addInput("test", "number") + const input = emptySubgraph.addInput('test', 'number') - emptySubgraph.removeInput(input) + emptySubgraph.removeInput(input) - expect(emptySubgraph.inputs).toContain(input) - expect(preventHandler).toHaveBeenCalled() - }) + expect(emptySubgraph.inputs).toContain(input) + expect(preventHandler).toHaveBeenCalled() + } + ) - subgraphTest("supports preventDefault() for output removal", ({ emptySubgraph }) => { - const preventHandler = vi.fn((event: Event) => { - event.preventDefault() - }) + subgraphTest( + 'supports preventDefault() for output removal', + ({ emptySubgraph }) => { + const preventHandler = vi.fn((event: Event) => { + event.preventDefault() + }) - emptySubgraph.events.addEventListener("removing-output", preventHandler) + emptySubgraph.events.addEventListener('removing-output', preventHandler) - const output = emptySubgraph.addOutput("test", "number") + const output = emptySubgraph.addOutput('test', 'number') - emptySubgraph.removeOutput(output) + emptySubgraph.removeOutput(output) - expect(emptySubgraph.outputs).toContain(output) - expect(preventHandler).toHaveBeenCalled() - }) + expect(emptySubgraph.outputs).toContain(output) + expect(preventHandler).toHaveBeenCalled() + } + ) - subgraphTest("allows removal when not prevented", ({ emptySubgraph }) => { + subgraphTest('allows removal when not prevented', ({ emptySubgraph }) => { const allowHandler = vi.fn() - emptySubgraph.events.addEventListener("removing-input", allowHandler) + emptySubgraph.events.addEventListener('removing-input', allowHandler) - const input = emptySubgraph.addInput("test", "number") + const input = emptySubgraph.addInput('test', 'number') emptySubgraph.removeInput(input) @@ -393,66 +444,69 @@ describe("SubgraphEvents - Event Cancellation", () => { }) }) -describe("SubgraphEvents - Event Detail Structure Validation", () => { - subgraphTest("validates all event detail structures match TypeScript types", ({ eventCapture }) => { - const { subgraph, capture } = eventCapture +describe('SubgraphEvents - Event Detail Structure Validation', () => { + subgraphTest( + 'validates all event detail structures match TypeScript types', + ({ eventCapture }) => { + const { subgraph, capture } = eventCapture - const input = subgraph.addInput("test_input", "number") - subgraph.renameInput(input, "renamed_input") - subgraph.removeInput(input) + const input = subgraph.addInput('test_input', 'number') + subgraph.renameInput(input, 'renamed_input') + subgraph.removeInput(input) - const output = subgraph.addOutput("test_output", "string") - subgraph.renameOutput(output, "renamed_output") - subgraph.removeOutput(output) + const output = subgraph.addOutput('test_output', 'string') + subgraph.renameOutput(output, 'renamed_output') + subgraph.removeOutput(output) - const addingInputEvent = capture.getEventsByType("adding-input")[0] - expect(addingInputEvent.detail).toEqual({ - name: expect.any(String), - type: expect.any(String), - }) + const addingInputEvent = capture.getEventsByType('adding-input')[0] + expect(addingInputEvent.detail).toEqual({ + name: expect.any(String), + type: expect.any(String) + }) - const inputAddedEvent = capture.getEventsByType("input-added")[0] - expect(inputAddedEvent.detail).toEqual({ - input: expect.any(Object), - }) + const inputAddedEvent = capture.getEventsByType('input-added')[0] + expect(inputAddedEvent.detail).toEqual({ + input: expect.any(Object) + }) - const renamingInputEvent = capture.getEventsByType("renaming-input")[0] - expect(renamingInputEvent.detail).toEqual({ - input: expect.any(Object), - index: expect.any(Number), - oldName: expect.any(String), - newName: expect.any(String), - }) + const renamingInputEvent = capture.getEventsByType('renaming-input')[0] + expect(renamingInputEvent.detail).toEqual({ + input: expect.any(Object), + index: expect.any(Number), + oldName: expect.any(String), + newName: expect.any(String) + }) - const removingInputEvent = capture.getEventsByType("removing-input")[0] - expect(removingInputEvent.detail).toEqual({ - input: expect.any(Object), - index: expect.any(Number), - }) + const removingInputEvent = capture.getEventsByType('removing-input')[0] + expect(removingInputEvent.detail).toEqual({ + input: expect.any(Object), + index: expect.any(Number) + }) - const addingOutputEvent = capture.getEventsByType("adding-output")[0] - expect(addingOutputEvent.detail).toEqual({ - name: expect.any(String), - type: expect.any(String), - }) + const addingOutputEvent = capture.getEventsByType('adding-output')[0] + expect(addingOutputEvent.detail).toEqual({ + name: expect.any(String), + type: expect.any(String) + }) - const outputAddedEvent = capture.getEventsByType("output-added")[0] - expect(outputAddedEvent.detail).toEqual({ - output: expect.any(Object), - }) + const outputAddedEvent = capture.getEventsByType('output-added')[0] + expect(outputAddedEvent.detail).toEqual({ + output: expect.any(Object) + }) - const renamingOutputEvent = capture.getEventsByType("renaming-output")[0] - expect(renamingOutputEvent.detail).toEqual({ - output: expect.any(Object), - index: expect.any(Number), - oldName: expect.any(String), - newName: expect.any(String), - }) + const renamingOutputEvent = capture.getEventsByType('renaming-output')[0] + expect(renamingOutputEvent.detail).toEqual({ + output: expect.any(Object), + index: expect.any(Number), + oldName: expect.any(String), + newName: expect.any(String) + }) - const removingOutputEvent = capture.getEventsByType("removing-output")[0] - expect(removingOutputEvent.detail).toEqual({ - output: expect.any(Object), - index: expect.any(Number), - }) - }) + const removingOutputEvent = capture.getEventsByType('removing-output')[0] + expect(removingOutputEvent.detail).toEqual({ + output: expect.any(Object), + index: expect.any(Number) + }) + } + ) }) diff --git a/src/lib/litegraph/test/subgraph/SubgraphIO.test.ts b/src/lib/litegraph/test/subgraph/SubgraphIO.test.ts index 5f5de89315..93332355b3 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphIO.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphIO.test.ts @@ -1,278 +1,320 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest' -import { LGraphNode } from "@/lib/litegraph/src/litegraph" +import { LGraphNode } from '@/lib/litegraph/src/litegraph' -import { subgraphTest } from "./fixtures/subgraphFixtures" +import { subgraphTest } from './fixtures/subgraphFixtures' import { createTestSubgraph, - createTestSubgraphNode, -} from "./fixtures/subgraphHelpers" + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("SubgraphIO - Input Slot Dual-Nature Behavior", () => { - subgraphTest("input accepts external connections from parent graph", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode, parentGraph } = subgraphWithNode +describe('SubgraphIO - Input Slot Dual-Nature Behavior', () => { + subgraphTest( + 'input accepts external connections from parent graph', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode - subgraph.addInput("test_input", "number") + subgraph.addInput('test_input', 'number') - const externalNode = new LGraphNode("External Source") - externalNode.addOutput("out", "number") - parentGraph.add(externalNode) + const externalNode = new LGraphNode('External Source') + externalNode.addOutput('out', 'number') + parentGraph.add(externalNode) + + expect(() => { + externalNode.connect(0, subgraphNode, 0) + }).not.toThrow() + + expect( + externalNode.outputs[0].links?.includes(subgraphNode.inputs[0].link) + ).toBe(true) + expect(subgraphNode.inputs[0].link).not.toBe(null) + } + ) + + subgraphTest( + 'empty input slot creation enables dynamic IO', + ({ simpleSubgraph }) => { + const initialInputCount = simpleSubgraph.inputs.length + + // Create empty input slot + simpleSubgraph.addInput('', '*') + + // Should create new input + expect(simpleSubgraph.inputs.length).toBe(initialInputCount + 1) + + // The empty slot should be configurable + const emptyInput = simpleSubgraph.inputs.at(-1) + expect(emptyInput.name).toBe('') + expect(emptyInput.type).toBe('*') + } + ) + + subgraphTest( + 'handles slot removal with active connections', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode + + const externalNode = new LGraphNode('External Source') + externalNode.addOutput('out', '*') + parentGraph.add(externalNode) - expect(() => { externalNode.connect(0, subgraphNode, 0) - }).not.toThrow() - expect(externalNode.outputs[0].links?.includes(subgraphNode.inputs[0].link)).toBe(true) - expect(subgraphNode.inputs[0].link).not.toBe(null) - }) + // Verify connection exists + expect(subgraphNode.inputs[0].link).not.toBe(null) - subgraphTest("empty input slot creation enables dynamic IO", ({ simpleSubgraph }) => { - const initialInputCount = simpleSubgraph.inputs.length + // Remove the existing input (fixture creates one input) + const inputToRemove = subgraph.inputs[0] + subgraph.removeInput(inputToRemove) - // Create empty input slot - simpleSubgraph.addInput("", "*") + // Connection should be cleaned up + expect(subgraphNode.inputs.length).toBe(0) + expect(externalNode.outputs[0].links).toHaveLength(0) + } + ) - // Should create new input - expect(simpleSubgraph.inputs.length).toBe(initialInputCount + 1) + subgraphTest( + 'handles slot renaming with active connections', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode - // The empty slot should be configurable - const emptyInput = simpleSubgraph.inputs.at(-1) - expect(emptyInput.name).toBe("") - expect(emptyInput.type).toBe("*") - }) + const externalNode = new LGraphNode('External Source') + externalNode.addOutput('out', '*') + parentGraph.add(externalNode) - subgraphTest("handles slot removal with active connections", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode, parentGraph } = subgraphWithNode + externalNode.connect(0, subgraphNode, 0) - const externalNode = new LGraphNode("External Source") - externalNode.addOutput("out", "*") - parentGraph.add(externalNode) + // Verify connection exists + expect(subgraphNode.inputs[0].link).not.toBe(null) - externalNode.connect(0, subgraphNode, 0) + // Rename the existing input (fixture creates input named "input") + const inputToRename = subgraph.inputs[0] + subgraph.renameInput(inputToRename, 'new_name') - // Verify connection exists - expect(subgraphNode.inputs[0].link).not.toBe(null) - - // Remove the existing input (fixture creates one input) - const inputToRemove = subgraph.inputs[0] - subgraph.removeInput(inputToRemove) - - // Connection should be cleaned up - expect(subgraphNode.inputs.length).toBe(0) - expect(externalNode.outputs[0].links).toHaveLength(0) - }) - - subgraphTest("handles slot renaming with active connections", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode, parentGraph } = subgraphWithNode - - const externalNode = new LGraphNode("External Source") - externalNode.addOutput("out", "*") - parentGraph.add(externalNode) - - externalNode.connect(0, subgraphNode, 0) - - // Verify connection exists - expect(subgraphNode.inputs[0].link).not.toBe(null) - - // Rename the existing input (fixture creates input named "input") - const inputToRename = subgraph.inputs[0] - subgraph.renameInput(inputToRename, "new_name") - - // Connection should persist and subgraph definition should be updated - expect(subgraphNode.inputs[0].link).not.toBe(null) - expect(subgraph.inputs[0].label).toBe("new_name") - expect(subgraph.inputs[0].displayName).toBe("new_name") - }) + // Connection should persist and subgraph definition should be updated + expect(subgraphNode.inputs[0].link).not.toBe(null) + expect(subgraph.inputs[0].label).toBe('new_name') + expect(subgraph.inputs[0].displayName).toBe('new_name') + } + ) }) -describe("SubgraphIO - Output Slot Dual-Nature Behavior", () => { - subgraphTest("output provides connections to parent graph", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode, parentGraph } = subgraphWithNode +describe('SubgraphIO - Output Slot Dual-Nature Behavior', () => { + subgraphTest( + 'output provides connections to parent graph', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode - // Add an output to the subgraph - subgraph.addOutput("test_output", "number") + // Add an output to the subgraph + subgraph.addOutput('test_output', 'number') - const externalNode = new LGraphNode("External Target") - externalNode.addInput("in", "number") - parentGraph.add(externalNode) + const externalNode = new LGraphNode('External Target') + externalNode.addInput('in', 'number') + parentGraph.add(externalNode) + + // External connection from subgraph output should work + expect(() => { + subgraphNode.connect(0, externalNode, 0) + }).not.toThrow() + + expect( + subgraphNode.outputs[0].links?.includes(externalNode.inputs[0].link) + ).toBe(true) + expect(externalNode.inputs[0].link).not.toBe(null) + } + ) + + subgraphTest( + 'empty output slot creation enables dynamic IO', + ({ simpleSubgraph }) => { + const initialOutputCount = simpleSubgraph.outputs.length + + // Create empty output slot + simpleSubgraph.addOutput('', '*') + + // Should create new output + expect(simpleSubgraph.outputs.length).toBe(initialOutputCount + 1) + + // The empty slot should be configurable + const emptyOutput = simpleSubgraph.outputs.at(-1) + expect(emptyOutput.name).toBe('') + expect(emptyOutput.type).toBe('*') + } + ) + + subgraphTest( + 'handles slot removal with active connections', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode + + const externalNode = new LGraphNode('External Target') + externalNode.addInput('in', '*') + parentGraph.add(externalNode) - // External connection from subgraph output should work - expect(() => { subgraphNode.connect(0, externalNode, 0) - }).not.toThrow() - expect(subgraphNode.outputs[0].links?.includes(externalNode.inputs[0].link)).toBe(true) - expect(externalNode.inputs[0].link).not.toBe(null) - }) + // Verify connection exists + expect(externalNode.inputs[0].link).not.toBe(null) - subgraphTest("empty output slot creation enables dynamic IO", ({ simpleSubgraph }) => { - const initialOutputCount = simpleSubgraph.outputs.length + // Remove the existing output (fixture creates one output) + const outputToRemove = subgraph.outputs[0] + subgraph.removeOutput(outputToRemove) - // Create empty output slot - simpleSubgraph.addOutput("", "*") + // Connection should be cleaned up + expect(subgraphNode.outputs.length).toBe(0) + expect(externalNode.inputs[0].link).toBe(null) + } + ) - // Should create new output - expect(simpleSubgraph.outputs.length).toBe(initialOutputCount + 1) + subgraphTest( + 'handles slot renaming updates all references', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode - // The empty slot should be configurable - const emptyOutput = simpleSubgraph.outputs.at(-1) - expect(emptyOutput.name).toBe("") - expect(emptyOutput.type).toBe("*") - }) + const externalNode = new LGraphNode('External Target') + externalNode.addInput('in', '*') + parentGraph.add(externalNode) - subgraphTest("handles slot removal with active connections", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode, parentGraph } = subgraphWithNode + subgraphNode.connect(0, externalNode, 0) - const externalNode = new LGraphNode("External Target") - externalNode.addInput("in", "*") - parentGraph.add(externalNode) + // Verify connection exists + expect(externalNode.inputs[0].link).not.toBe(null) - subgraphNode.connect(0, externalNode, 0) + // Rename the existing output (fixture creates output named "output") + const outputToRename = subgraph.outputs[0] + subgraph.renameOutput(outputToRename, 'new_name') - // Verify connection exists - expect(externalNode.inputs[0].link).not.toBe(null) - - // Remove the existing output (fixture creates one output) - const outputToRemove = subgraph.outputs[0] - subgraph.removeOutput(outputToRemove) - - // Connection should be cleaned up - expect(subgraphNode.outputs.length).toBe(0) - expect(externalNode.inputs[0].link).toBe(null) - }) - - subgraphTest("handles slot renaming updates all references", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode, parentGraph } = subgraphWithNode - - const externalNode = new LGraphNode("External Target") - externalNode.addInput("in", "*") - parentGraph.add(externalNode) - - subgraphNode.connect(0, externalNode, 0) - - // Verify connection exists - expect(externalNode.inputs[0].link).not.toBe(null) - - // Rename the existing output (fixture creates output named "output") - const outputToRename = subgraph.outputs[0] - subgraph.renameOutput(outputToRename, "new_name") - - // Connection should persist and subgraph definition should be updated - expect(externalNode.inputs[0].link).not.toBe(null) - expect(subgraph.outputs[0].label).toBe("new_name") - expect(subgraph.outputs[0].displayName).toBe("new_name") - }) + // Connection should persist and subgraph definition should be updated + expect(externalNode.inputs[0].link).not.toBe(null) + expect(subgraph.outputs[0].label).toBe('new_name') + expect(subgraph.outputs[0].displayName).toBe('new_name') + } + ) }) -describe("SubgraphIO - Boundary Connection Management", () => { - subgraphTest("verifies cross-boundary link resolution", ({ complexSubgraph }) => { - const subgraphNode = createTestSubgraphNode(complexSubgraph) - const parentGraph = subgraphNode.graph! +describe('SubgraphIO - Boundary Connection Management', () => { + subgraphTest( + 'verifies cross-boundary link resolution', + ({ complexSubgraph }) => { + const subgraphNode = createTestSubgraphNode(complexSubgraph) + const parentGraph = subgraphNode.graph! - const externalSource = new LGraphNode("External Source") - externalSource.addOutput("out", "number") - parentGraph.add(externalSource) + const externalSource = new LGraphNode('External Source') + externalSource.addOutput('out', 'number') + parentGraph.add(externalSource) - const externalTarget = new LGraphNode("External Target") - externalTarget.addInput("in", "number") - parentGraph.add(externalTarget) + const externalTarget = new LGraphNode('External Target') + externalTarget.addInput('in', 'number') + parentGraph.add(externalTarget) - externalSource.connect(0, subgraphNode, 0) - subgraphNode.connect(0, externalTarget, 0) + externalSource.connect(0, subgraphNode, 0) + subgraphNode.connect(0, externalTarget, 0) - expect(subgraphNode.inputs[0].link).not.toBe(null) - expect(externalTarget.inputs[0].link).not.toBe(null) - }) + expect(subgraphNode.inputs[0].link).not.toBe(null) + expect(externalTarget.inputs[0].link).not.toBe(null) + } + ) - subgraphTest("handles bypass nodes that pass through data", ({ simpleSubgraph }) => { - const subgraphNode = createTestSubgraphNode(simpleSubgraph) - const parentGraph = subgraphNode.graph! + subgraphTest( + 'handles bypass nodes that pass through data', + ({ simpleSubgraph }) => { + const subgraphNode = createTestSubgraphNode(simpleSubgraph) + const parentGraph = subgraphNode.graph! - const externalSource = new LGraphNode("External Source") - externalSource.addOutput("out", "number") - parentGraph.add(externalSource) + const externalSource = new LGraphNode('External Source') + externalSource.addOutput('out', 'number') + parentGraph.add(externalSource) - const externalTarget = new LGraphNode("External Target") - externalTarget.addInput("in", "number") - parentGraph.add(externalTarget) + const externalTarget = new LGraphNode('External Target') + externalTarget.addInput('in', 'number') + parentGraph.add(externalTarget) - externalSource.connect(0, subgraphNode, 0) - subgraphNode.connect(0, externalTarget, 0) + externalSource.connect(0, subgraphNode, 0) + subgraphNode.connect(0, externalTarget, 0) - expect(subgraphNode.inputs[0].link).not.toBe(null) - expect(externalTarget.inputs[0].link).not.toBe(null) - }) + expect(subgraphNode.inputs[0].link).not.toBe(null) + expect(externalTarget.inputs[0].link).not.toBe(null) + } + ) - subgraphTest("tests link integrity across subgraph boundaries", ({ subgraphWithNode }) => { - const { subgraphNode, parentGraph } = subgraphWithNode + subgraphTest( + 'tests link integrity across subgraph boundaries', + ({ subgraphWithNode }) => { + const { subgraphNode, parentGraph } = subgraphWithNode - const externalSource = new LGraphNode("External Source") - externalSource.addOutput("out", "*") - parentGraph.add(externalSource) + const externalSource = new LGraphNode('External Source') + externalSource.addOutput('out', '*') + parentGraph.add(externalSource) - const externalTarget = new LGraphNode("External Target") - externalTarget.addInput("in", "*") - parentGraph.add(externalTarget) + const externalTarget = new LGraphNode('External Target') + externalTarget.addInput('in', '*') + parentGraph.add(externalTarget) - externalSource.connect(0, subgraphNode, 0) - subgraphNode.connect(0, externalTarget, 0) + externalSource.connect(0, subgraphNode, 0) + subgraphNode.connect(0, externalTarget, 0) - const inputBoundaryLink = subgraphNode.inputs[0].link - const outputBoundaryLink = externalTarget.inputs[0].link + const inputBoundaryLink = subgraphNode.inputs[0].link + const outputBoundaryLink = externalTarget.inputs[0].link - expect(inputBoundaryLink).toBeTruthy() - expect(outputBoundaryLink).toBeTruthy() + expect(inputBoundaryLink).toBeTruthy() + expect(outputBoundaryLink).toBeTruthy() - // Links should exist in parent graph - expect(inputBoundaryLink).toBeTruthy() - expect(outputBoundaryLink).toBeTruthy() - }) + // Links should exist in parent graph + expect(inputBoundaryLink).toBeTruthy() + expect(outputBoundaryLink).toBeTruthy() + } + ) - subgraphTest("verifies proper link cleanup on slot removal", ({ complexSubgraph }) => { - const subgraphNode = createTestSubgraphNode(complexSubgraph) - const parentGraph = subgraphNode.graph! + subgraphTest( + 'verifies proper link cleanup on slot removal', + ({ complexSubgraph }) => { + const subgraphNode = createTestSubgraphNode(complexSubgraph) + const parentGraph = subgraphNode.graph! - const externalSource = new LGraphNode("External Source") - externalSource.addOutput("out", "number") - parentGraph.add(externalSource) + const externalSource = new LGraphNode('External Source') + externalSource.addOutput('out', 'number') + parentGraph.add(externalSource) - const externalTarget = new LGraphNode("External Target") - externalTarget.addInput("in", "number") - parentGraph.add(externalTarget) + const externalTarget = new LGraphNode('External Target') + externalTarget.addInput('in', 'number') + parentGraph.add(externalTarget) - externalSource.connect(0, subgraphNode, 0) - subgraphNode.connect(0, externalTarget, 0) + externalSource.connect(0, subgraphNode, 0) + subgraphNode.connect(0, externalTarget, 0) - expect(subgraphNode.inputs[0].link).not.toBe(null) - expect(externalTarget.inputs[0].link).not.toBe(null) + expect(subgraphNode.inputs[0].link).not.toBe(null) + expect(externalTarget.inputs[0].link).not.toBe(null) - const inputToRemove = complexSubgraph.inputs[0] - complexSubgraph.removeInput(inputToRemove) + const inputToRemove = complexSubgraph.inputs[0] + complexSubgraph.removeInput(inputToRemove) - expect(subgraphNode.inputs.findIndex(i => i.name === "data")).toBe(-1) - expect(externalSource.outputs[0].links).toHaveLength(0) + expect(subgraphNode.inputs.findIndex((i) => i.name === 'data')).toBe(-1) + expect(externalSource.outputs[0].links).toHaveLength(0) - const outputToRemove = complexSubgraph.outputs[0] - complexSubgraph.removeOutput(outputToRemove) + const outputToRemove = complexSubgraph.outputs[0] + complexSubgraph.removeOutput(outputToRemove) - expect(subgraphNode.outputs.findIndex(o => o.name === "result")).toBe(-1) - expect(externalTarget.inputs[0].link).toBe(null) - }) + expect(subgraphNode.outputs.findIndex((o) => o.name === 'result')).toBe( + -1 + ) + expect(externalTarget.inputs[0].link).toBe(null) + } + ) }) -describe("SubgraphIO - Advanced Scenarios", () => { - it("handles multiple inputs and outputs with complex connections", () => { +describe('SubgraphIO - Advanced Scenarios', () => { + it('handles multiple inputs and outputs with complex connections', () => { const subgraph = createTestSubgraph({ - name: "Complex IO Test", + name: 'Complex IO Test', inputs: [ - { name: "input1", type: "number" }, - { name: "input2", type: "string" }, - { name: "input3", type: "boolean" }, + { name: 'input1', type: 'number' }, + { name: 'input2', type: 'string' }, + { name: 'input3', type: 'boolean' } ], outputs: [ - { name: "output1", type: "number" }, - { name: "output2", type: "string" }, - ], + { name: 'output1', type: 'number' }, + { name: 'output2', type: 'string' } + ] }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -282,16 +324,16 @@ describe("SubgraphIO - Advanced Scenarios", () => { expect(subgraphNode.outputs.length).toBe(2) // Each slot should have correct type - expect(subgraphNode.inputs[0].type).toBe("number") - expect(subgraphNode.inputs[1].type).toBe("string") - expect(subgraphNode.inputs[2].type).toBe("boolean") - expect(subgraphNode.outputs[0].type).toBe("number") - expect(subgraphNode.outputs[1].type).toBe("string") + expect(subgraphNode.inputs[0].type).toBe('number') + expect(subgraphNode.inputs[1].type).toBe('string') + expect(subgraphNode.inputs[2].type).toBe('boolean') + expect(subgraphNode.outputs[0].type).toBe('number') + expect(subgraphNode.outputs[1].type).toBe('string') }) - it("handles dynamic slot creation and removal", () => { + it('handles dynamic slot creation and removal', () => { const subgraph = createTestSubgraph({ - name: "Dynamic IO Test", + name: 'Dynamic IO Test' }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -301,14 +343,14 @@ describe("SubgraphIO - Advanced Scenarios", () => { expect(subgraphNode.outputs.length).toBe(0) // Add slots dynamically - subgraph.addInput("dynamic_input", "number") - subgraph.addOutput("dynamic_output", "string") + subgraph.addInput('dynamic_input', 'number') + subgraph.addOutput('dynamic_output', 'string') // SubgraphNode should automatically update expect(subgraphNode.inputs.length).toBe(1) expect(subgraphNode.outputs.length).toBe(1) - expect(subgraphNode.inputs[0].name).toBe("dynamic_input") - expect(subgraphNode.outputs[0].name).toBe("dynamic_output") + expect(subgraphNode.inputs[0].name).toBe('dynamic_input') + expect(subgraphNode.outputs[0].name).toBe('dynamic_output') // Remove slots subgraph.removeInput(subgraph.inputs[0]) @@ -319,11 +361,11 @@ describe("SubgraphIO - Advanced Scenarios", () => { expect(subgraphNode.outputs.length).toBe(0) }) - it("maintains slot synchronization across multiple instances", () => { + it('maintains slot synchronization across multiple instances', () => { const subgraph = createTestSubgraph({ - name: "Multi-Instance Test", - inputs: [{ name: "shared_input", type: "number" }], - outputs: [{ name: "shared_output", type: "number" }], + name: 'Multi-Instance Test', + inputs: [{ name: 'shared_input', type: 'number' }], + outputs: [{ name: 'shared_output', type: 'number' }] }) // Create multiple instances @@ -337,8 +379,8 @@ describe("SubgraphIO - Advanced Scenarios", () => { expect(instance3.inputs.length).toBe(1) // Modify the subgraph definition - subgraph.addInput("new_input", "string") - subgraph.addOutput("new_output", "boolean") + subgraph.addInput('new_input', 'string') + subgraph.addOutput('new_output', 'boolean') // All instances should automatically update expect(instance1.inputs.length).toBe(2) @@ -350,37 +392,40 @@ describe("SubgraphIO - Advanced Scenarios", () => { }) }) -describe("SubgraphIO - Empty Slot Connection", () => { - subgraphTest("creates new input and connects when dragging from empty slot inside subgraph", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode } = subgraphWithNode +describe('SubgraphIO - Empty Slot Connection', () => { + subgraphTest( + 'creates new input and connects when dragging from empty slot inside subgraph', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode } = subgraphWithNode - // Create a node inside the subgraph that will receive the connection - const internalNode = new LGraphNode("Internal Node") - internalNode.addInput("in", "string") - subgraph.add(internalNode) + // Create a node inside the subgraph that will receive the connection + const internalNode = new LGraphNode('Internal Node') + internalNode.addInput('in', 'string') + subgraph.add(internalNode) - // Simulate the connection process from the empty slot to an internal node - // The -1 indicates a connection from the "empty" slot - subgraph.inputNode.connectByType(-1, internalNode, "string") + // Simulate the connection process from the empty slot to an internal node + // The -1 indicates a connection from the "empty" slot + subgraph.inputNode.connectByType(-1, internalNode, 'string') - // 1. A new input should have been created on the subgraph - expect(subgraph.inputs.length).toBe(2) // Fixture adds one input already - const newInput = subgraph.inputs[1] - expect(newInput.name).toBe("in") - expect(newInput.type).toBe("string") + // 1. A new input should have been created on the subgraph + expect(subgraph.inputs.length).toBe(2) // Fixture adds one input already + const newInput = subgraph.inputs[1] + expect(newInput.name).toBe('in') + expect(newInput.type).toBe('string') - // 2. The subgraph node should now have a corresponding real input slot - expect(subgraphNode.inputs.length).toBe(2) - const subgraphInputSlot = subgraphNode.inputs[1] - expect(subgraphInputSlot.name).toBe("in") + // 2. The subgraph node should now have a corresponding real input slot + expect(subgraphNode.inputs.length).toBe(2) + const subgraphInputSlot = subgraphNode.inputs[1] + expect(subgraphInputSlot.name).toBe('in') - // 3. A link should be established inside the subgraph - expect(internalNode.inputs[0].link).not.toBe(null) - const link = subgraph.links.get(internalNode.inputs[0].link!) - expect(link).toBeDefined() - expect(link.target_id).toBe(internalNode.id) - expect(link.target_slot).toBe(0) - expect(link.origin_id).toBe(subgraph.inputNode.id) - expect(link.origin_slot).toBe(1) // Should be the second slot - }) + // 3. A link should be established inside the subgraph + expect(internalNode.inputs[0].link).not.toBe(null) + const link = subgraph.links.get(internalNode.inputs[0].link!) + expect(link).toBeDefined() + expect(link.target_id).toBe(internalNode.id) + expect(link.target_slot).toBe(0) + expect(link.origin_id).toBe(subgraph.inputNode.id) + expect(link.origin_slot).toBe(1) // Should be the second slot + } + ) }) diff --git a/src/lib/litegraph/test/subgraph/SubgraphMemory.test.ts b/src/lib/litegraph/test/subgraph/SubgraphMemory.test.ts index 9f81f05f11..3f4e077f78 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphMemory.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphMemory.test.ts @@ -1,20 +1,20 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from 'vitest' -import { LGraph } from "@/lib/litegraph/src/litegraph" +import { LGraph } from '@/lib/litegraph/src/litegraph' -import { subgraphTest } from "./fixtures/subgraphFixtures" +import { subgraphTest } from './fixtures/subgraphFixtures' import { createTestSubgraph, - createTestSubgraphNode, -} from "./fixtures/subgraphHelpers" + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("SubgraphNode Memory Management", () => { - describe("Event Listener Cleanup", () => { - it("should register event listeners on construction", () => { +describe('SubgraphNode Memory Management', () => { + describe('Event Listener Cleanup', () => { + it('should register event listeners on construction', () => { const subgraph = createTestSubgraph() // Spy on addEventListener to track listener registration - const addEventSpy = vi.spyOn(subgraph.events, "addEventListener") + const addEventSpy = vi.spyOn(subgraph.events, 'addEventListener') const initialCalls = addEventSpy.mock.calls.length createTestSubgraphNode(subgraph) @@ -23,39 +23,43 @@ describe("SubgraphNode Memory Management", () => { expect(addEventSpy.mock.calls.length).toBeGreaterThan(initialCalls) // Should have registered listeners for all major events - const eventTypes = addEventSpy.mock.calls.map(call => call[0]) - expect(eventTypes).toContain("input-added") - expect(eventTypes).toContain("removing-input") - expect(eventTypes).toContain("output-added") - expect(eventTypes).toContain("removing-output") - expect(eventTypes).toContain("renaming-input") - expect(eventTypes).toContain("renaming-output") + const eventTypes = addEventSpy.mock.calls.map((call) => call[0]) + expect(eventTypes).toContain('input-added') + expect(eventTypes).toContain('removing-input') + expect(eventTypes).toContain('output-added') + expect(eventTypes).toContain('removing-output') + expect(eventTypes).toContain('renaming-input') + expect(eventTypes).toContain('renaming-output') }) - it("should clean up input listeners on removal", () => { + it('should clean up input listeners on removal', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input1", type: "number" }], + inputs: [{ name: 'input1', type: 'number' }] }) const subgraphNode = createTestSubgraphNode(subgraph) // Add input should have created listeners expect(subgraphNode.inputs[0]._listenerController).toBeDefined() - expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe(false) + expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe( + false + ) // Call onRemoved to simulate node removal subgraphNode.onRemoved() // Input listeners should be aborted - expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe(true) + expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe( + true + ) }) - it("should not accumulate listeners during reconfiguration", () => { + it('should not accumulate listeners during reconfiguration', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input1", type: "number" }], + inputs: [{ name: 'input1', type: 'number' }] }) const subgraphNode = createTestSubgraphNode(subgraph) - const addEventSpy = vi.spyOn(subgraph.events, "addEventListener") + const addEventSpy = vi.spyOn(subgraph.events, 'addEventListener') const initialCalls = addEventSpy.mock.calls.length // Reconfigure multiple times @@ -69,7 +73,7 @@ describe("SubgraphNode Memory Management", () => { outputs: [], properties: {}, flags: {}, - mode: 0, + mode: 0 }) } @@ -80,32 +84,32 @@ describe("SubgraphNode Memory Management", () => { }) }) - describe("Widget Promotion Memory Management", () => { - it("should clean up promoted widget references", () => { + describe('Widget Promotion Memory Management', () => { + it('should clean up promoted widget references', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "testInput", type: "number" }], + inputs: [{ name: 'testInput', type: 'number' }] }) const subgraphNode = createTestSubgraphNode(subgraph) // Simulate widget promotion scenario const input = subgraphNode.inputs[0] const mockWidget = { - type: "number", - name: "promoted_widget", + type: 'number', + name: 'promoted_widget', value: 123, draw: vi.fn(), mouse: vi.fn(), computeSize: vi.fn(), createCopyForNode: vi.fn().mockReturnValue({ - type: "number", - name: "promoted_widget", - value: 123, - }), + type: 'number', + name: 'promoted_widget', + value: 123 + }) } // Simulate widget promotion input._widget = mockWidget - input.widget = { name: "promoted_widget" } + input.widget = { name: 'promoted_widget' } subgraphNode.widgets.push(mockWidget) expect(input._widget).toBe(mockWidget) @@ -119,9 +123,9 @@ describe("SubgraphNode Memory Management", () => { expect(subgraphNode.widgets).not.toContain(mockWidget) }) - it("should not leak widgets during reconfiguration", () => { + it('should not leak widgets during reconfiguration', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input1", type: "number" }], + inputs: [{ name: 'input1', type: 'number' }] }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -139,7 +143,7 @@ describe("SubgraphNode Memory Management", () => { outputs: [], properties: {}, flags: {}, - mode: 0, + mode: 0 }) } @@ -149,67 +153,35 @@ describe("SubgraphNode Memory Management", () => { }) }) -describe("SubgraphMemory - Event Listener Management", () => { - subgraphTest("event handlers still work after node creation", ({ emptySubgraph }) => { - const rootGraph = new LGraph() - const subgraphNode = createTestSubgraphNode(emptySubgraph) - rootGraph.add(subgraphNode) - - const handler = vi.fn() - emptySubgraph.events.addEventListener("input-added", handler) - - emptySubgraph.addInput("test", "number") - - expect(handler).toHaveBeenCalledTimes(1) - expect(handler).toHaveBeenCalledWith(expect.objectContaining({ - type: "input-added", - })) - }) - - subgraphTest("can add and remove multiple nodes without errors", ({ emptySubgraph }) => { - const rootGraph = new LGraph() - const nodes: ReturnType[] = [] - - // Should be able to create multiple nodes without issues - for (let i = 0; i < 5; i++) { +describe('SubgraphMemory - Event Listener Management', () => { + subgraphTest( + 'event handlers still work after node creation', + ({ emptySubgraph }) => { + const rootGraph = new LGraph() const subgraphNode = createTestSubgraphNode(emptySubgraph) rootGraph.add(subgraphNode) - nodes.push(subgraphNode) + + const handler = vi.fn() + emptySubgraph.events.addEventListener('input-added', handler) + + emptySubgraph.addInput('test', 'number') + + expect(handler).toHaveBeenCalledTimes(1) + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'input-added' + }) + ) } + ) - expect(rootGraph.nodes.length).toBe(5) - - // Should be able to remove them all without issues - for (const node of nodes) { - rootGraph.remove(node) - } - - expect(rootGraph.nodes.length).toBe(0) - }) - - subgraphTest("supports AbortController cleanup patterns", ({ emptySubgraph }) => { - const abortController = new AbortController() - const { signal } = abortController - - const handler = vi.fn() - - emptySubgraph.events.addEventListener("input-added", handler, { signal }) - - emptySubgraph.addInput("test1", "number") - expect(handler).toHaveBeenCalledTimes(1) - - abortController.abort() - - emptySubgraph.addInput("test2", "number") - expect(handler).toHaveBeenCalledTimes(1) - }) - - subgraphTest("handles multiple creation/deletion cycles", ({ emptySubgraph }) => { - const rootGraph = new LGraph() - - for (let cycle = 0; cycle < 3; cycle++) { - const nodes = [] + subgraphTest( + 'can add and remove multiple nodes without errors', + ({ emptySubgraph }) => { + const rootGraph = new LGraph() + const nodes: ReturnType[] = [] + // Should be able to create multiple nodes without issues for (let i = 0; i < 5; i++) { const subgraphNode = createTestSubgraphNode(emptySubgraph) rootGraph.add(subgraphNode) @@ -218,17 +190,63 @@ describe("SubgraphMemory - Event Listener Management", () => { expect(rootGraph.nodes.length).toBe(5) + // Should be able to remove them all without issues for (const node of nodes) { rootGraph.remove(node) } expect(rootGraph.nodes.length).toBe(0) } - }) + ) + + subgraphTest( + 'supports AbortController cleanup patterns', + ({ emptySubgraph }) => { + const abortController = new AbortController() + const { signal } = abortController + + const handler = vi.fn() + + emptySubgraph.events.addEventListener('input-added', handler, { signal }) + + emptySubgraph.addInput('test1', 'number') + expect(handler).toHaveBeenCalledTimes(1) + + abortController.abort() + + emptySubgraph.addInput('test2', 'number') + expect(handler).toHaveBeenCalledTimes(1) + } + ) + + subgraphTest( + 'handles multiple creation/deletion cycles', + ({ emptySubgraph }) => { + const rootGraph = new LGraph() + + for (let cycle = 0; cycle < 3; cycle++) { + const nodes = [] + + for (let i = 0; i < 5; i++) { + const subgraphNode = createTestSubgraphNode(emptySubgraph) + rootGraph.add(subgraphNode) + nodes.push(subgraphNode) + } + + expect(rootGraph.nodes.length).toBe(5) + + for (const node of nodes) { + rootGraph.remove(node) + } + + expect(rootGraph.nodes.length).toBe(0) + } + } + ) }) -describe("SubgraphMemory - Reference Management", () => { - it("properly manages subgraph references in root graph", () => { +describe('SubgraphMemory - Reference Management', () => { + it('properly manages subgraph references in root graph', () => { const rootGraph = new LGraph() const subgraph = createTestSubgraph() const subgraphId = subgraph.id @@ -243,7 +261,7 @@ describe("SubgraphMemory - Reference Management", () => { expect(rootGraph.subgraphs.has(subgraphId)).toBe(false) }) - it("maintains proper parent-child references", () => { + it('maintains proper parent-child references', () => { const rootGraph = new LGraph() const subgraph = createTestSubgraph({ nodeCount: 2 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -258,7 +276,7 @@ describe("SubgraphMemory - Reference Management", () => { expect(rootGraph.nodes).not.toContain(subgraphNode) }) - it("prevents circular reference creation", () => { + it('prevents circular reference creation', () => { const subgraph = createTestSubgraph({ nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -271,39 +289,42 @@ describe("SubgraphMemory - Reference Management", () => { }) }) -describe("SubgraphMemory - Widget Reference Management", () => { - subgraphTest("properly sets and clears widget references", ({ simpleSubgraph }) => { - const subgraphNode = createTestSubgraphNode(simpleSubgraph) - const input = subgraphNode.inputs[0] +describe('SubgraphMemory - Widget Reference Management', () => { + subgraphTest( + 'properly sets and clears widget references', + ({ simpleSubgraph }) => { + const subgraphNode = createTestSubgraphNode(simpleSubgraph) + const input = subgraphNode.inputs[0] - // Mock widget for testing - const mockWidget = { - type: "number", - value: 42, - name: "test_widget", + // Mock widget for testing + const mockWidget = { + type: 'number', + value: 42, + name: 'test_widget' + } + + // Set widget reference + if (input && '_widget' in input) { + ;(input as any)._widget = mockWidget + expect((input as any)._widget).toBe(mockWidget) + } + + // Clear widget reference + if (input && '_widget' in input) { + ;(input as any)._widget = undefined + expect((input as any)._widget).toBeUndefined() + } } + ) - // Set widget reference - if (input && "_widget" in input) { - ;(input as any)._widget = mockWidget - expect((input as any)._widget).toBe(mockWidget) - } - - // Clear widget reference - if (input && "_widget" in input) { - ;(input as any)._widget = undefined - expect((input as any)._widget).toBeUndefined() - } - }) - - subgraphTest("maintains widget count consistency", ({ simpleSubgraph }) => { + subgraphTest('maintains widget count consistency', ({ simpleSubgraph }) => { const subgraphNode = createTestSubgraphNode(simpleSubgraph) const initialWidgetCount = subgraphNode.widgets?.length || 0 // Add mock widgets - const widget1 = { type: "number", value: 1, name: "widget1" } - const widget2 = { type: "string", value: "test", name: "widget2" } + const widget1 = { type: 'number', value: 1, name: 'widget1' } + const widget2 = { type: 'string', value: 'test', name: 'widget2' } if (subgraphNode.widgets) { subgraphNode.widgets.push(widget1, widget2) @@ -317,67 +338,73 @@ describe("SubgraphMemory - Widget Reference Management", () => { } }) - subgraphTest("cleans up references during node removal", ({ simpleSubgraph }) => { - const subgraphNode = createTestSubgraphNode(simpleSubgraph) - const input = subgraphNode.inputs[0] - const output = subgraphNode.outputs[0] + subgraphTest( + 'cleans up references during node removal', + ({ simpleSubgraph }) => { + const subgraphNode = createTestSubgraphNode(simpleSubgraph) + const input = subgraphNode.inputs[0] + const output = subgraphNode.outputs[0] - // Set up references that should be cleaned up - const mockReferences = { - widget: { type: "number", value: 42 }, - connection: { id: 1, type: "number" }, - listener: vi.fn(), + // Set up references that should be cleaned up + const mockReferences = { + widget: { type: 'number', value: 42 }, + connection: { id: 1, type: 'number' }, + listener: vi.fn() + } + + // Set references + if (input) { + ;(input as any)._widget = mockReferences.widget + ;(input as any)._connection = mockReferences.connection + } + if (output) { + ;(input as any)._connection = mockReferences.connection + } + + // Verify references are set + expect((input as any)?._widget).toBe(mockReferences.widget) + expect((input as any)?._connection).toBe(mockReferences.connection) + + // Simulate proper cleanup (what onRemoved should do) + subgraphNode.onRemoved() + + // Input-specific listeners should be cleaned up (this works) + if (input && '_listenerController' in input) { + expect((input as any)._listenerController?.signal.aborted).toBe(true) + } } - - // Set references - if (input) { - ;(input as any)._widget = mockReferences.widget - ;(input as any)._connection = mockReferences.connection - } - if (output) { - ;(input as any)._connection = mockReferences.connection - } - - // Verify references are set - expect((input as any)?._widget).toBe(mockReferences.widget) - expect((input as any)?._connection).toBe(mockReferences.connection) - - // Simulate proper cleanup (what onRemoved should do) - subgraphNode.onRemoved() - - // Input-specific listeners should be cleaned up (this works) - if (input && "_listenerController" in input) { - expect((input as any)._listenerController?.signal.aborted).toBe(true) - } - }) + ) }) -describe("SubgraphMemory - Performance and Scale", () => { - subgraphTest("handles multiple subgraphs in same graph", ({ subgraphWithNode }) => { - const { parentGraph } = subgraphWithNode - const subgraphA = createTestSubgraph({ name: "Subgraph A" }) - const subgraphB = createTestSubgraph({ name: "Subgraph B" }) +describe('SubgraphMemory - Performance and Scale', () => { + subgraphTest( + 'handles multiple subgraphs in same graph', + ({ subgraphWithNode }) => { + const { parentGraph } = subgraphWithNode + const subgraphA = createTestSubgraph({ name: 'Subgraph A' }) + const subgraphB = createTestSubgraph({ name: 'Subgraph B' }) - const nodeA = createTestSubgraphNode(subgraphA) - const nodeB = createTestSubgraphNode(subgraphB) + const nodeA = createTestSubgraphNode(subgraphA) + const nodeB = createTestSubgraphNode(subgraphB) - parentGraph.add(nodeA) - parentGraph.add(nodeB) + parentGraph.add(nodeA) + parentGraph.add(nodeB) - expect(nodeA.graph).toBe(parentGraph) - expect(nodeB.graph).toBe(parentGraph) - expect(parentGraph.nodes.length).toBe(3) // Original + nodeA + nodeB + expect(nodeA.graph).toBe(parentGraph) + expect(nodeB.graph).toBe(parentGraph) + expect(parentGraph.nodes.length).toBe(3) // Original + nodeA + nodeB - parentGraph.remove(nodeA) - parentGraph.remove(nodeB) + parentGraph.remove(nodeA) + parentGraph.remove(nodeB) - expect(parentGraph.nodes.length).toBe(1) // Only the original subgraphNode remains - }) + expect(parentGraph.nodes.length).toBe(1) // Only the original subgraphNode remains + } + ) - it("handles many instances without issues", () => { + it('handles many instances without issues', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "stress_input", type: "number" }], - outputs: [{ name: "stress_output", type: "number" }], + inputs: [{ name: 'stress_input', type: 'number' }], + outputs: [{ name: 'stress_output', type: 'number' }] }) const rootGraph = new LGraph() @@ -401,7 +428,7 @@ describe("SubgraphMemory - Performance and Scale", () => { expect(rootGraph.nodes.length).toBe(0) }) - it("maintains consistent behavior across multiple cycles", () => { + it('maintains consistent behavior across multiple cycles', () => { const subgraph = createTestSubgraph() const rootGraph = new LGraph() diff --git a/src/lib/litegraph/test/subgraph/SubgraphNode.test.ts b/src/lib/litegraph/test/subgraph/SubgraphNode.test.ts index 392caeaf5a..1e4ffdf877 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphNode.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphNode.test.ts @@ -4,23 +4,22 @@ * Tests for SubgraphNode instances including construction, * IO synchronization, and edge cases. */ +import { describe, expect, it, vi } from 'vitest' -import { describe, expect, it, vi } from "vitest" +import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph' -import { LGraph, Subgraph } from "@/lib/litegraph/src/litegraph" - -import { subgraphTest } from "./fixtures/subgraphFixtures" +import { subgraphTest } from './fixtures/subgraphFixtures' import { createTestSubgraph, - createTestSubgraphNode, -} from "./fixtures/subgraphHelpers" + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("SubgraphNode Construction", () => { - it("should create a SubgraphNode from a subgraph definition", () => { +describe('SubgraphNode Construction', () => { + it('should create a SubgraphNode from a subgraph definition', () => { const subgraph = createTestSubgraph({ - name: "Test Definition", - inputs: [{ name: "input", type: "number" }], - outputs: [{ name: "output", type: "number" }], + name: 'Test Definition', + inputs: [{ name: 'input', type: 'number' }], + outputs: [{ name: 'output', type: 'number' }] }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -29,19 +28,19 @@ describe("SubgraphNode Construction", () => { expect(subgraphNode.subgraph).toBe(subgraph) expect(subgraphNode.type).toBe(subgraph.id) expect(subgraphNode.isVirtualNode).toBe(true) - expect(subgraphNode.displayType).toBe("Subgraph node") + expect(subgraphNode.displayType).toBe('Subgraph node') }) - it("should configure from instance data", () => { + it('should configure from instance data', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "value", type: "number" }], - outputs: [{ name: "result", type: "number" }], + inputs: [{ name: 'value', type: 'number' }], + outputs: [{ name: 'result', type: 'number' }] }) const subgraphNode = createTestSubgraphNode(subgraph, { id: 42, pos: [300, 150], - size: [180, 80], + size: [180, 80] }) expect(subgraphNode.id).toBe(42) @@ -49,7 +48,7 @@ describe("SubgraphNode Construction", () => { expect(Array.from(subgraphNode.size)).toEqual([180, 80]) }) - it("should maintain reference to root graph", () => { + it('should maintain reference to root graph', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) const parentGraph = subgraphNode.graph @@ -57,62 +56,68 @@ describe("SubgraphNode Construction", () => { expect(subgraphNode.rootGraph).toBe(parentGraph.rootGraph) }) - subgraphTest("should synchronize slots with subgraph definition", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode } = subgraphWithNode + subgraphTest( + 'should synchronize slots with subgraph definition', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode } = subgraphWithNode - // SubgraphNode should have same number of inputs/outputs as definition - expect(subgraphNode.inputs).toHaveLength(subgraph.inputs.length) - expect(subgraphNode.outputs).toHaveLength(subgraph.outputs.length) - }) + // SubgraphNode should have same number of inputs/outputs as definition + expect(subgraphNode.inputs).toHaveLength(subgraph.inputs.length) + expect(subgraphNode.outputs).toHaveLength(subgraph.outputs.length) + } + ) - subgraphTest("should update slots when subgraph definition changes", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode } = subgraphWithNode + subgraphTest( + 'should update slots when subgraph definition changes', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode } = subgraphWithNode - const initialInputCount = subgraphNode.inputs.length + const initialInputCount = subgraphNode.inputs.length - // Add an input to the subgraph definition - subgraph.addInput("new_input", "string") + // Add an input to the subgraph definition + subgraph.addInput('new_input', 'string') - // SubgraphNode should automatically update (this tests the event system) - expect(subgraphNode.inputs).toHaveLength(initialInputCount + 1) - expect(subgraphNode.inputs.at(-1)?.name).toBe("new_input") - expect(subgraphNode.inputs.at(-1)?.type).toBe("string") - }) + // SubgraphNode should automatically update (this tests the event system) + expect(subgraphNode.inputs).toHaveLength(initialInputCount + 1) + expect(subgraphNode.inputs.at(-1)?.name).toBe('new_input') + expect(subgraphNode.inputs.at(-1)?.type).toBe('string') + } + ) }) -describe("SubgraphNode Synchronization", () => { - it("should sync input addition", () => { +describe('SubgraphNode Synchronization', () => { + it('should sync input addition', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) expect(subgraphNode.inputs).toHaveLength(0) - subgraph.addInput("value", "number") + subgraph.addInput('value', 'number') expect(subgraphNode.inputs).toHaveLength(1) - expect(subgraphNode.inputs[0].name).toBe("value") - expect(subgraphNode.inputs[0].type).toBe("number") + expect(subgraphNode.inputs[0].name).toBe('value') + expect(subgraphNode.inputs[0].type).toBe('number') }) - it("should sync output addition", () => { + it('should sync output addition', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) expect(subgraphNode.outputs).toHaveLength(0) - subgraph.addOutput("result", "string") + subgraph.addOutput('result', 'string') expect(subgraphNode.outputs).toHaveLength(1) - expect(subgraphNode.outputs[0].name).toBe("result") - expect(subgraphNode.outputs[0].type).toBe("string") + expect(subgraphNode.outputs[0].name).toBe('result') + expect(subgraphNode.outputs[0].type).toBe('string') }) - it("should sync input removal", () => { + it('should sync input removal', () => { const subgraph = createTestSubgraph({ inputs: [ - { name: "input1", type: "number" }, - { name: "input2", type: "string" }, - ], + { name: 'input1', type: 'number' }, + { name: 'input2', type: 'string' } + ] }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -121,15 +126,15 @@ describe("SubgraphNode Synchronization", () => { subgraph.removeInput(subgraph.inputs[0]) expect(subgraphNode.inputs).toHaveLength(1) - expect(subgraphNode.inputs[0].name).toBe("input2") + expect(subgraphNode.inputs[0].name).toBe('input2') }) - it("should sync output removal", () => { + it('should sync output removal', () => { const subgraph = createTestSubgraph({ outputs: [ - { name: "output1", type: "number" }, - { name: "output2", type: "string" }, - ], + { name: 'output1', type: 'number' }, + { name: 'output2', type: 'string' } + ] }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -138,42 +143,42 @@ describe("SubgraphNode Synchronization", () => { subgraph.removeOutput(subgraph.outputs[0]) expect(subgraphNode.outputs).toHaveLength(1) - expect(subgraphNode.outputs[0].name).toBe("output2") + expect(subgraphNode.outputs[0].name).toBe('output2') }) - it("should sync slot renaming", () => { + it('should sync slot renaming', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "oldName", type: "number" }], - outputs: [{ name: "oldOutput", type: "string" }], + inputs: [{ name: 'oldName', type: 'number' }], + outputs: [{ name: 'oldOutput', type: 'string' }] }) const subgraphNode = createTestSubgraphNode(subgraph) // Rename input - subgraph.inputs[0].label = "newName" - subgraph.events.dispatch("renaming-input", { + subgraph.inputs[0].label = 'newName' + subgraph.events.dispatch('renaming-input', { input: subgraph.inputs[0], index: 0, - oldName: "oldName", - newName: "newName", + oldName: 'oldName', + newName: 'newName' }) - expect(subgraphNode.inputs[0].label).toBe("newName") + expect(subgraphNode.inputs[0].label).toBe('newName') // Rename output - subgraph.outputs[0].label = "newOutput" - subgraph.events.dispatch("renaming-output", { + subgraph.outputs[0].label = 'newOutput' + subgraph.events.dispatch('renaming-output', { output: subgraph.outputs[0], index: 0, - oldName: "oldOutput", - newName: "newOutput", + oldName: 'oldOutput', + newName: 'newOutput' }) - expect(subgraphNode.outputs[0].label).toBe("newOutput") + expect(subgraphNode.outputs[0].label).toBe('newOutput') }) }) -describe("SubgraphNode Lifecycle", () => { - it("should initialize with empty widgets array", () => { +describe('SubgraphNode Lifecycle', () => { + it('should initialize with empty widgets array', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) @@ -181,10 +186,10 @@ describe("SubgraphNode Lifecycle", () => { expect(subgraphNode.widgets).toHaveLength(0) }) - it("should handle reconfiguration", () => { + it('should handle reconfiguration', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input1", type: "number" }], - outputs: [{ name: "output1", type: "string" }], + inputs: [{ name: 'input1', type: 'number' }], + outputs: [{ name: 'output1', type: 'string' }] }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -193,8 +198,8 @@ describe("SubgraphNode Lifecycle", () => { expect(subgraphNode.outputs).toHaveLength(1) // Add more slots to subgraph - subgraph.addInput("input2", "string") - subgraph.addOutput("output2", "number") + subgraph.addInput('input2', 'string') + subgraph.addOutput('output2', 'number') // Reconfigure subgraphNode.configure({ @@ -206,7 +211,7 @@ describe("SubgraphNode Lifecycle", () => { outputs: [], properties: {}, flags: {}, - mode: 0, + mode: 0 }) // Should reflect updated subgraph structure @@ -214,7 +219,7 @@ describe("SubgraphNode Lifecycle", () => { expect(subgraphNode.outputs).toHaveLength(2) }) - it("should handle removal lifecycle", () => { + it('should handle removal lifecycle', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) const parentGraph = new LGraph() @@ -231,8 +236,8 @@ describe("SubgraphNode Lifecycle", () => { }) }) -describe("SubgraphNode Basic Functionality", () => { - it("should identify as subgraph node", () => { +describe('SubgraphNode Basic Functionality', () => { + it('should identify as subgraph node', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) @@ -240,39 +245,39 @@ describe("SubgraphNode Basic Functionality", () => { expect(subgraphNode.isVirtualNode).toBe(true) }) - it("should inherit input types correctly", () => { + it('should inherit input types correctly', () => { const subgraph = createTestSubgraph({ inputs: [ - { name: "numberInput", type: "number" }, - { name: "stringInput", type: "string" }, - { name: "anyInput", type: "*" }, - ], + { name: 'numberInput', type: 'number' }, + { name: 'stringInput', type: 'string' }, + { name: 'anyInput', type: '*' } + ] }) const subgraphNode = createTestSubgraphNode(subgraph) - expect(subgraphNode.inputs[0].type).toBe("number") - expect(subgraphNode.inputs[1].type).toBe("string") - expect(subgraphNode.inputs[2].type).toBe("*") + expect(subgraphNode.inputs[0].type).toBe('number') + expect(subgraphNode.inputs[1].type).toBe('string') + expect(subgraphNode.inputs[2].type).toBe('*') }) - it("should inherit output types correctly", () => { + it('should inherit output types correctly', () => { const subgraph = createTestSubgraph({ outputs: [ - { name: "numberOutput", type: "number" }, - { name: "stringOutput", type: "string" }, - { name: "anyOutput", type: "*" }, - ], + { name: 'numberOutput', type: 'number' }, + { name: 'stringOutput', type: 'string' }, + { name: 'anyOutput', type: '*' } + ] }) const subgraphNode = createTestSubgraphNode(subgraph) - expect(subgraphNode.outputs[0].type).toBe("number") - expect(subgraphNode.outputs[1].type).toBe("string") - expect(subgraphNode.outputs[2].type).toBe("*") + expect(subgraphNode.outputs[0].type).toBe('number') + expect(subgraphNode.outputs[1].type).toBe('string') + expect(subgraphNode.outputs[2].type).toBe('*') }) }) -describe("SubgraphNode Execution", () => { - it("should flatten to ExecutableNodeDTOs", () => { +describe('SubgraphNode Execution', () => { + it('should flatten to ExecutableNodeDTOs', () => { const subgraph = createTestSubgraph({ nodeCount: 3 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -285,24 +290,26 @@ describe("SubgraphNode Execution", () => { expect(flattened[2].id).toMatch(/^1:\d+$/) }) - it.skip("should handle nested subgraph execution", () => { + it.skip('should handle nested subgraph execution', () => { // FIXME: Complex nested structure requires proper parent graph setup // Skip for now - similar issue to ExecutableNodeDTO nested test // Will implement proper nested execution test in edge cases file const childSubgraph = createTestSubgraph({ - name: "Child", - nodeCount: 1, + name: 'Child', + nodeCount: 1 }) const parentSubgraph = createTestSubgraph({ - name: "Parent", - nodeCount: 1, + name: 'Parent', + nodeCount: 1 }) const childSubgraphNode = createTestSubgraphNode(childSubgraph, { id: 42 }) parentSubgraph.add(childSubgraphNode) - const parentSubgraphNode = createTestSubgraphNode(parentSubgraph, { id: 10 }) + const parentSubgraphNode = createTestSubgraphNode(parentSubgraph, { + id: 10 + }) const executableNodes = new Map() const flattened = parentSubgraphNode.getInnerNodes(executableNodes) @@ -310,10 +317,10 @@ describe("SubgraphNode Execution", () => { expect(flattened.length).toBeGreaterThan(0) }) - it("should resolve cross-boundary input links", () => { + it('should resolve cross-boundary input links', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input1", type: "number" }], - nodeCount: 1, + inputs: [{ name: 'input1', type: 'number' }], + nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -323,20 +330,20 @@ describe("SubgraphNode Execution", () => { expect(Array.isArray(resolved)).toBe(true) }) - it("should resolve cross-boundary output links", () => { + it('should resolve cross-boundary output links', () => { const subgraph = createTestSubgraph({ - outputs: [{ name: "output1", type: "number" }], - nodeCount: 1, + outputs: [{ name: 'output1', type: 'number' }], + nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph) const resolved = subgraphNode.resolveSubgraphOutputLink(0) // May be undefined if no internal connection exists - expect(resolved === undefined || typeof resolved === "object").toBe(true) + expect(resolved === undefined || typeof resolved === 'object').toBe(true) }) - it("should prevent infinite recursion", () => { + it('should prevent infinite recursion', () => { // Cycle detection properly prevents infinite recursion when a subgraph contains itself const subgraph = createTestSubgraph({ nodeCount: 1 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -347,15 +354,17 @@ describe("SubgraphNode Execution", () => { const executableNodes = new Map() expect(() => { subgraphNode.getInnerNodes(executableNodes) - }).toThrow(/Circular reference detected.*infinite loop in the subgraph hierarchy/i) + }).toThrow( + /Circular reference detected.*infinite loop in the subgraph hierarchy/i + ) }) - it("should handle nested subgraph execution", () => { + it('should handle nested subgraph execution', () => { // This test verifies that subgraph nodes can be properly executed // when they contain other nodes and produce correct output const subgraph = createTestSubgraph({ - name: "Nested Execution Test", - nodeCount: 3, + name: 'Nested Execution Test', + nodeCount: 3 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -368,21 +377,21 @@ describe("SubgraphNode Execution", () => { // Each DTO should have proper execution context for (const dto of flattened) { - expect(dto).toHaveProperty("id") - expect(dto).toHaveProperty("graph") - expect(dto).toHaveProperty("inputs") + expect(dto).toHaveProperty('id') + expect(dto).toHaveProperty('graph') + expect(dto).toHaveProperty('inputs') expect(dto.id).toMatch(/^\d+:\d+$/) // Path-based ID format } }) - it("should resolve cross-boundary links", () => { + it('should resolve cross-boundary links', () => { // This test verifies that links can cross subgraph boundaries // Currently this is a basic test - full cross-boundary linking // requires more complex setup with actual connected nodes const subgraph = createTestSubgraph({ - inputs: [{ name: "external_input", type: "number" }], - outputs: [{ name: "external_output", type: "number" }], - nodeCount: 2, + inputs: [{ name: 'external_input', type: 'number' }], + outputs: [{ name: 'external_output', type: 'number' }], + nodeCount: 2 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -390,8 +399,8 @@ describe("SubgraphNode Execution", () => { // Verify the subgraph node has the expected I/O structure for cross-boundary links expect(subgraphNode.inputs).toHaveLength(1) expect(subgraphNode.outputs).toHaveLength(1) - expect(subgraphNode.inputs[0].name).toBe("external_input") - expect(subgraphNode.outputs[0].name).toBe("external_output") + expect(subgraphNode.inputs[0].name).toBe('external_input') + expect(subgraphNode.outputs[0].name).toBe('external_output') // Internal nodes should be flattened correctly const executableNodes = new Map() @@ -400,12 +409,12 @@ describe("SubgraphNode Execution", () => { }) }) -describe("SubgraphNode Edge Cases", () => { - it("should handle deep nesting", () => { +describe('SubgraphNode Edge Cases', () => { + it('should handle deep nesting', () => { // Create a simpler deep nesting test that works with current implementation const subgraph = createTestSubgraph({ - name: "Deep Test", - nodeCount: 5, // Multiple nodes to test flattening at depth + name: 'Deep Test', + nodeCount: 5 // Multiple nodes to test flattening at depth }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -425,7 +434,7 @@ describe("SubgraphNode Edge Cases", () => { } }) - it("should validate against MAX_NESTED_SUBGRAPHS", () => { + it('should validate against MAX_NESTED_SUBGRAPHS', () => { // Test that the MAX_NESTED_SUBGRAPHS constant exists // Note: Currently not enforced in the implementation expect(Subgraph.MAX_NESTED_SUBGRAPHS).toBe(1000) @@ -435,8 +444,8 @@ describe("SubgraphNode Edge Cases", () => { }) }) -describe("SubgraphNode Integration", () => { - it("should be addable to a parent graph", () => { +describe('SubgraphNode Integration', () => { + it('should be addable to a parent graph', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) const parentGraph = new LGraph() @@ -447,15 +456,18 @@ describe("SubgraphNode Integration", () => { expect(subgraphNode.graph).toBe(parentGraph) }) - subgraphTest("should maintain reference to root graph", ({ subgraphWithNode }) => { - const { subgraphNode } = subgraphWithNode + subgraphTest( + 'should maintain reference to root graph', + ({ subgraphWithNode }) => { + const { subgraphNode } = subgraphWithNode - // For this test, parentGraph should be the root, but in nested scenarios - // it would traverse up to find the actual root - expect(subgraphNode.rootGraph).toBeDefined() - }) + // For this test, parentGraph should be the root, but in nested scenarios + // it would traverse up to find the actual root + expect(subgraphNode.rootGraph).toBeDefined() + } + ) - it("should handle graph removal properly", () => { + it('should handle graph removal properly', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) const parentGraph = new LGraph() @@ -468,33 +480,36 @@ describe("SubgraphNode Integration", () => { }) }) -describe("Foundation Test Utilities", () => { - it("should create test SubgraphNodes with custom options", () => { +describe('Foundation Test Utilities', () => { + it('should create test SubgraphNodes with custom options', () => { const subgraph = createTestSubgraph() const customPos: [number, number] = [500, 300] const customSize: [number, number] = [250, 120] const subgraphNode = createTestSubgraphNode(subgraph, { pos: customPos, - size: customSize, + size: customSize }) expect(Array.from(subgraphNode.pos)).toEqual(customPos) expect(Array.from(subgraphNode.size)).toEqual(customSize) }) - subgraphTest("fixtures should provide properly configured SubgraphNode", ({ subgraphWithNode }) => { - const { subgraph, subgraphNode, parentGraph } = subgraphWithNode + subgraphTest( + 'fixtures should provide properly configured SubgraphNode', + ({ subgraphWithNode }) => { + const { subgraph, subgraphNode, parentGraph } = subgraphWithNode - expect(subgraph).toBeDefined() - expect(subgraphNode).toBeDefined() - expect(parentGraph).toBeDefined() - expect(parentGraph.nodes).toContain(subgraphNode) - }) + expect(subgraph).toBeDefined() + expect(subgraphNode).toBeDefined() + expect(parentGraph).toBeDefined() + expect(parentGraph.nodes).toContain(subgraphNode) + } + ) }) -describe("SubgraphNode Cleanup", () => { - it("should clean up event listeners when removed", () => { +describe('SubgraphNode Cleanup', () => { + it('should clean up event listeners when removed', () => { const rootGraph = new LGraph() const subgraph = createTestSubgraph() @@ -512,8 +527,8 @@ describe("SubgraphNode Cleanup", () => { rootGraph.remove(node2) // Now trigger an event - only node1 should respond - subgraph.events.dispatch("input-added", { - input: { name: "test", type: "number", id: "test-id" } as any, + subgraph.events.dispatch('input-added', { + input: { name: 'test', type: 'number', id: 'test-id' } as any }) // Only node1 should have added an input @@ -521,7 +536,7 @@ describe("SubgraphNode Cleanup", () => { expect(node2.inputs.length).toBe(0) // node2 should NOT respond (but currently does) }) - it("should not accumulate handlers over multiple add/remove cycles", () => { + it('should not accumulate handlers over multiple add/remove cycles', () => { const rootGraph = new LGraph() const subgraph = createTestSubgraph() @@ -540,8 +555,8 @@ describe("SubgraphNode Cleanup", () => { } // Trigger an event - no nodes should respond - subgraph.events.dispatch("input-added", { - input: { name: "test", type: "number", id: "test-id" } as any, + subgraph.events.dispatch('input-added', { + input: { name: 'test', type: 'number', id: 'test-id' } as any }) // Without cleanup: all 3 removed nodes would have added an input @@ -551,10 +566,13 @@ describe("SubgraphNode Cleanup", () => { } }) - it("should clean up input listener controllers on removal", () => { + it('should clean up input listener controllers on removal', () => { const rootGraph = new LGraph() const subgraph = createTestSubgraph({ - inputs: [{ name: "in1", type: "number" }, { name: "in2", type: "string" }], + inputs: [ + { name: 'in1', type: 'number' }, + { name: 'in2', type: 'string' } + ] }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -565,8 +583,14 @@ describe("SubgraphNode Cleanup", () => { expect(subgraphNode.inputs[1]._listenerController).toBeDefined() // Track abort calls - const abortSpy1 = vi.spyOn(subgraphNode.inputs[0]._listenerController!, "abort") - const abortSpy2 = vi.spyOn(subgraphNode.inputs[1]._listenerController!, "abort") + const abortSpy1 = vi.spyOn( + subgraphNode.inputs[0]._listenerController!, + 'abort' + ) + const abortSpy2 = vi.spyOn( + subgraphNode.inputs[1]._listenerController!, + 'abort' + ) // Remove node rootGraph.remove(subgraphNode) diff --git a/src/lib/litegraph/test/subgraph/SubgraphNode.titleButton.test.ts b/src/lib/litegraph/test/subgraph/SubgraphNode.titleButton.test.ts index d43fa5650e..94355cb72f 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphNode.titleButton.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphNode.titleButton.test.ts @@ -1,16 +1,19 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from 'vitest' -import { LGraphButton } from "@/lib/litegraph/src/LGraphButton" -import { LGraphCanvas } from "@/lib/litegraph/src/LGraphCanvas" +import { LGraphButton } from '@/lib/litegraph/src/LGraphButton' +import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' -import { createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers" +import { + createTestSubgraph, + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("SubgraphNode Title Button", () => { - describe("Constructor", () => { - it("should automatically add enter_subgraph button", () => { +describe('SubgraphNode Title Button', () => { + describe('Constructor', () => { + it('should automatically add enter_subgraph button', () => { const subgraph = createTestSubgraph({ - name: "Test Subgraph", - inputs: [{ name: "input", type: "number" }], + name: 'Test Subgraph', + inputs: [{ name: 'input', type: 'number' }] }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -19,33 +22,33 @@ describe("SubgraphNode Title Button", () => { const button = subgraphNode.title_buttons[0] expect(button).toBeInstanceOf(LGraphButton) - expect(button.name).toBe("enter_subgraph") - expect(button.text).toBe("\uE93B") // pi-window-maximize + expect(button.name).toBe('enter_subgraph') + expect(button.text).toBe('\uE93B') // pi-window-maximize expect(button.xOffset).toBe(-10) expect(button.yOffset).toBe(0) expect(button.fontSize).toBe(16) }) - it("should preserve enter_subgraph button when adding more buttons", () => { + it('should preserve enter_subgraph button when adding more buttons', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) // Add another button const customButton = subgraphNode.addTitleButton({ - name: "custom_button", - text: "C", + name: 'custom_button', + text: 'C' }) expect(subgraphNode.title_buttons).toHaveLength(2) - expect(subgraphNode.title_buttons[0].name).toBe("enter_subgraph") + expect(subgraphNode.title_buttons[0].name).toBe('enter_subgraph') expect(subgraphNode.title_buttons[1]).toBe(customButton) }) }) - describe("onTitleButtonClick", () => { - it("should open subgraph when enter_subgraph button is clicked", () => { + describe('onTitleButtonClick', () => { + it('should open subgraph when enter_subgraph button is clicked', () => { const subgraph = createTestSubgraph({ - name: "Test Subgraph", + name: 'Test Subgraph' }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -53,7 +56,7 @@ describe("SubgraphNode Title Button", () => { const canvas = { openSubgraph: vi.fn(), - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas subgraphNode.onTitleButtonClick(enterButton, canvas) @@ -62,35 +65,38 @@ describe("SubgraphNode Title Button", () => { expect(canvas.dispatch).not.toHaveBeenCalled() // Should not call parent implementation }) - it("should call parent implementation for other buttons", () => { + it('should call parent implementation for other buttons', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) const customButton = subgraphNode.addTitleButton({ - name: "custom_button", - text: "X", + name: 'custom_button', + text: 'X' }) const canvas = { openSubgraph: vi.fn(), - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas subgraphNode.onTitleButtonClick(customButton, canvas) expect(canvas.openSubgraph).not.toHaveBeenCalled() - expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", { - node: subgraphNode, - button: customButton, - }) + expect(canvas.dispatch).toHaveBeenCalledWith( + 'litegraph:node-title-button-clicked', + { + node: subgraphNode, + button: customButton + } + ) }) }) - describe("Integration with node click handling", () => { - it("should handle clicks on enter_subgraph button", () => { + describe('Integration with node click handling', () => { + it('should handle clicks on enter_subgraph button', () => { const subgraph = createTestSubgraph({ - name: "Nested Subgraph", - nodeCount: 3, + name: 'Nested Subgraph', + nodeCount: 3 }) const subgraphNode = createTestSubgraphNode(subgraph) @@ -111,31 +117,35 @@ describe("SubgraphNode Title Button", () => { const canvas = { ctx: { - measureText: vi.fn().mockReturnValue({ width: 25 }), + measureText: vi.fn().mockReturnValue({ width: 25 }) } as unknown as CanvasRenderingContext2D, openSubgraph: vi.fn(), - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas // Simulate click on the enter button const event = { canvasX: 275, // Near right edge where button should be - canvasY: 80, // In title area + canvasY: 80 // In title area } as any // Calculate node-relative position const clickPosRelativeToNode: [number, number] = [ 275 - subgraphNode.pos[0], // 275 - 100 = 175 - 80 - subgraphNode.pos[1], // 80 - 100 = -20 + 80 - subgraphNode.pos[1] // 80 - 100 = -20 ] - const handled = subgraphNode.onMouseDown(event, clickPosRelativeToNode, canvas) + const handled = subgraphNode.onMouseDown( + event, + clickPosRelativeToNode, + canvas + ) expect(handled).toBe(true) expect(canvas.openSubgraph).toHaveBeenCalledWith(subgraph) }) - it("should not interfere with normal node operations", () => { + it('should not interfere with normal node operations', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) subgraphNode.pos = [100, 100] @@ -143,31 +153,35 @@ describe("SubgraphNode Title Button", () => { const canvas = { ctx: { - measureText: vi.fn().mockReturnValue({ width: 25 }), + measureText: vi.fn().mockReturnValue({ width: 25 }) } as unknown as CanvasRenderingContext2D, openSubgraph: vi.fn(), - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas // Click in the body of the node, not on button const event = { canvasX: 200, // Middle of node - canvasY: 150, // Body area + canvasY: 150 // Body area } as any // Calculate node-relative position const clickPosRelativeToNode: [number, number] = [ 200 - subgraphNode.pos[0], // 200 - 100 = 100 - 150 - subgraphNode.pos[1], // 150 - 100 = 50 + 150 - subgraphNode.pos[1] // 150 - 100 = 50 ] - const handled = subgraphNode.onMouseDown(event, clickPosRelativeToNode, canvas) + const handled = subgraphNode.onMouseDown( + event, + clickPosRelativeToNode, + canvas + ) expect(handled).toBe(false) expect(canvas.openSubgraph).not.toHaveBeenCalled() }) - it("should not process button clicks when node is collapsed", () => { + it('should not process button clicks when node is collapsed', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) subgraphNode.pos = [100, 100] @@ -186,24 +200,28 @@ describe("SubgraphNode Title Button", () => { const canvas = { ctx: { - measureText: vi.fn().mockReturnValue({ width: 25 }), + measureText: vi.fn().mockReturnValue({ width: 25 }) } as unknown as CanvasRenderingContext2D, openSubgraph: vi.fn(), - dispatch: vi.fn(), + dispatch: vi.fn() } as unknown as LGraphCanvas // Try to click on where the button would be const event = { canvasX: 275, - canvasY: 80, + canvasY: 80 } as any const clickPosRelativeToNode: [number, number] = [ 275 - subgraphNode.pos[0], // 175 - 80 - subgraphNode.pos[1], // -20 + 80 - subgraphNode.pos[1] // -20 ] - const handled = subgraphNode.onMouseDown(event, clickPosRelativeToNode, canvas) + const handled = subgraphNode.onMouseDown( + event, + clickPosRelativeToNode, + canvas + ) // Should not handle the click when collapsed expect(handled).toBe(false) @@ -211,15 +229,15 @@ describe("SubgraphNode Title Button", () => { }) }) - describe("Visual properties", () => { - it("should have appropriate visual properties for enter button", () => { + describe('Visual properties', () => { + it('should have appropriate visual properties for enter button', () => { const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) const enterButton = subgraphNode.title_buttons[0] // Check visual properties - expect(enterButton.text).toBe("\uE93B") // pi-window-maximize + expect(enterButton.text).toBe('\uE93B') // pi-window-maximize expect(enterButton.fontSize).toBe(16) // Icon size expect(enterButton.xOffset).toBe(-10) // Positioned from right edge expect(enterButton.yOffset).toBe(0) // Centered vertically diff --git a/src/lib/litegraph/test/subgraph/SubgraphSerialization.test.ts b/src/lib/litegraph/test/subgraph/SubgraphSerialization.test.ts index 087d464b7c..79844b8806 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphSerialization.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphSerialization.test.ts @@ -4,37 +4,36 @@ * Tests for saving, loading, and version compatibility of subgraphs. * This covers serialization, deserialization, data integrity, and migration scenarios. */ +import { describe, expect, it } from 'vitest' -import { describe, expect, it } from "vitest" - -import { LGraph, Subgraph } from "@/lib/litegraph/src/litegraph" +import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph' import { createTestSubgraph, - createTestSubgraphNode, -} from "./fixtures/subgraphHelpers" + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("SubgraphSerialization - Basic Serialization", () => { - it("should save and load simple subgraphs", () => { +describe('SubgraphSerialization - Basic Serialization', () => { + it('should save and load simple subgraphs', () => { const original = createTestSubgraph({ - name: "Simple Test", - nodeCount: 2, + name: 'Simple Test', + nodeCount: 2 }) - original.addInput("in1", "number") - original.addInput("in2", "string") - original.addOutput("out", "boolean") + original.addInput('in1', 'number') + original.addInput('in2', 'string') + original.addOutput('out', 'boolean') // Serialize const exported = original.asSerialisable() // Verify exported structure - expect(exported).toHaveProperty("id", original.id) - expect(exported).toHaveProperty("name", "Simple Test") - expect(exported).toHaveProperty("nodes") - expect(exported).toHaveProperty("links") - expect(exported).toHaveProperty("inputs") - expect(exported).toHaveProperty("outputs") - expect(exported).toHaveProperty("version") + expect(exported).toHaveProperty('id', original.id) + expect(exported).toHaveProperty('name', 'Simple Test') + expect(exported).toHaveProperty('nodes') + expect(exported).toHaveProperty('links') + expect(exported).toHaveProperty('inputs') + expect(exported).toHaveProperty('outputs') + expect(exported).toHaveProperty('version') // Create new instance from serialized data const restored = new Subgraph(new LGraph(), exported) @@ -48,26 +47,26 @@ describe("SubgraphSerialization - Basic Serialization", () => { // This is expected behavior - serialization preserves I/O but nodes need valid types // Verify input details - expect(restored.inputs[0].name).toBe("in1") - expect(restored.inputs[0].type).toBe("number") - expect(restored.inputs[1].name).toBe("in2") - expect(restored.inputs[1].type).toBe("string") - expect(restored.outputs[0].name).toBe("out") - expect(restored.outputs[0].type).toBe("boolean") + expect(restored.inputs[0].name).toBe('in1') + expect(restored.inputs[0].type).toBe('number') + expect(restored.inputs[1].name).toBe('in2') + expect(restored.inputs[1].type).toBe('string') + expect(restored.outputs[0].name).toBe('out') + expect(restored.outputs[0].type).toBe('boolean') }) - it("should verify all properties are preserved", () => { + it('should verify all properties are preserved', () => { const original = createTestSubgraph({ - name: "Property Test", + name: 'Property Test', nodeCount: 3, inputs: [ - { name: "input1", type: "number" }, - { name: "input2", type: "string" }, + { name: 'input1', type: 'number' }, + { name: 'input2', type: 'string' } ], outputs: [ - { name: "output1", type: "boolean" }, - { name: "output2", type: "array" }, - ], + { name: 'output1', type: 'boolean' }, + { name: 'output2', type: 'array' } + ] }) const exported = original.asSerialisable() @@ -95,18 +94,18 @@ describe("SubgraphSerialization - Basic Serialization", () => { } }) - it("should test export() and configure() methods", () => { + it('should test export() and configure() methods', () => { const subgraph = createTestSubgraph({ nodeCount: 1 }) - subgraph.addInput("test_input", "number") - subgraph.addOutput("test_output", "string") + subgraph.addInput('test_input', 'number') + subgraph.addOutput('test_output', 'string') // Test export const exported = subgraph.asSerialisable() - expect(exported).toHaveProperty("id") - expect(exported).toHaveProperty("nodes") - expect(exported).toHaveProperty("links") - expect(exported).toHaveProperty("inputs") - expect(exported).toHaveProperty("outputs") + expect(exported).toHaveProperty('id') + expect(exported).toHaveProperty('nodes') + expect(exported).toHaveProperty('links') + expect(exported).toHaveProperty('inputs') + expect(exported).toHaveProperty('outputs') // Test configure with partial data const newSubgraph = createTestSubgraph({ nodeCount: 0 }) @@ -117,26 +116,26 @@ describe("SubgraphSerialization - Basic Serialization", () => { // Verify configuration applied expect(newSubgraph.inputs.length).toBe(1) expect(newSubgraph.outputs.length).toBe(1) - expect(newSubgraph.inputs[0].name).toBe("test_input") - expect(newSubgraph.outputs[0].name).toBe("test_output") + expect(newSubgraph.inputs[0].name).toBe('test_input') + expect(newSubgraph.outputs[0].name).toBe('test_output') }) }) -describe("SubgraphSerialization - Complex Serialization", () => { - it("should serialize nested subgraphs with multiple levels", () => { +describe('SubgraphSerialization - Complex Serialization', () => { + it('should serialize nested subgraphs with multiple levels', () => { // Create a nested structure const childSubgraph = createTestSubgraph({ - name: "Child", + name: 'Child', nodeCount: 2, - inputs: [{ name: "child_in", type: "number" }], - outputs: [{ name: "child_out", type: "string" }], + inputs: [{ name: 'child_in', type: 'number' }], + outputs: [{ name: 'child_out', type: 'string' }] }) const parentSubgraph = createTestSubgraph({ - name: "Parent", + name: 'Parent', nodeCount: 1, - inputs: [{ name: "parent_in", type: "boolean" }], - outputs: [{ name: "parent_out", type: "array" }], + inputs: [{ name: 'parent_in', type: 'boolean' }], + outputs: [{ name: 'parent_out', type: 'array' }] }) // Add child to parent @@ -148,30 +147,30 @@ describe("SubgraphSerialization - Complex Serialization", () => { const parentExported = parentSubgraph.asSerialisable() // Verify both can be serialized - expect(childExported).toHaveProperty("name", "Child") - expect(parentExported).toHaveProperty("name", "Parent") + expect(childExported).toHaveProperty('name', 'Child') + expect(parentExported).toHaveProperty('name', 'Parent') expect(parentExported.nodes.length).toBe(2) // 1 original + 1 child subgraph // Restore and verify const restoredChild = new Subgraph(new LGraph(), childExported) const restoredParent = new Subgraph(new LGraph(), parentExported) - expect(restoredChild.name).toBe("Child") - expect(restoredParent.name).toBe("Parent") + expect(restoredChild.name).toBe('Child') + expect(restoredParent.name).toBe('Parent') expect(restoredChild.inputs.length).toBe(1) expect(restoredParent.inputs.length).toBe(1) }) - it("should serialize subgraphs with many nodes and connections", () => { + it('should serialize subgraphs with many nodes and connections', () => { const largeSubgraph = createTestSubgraph({ - name: "Large Subgraph", - nodeCount: 10, // Many nodes + name: 'Large Subgraph', + nodeCount: 10 // Many nodes }) // Add many I/O slots for (let i = 0; i < 5; i++) { - largeSubgraph.addInput(`input_${i}`, "number") - largeSubgraph.addOutput(`output_${i}`, "string") + largeSubgraph.addInput(`input_${i}`, 'number') + largeSubgraph.addOutput(`output_${i}`, 'string') } const exported = largeSubgraph.asSerialisable() @@ -189,7 +188,7 @@ describe("SubgraphSerialization - Complex Serialization", () => { } }) - it("should preserve custom node data", () => { + it('should preserve custom node data', () => { const subgraph = createTestSubgraph({ nodeCount: 2 }) // Add custom properties to nodes (if supported) @@ -198,7 +197,7 @@ describe("SubgraphSerialization - Complex Serialization", () => { const firstNode = nodes[0] if (firstNode.properties) { firstNode.properties.customValue = 42 - firstNode.properties.customString = "test" + firstNode.properties.customString = 'test' } } @@ -217,52 +216,52 @@ describe("SubgraphSerialization - Complex Serialization", () => { }) }) -describe("SubgraphSerialization - Version Compatibility", () => { - it("should handle version field in exports", () => { +describe('SubgraphSerialization - Version Compatibility', () => { + it('should handle version field in exports', () => { const subgraph = createTestSubgraph({ nodeCount: 1 }) const exported = subgraph.asSerialisable() // Should have version field - expect(exported).toHaveProperty("version") - expect(typeof exported.version).toBe("number") + expect(exported).toHaveProperty('version') + expect(typeof exported.version).toBe('number') }) - it("should load version 1.0+ format", () => { + it('should load version 1.0+ format', () => { const modernFormat = { version: 1, // Number as expected by current implementation - id: "test-modern-id", - name: "Modern Subgraph", + id: 'test-modern-id', + name: 'Modern Subgraph', nodes: [], links: {}, groups: [], config: {}, definitions: { subgraphs: [] }, - inputs: [{ id: "input-id", name: "modern_input", type: "number" }], - outputs: [{ id: "output-id", name: "modern_output", type: "string" }], + inputs: [{ id: 'input-id', name: 'modern_input', type: 'number' }], + outputs: [{ id: 'output-id', name: 'modern_output', type: 'string' }], inputNode: { id: -10, - bounding: [0, 0, 120, 60], + bounding: [0, 0, 120, 60] }, outputNode: { id: -20, - bounding: [300, 0, 120, 60], + bounding: [300, 0, 120, 60] }, - widgets: [], + widgets: [] } expect(() => { const subgraph = new Subgraph(new LGraph(), modernFormat) - expect(subgraph.name).toBe("Modern Subgraph") + expect(subgraph.name).toBe('Modern Subgraph') expect(subgraph.inputs.length).toBe(1) expect(subgraph.outputs.length).toBe(1) }).not.toThrow() }) - it("should handle missing fields gracefully", () => { + it('should handle missing fields gracefully', () => { const incompleteFormat = { version: 1, - id: "incomplete-id", - name: "Incomplete Subgraph", + id: 'incomplete-id', + name: 'Incomplete Subgraph', nodes: [], links: {}, groups: [], @@ -270,29 +269,29 @@ describe("SubgraphSerialization - Version Compatibility", () => { definitions: { subgraphs: [] }, inputNode: { id: -10, - bounding: [0, 0, 120, 60], + bounding: [0, 0, 120, 60] }, outputNode: { id: -20, - bounding: [300, 0, 120, 60], - }, + bounding: [300, 0, 120, 60] + } // Missing optional: inputs, outputs, widgets } expect(() => { const subgraph = new Subgraph(new LGraph(), incompleteFormat) - expect(subgraph.name).toBe("Incomplete Subgraph") + expect(subgraph.name).toBe('Incomplete Subgraph') // Should have default empty arrays expect(Array.isArray(subgraph.inputs)).toBe(true) expect(Array.isArray(subgraph.outputs)).toBe(true) }).not.toThrow() }) - it("should consider future-proofing", () => { + it('should consider future-proofing', () => { const futureFormat = { version: 2, // Future version (number) - id: "future-id", - name: "Future Subgraph", + id: 'future-id', + name: 'Future Subgraph', nodes: [], links: {}, groups: [], @@ -302,34 +301,34 @@ describe("SubgraphSerialization - Version Compatibility", () => { outputs: [], inputNode: { id: -10, - bounding: [0, 0, 120, 60], + bounding: [0, 0, 120, 60] }, outputNode: { id: -20, - bounding: [300, 0, 120, 60], + bounding: [300, 0, 120, 60] }, widgets: [], - futureFeature: "unknown_data", // Unknown future field + futureFeature: 'unknown_data' // Unknown future field } // Should handle future format gracefully expect(() => { const subgraph = new Subgraph(new LGraph(), futureFormat) - expect(subgraph.name).toBe("Future Subgraph") + expect(subgraph.name).toBe('Future Subgraph') }).not.toThrow() }) }) -describe("SubgraphSerialization - Data Integrity", () => { - it("should pass round-trip testing (save → load → save → compare)", () => { +describe('SubgraphSerialization - Data Integrity', () => { + it('should pass round-trip testing (save → load → save → compare)', () => { const original = createTestSubgraph({ - name: "Round Trip Test", + name: 'Round Trip Test', nodeCount: 3, inputs: [ - { name: "rt_input1", type: "number" }, - { name: "rt_input2", type: "string" }, + { name: 'rt_input1', type: 'number' }, + { name: 'rt_input2', type: 'string' } ], - outputs: [{ name: "rt_output1", type: "boolean" }], + outputs: [{ name: 'rt_output1', type: 'boolean' }] }) // First round trip @@ -359,9 +358,9 @@ describe("SubgraphSerialization - Data Integrity", () => { } }) - it("should verify IDs remain unique", () => { - const subgraph1 = createTestSubgraph({ name: "Unique1", nodeCount: 2 }) - const subgraph2 = createTestSubgraph({ name: "Unique2", nodeCount: 2 }) + it('should verify IDs remain unique', () => { + const subgraph1 = createTestSubgraph({ name: 'Unique1', nodeCount: 2 }) + const subgraph2 = createTestSubgraph({ name: 'Unique2', nodeCount: 2 }) const exported1 = subgraph1.asSerialisable() const exported2 = subgraph2.asSerialisable() @@ -377,10 +376,10 @@ describe("SubgraphSerialization - Data Integrity", () => { expect(restored2.id).toBe(subgraph2.id) }) - it("should maintain connection integrity after load", () => { + it('should maintain connection integrity after load', () => { const subgraph = createTestSubgraph({ nodeCount: 2 }) - subgraph.addInput("connection_test", "number") - subgraph.addOutput("connection_result", "string") + subgraph.addInput('connection_test', 'number') + subgraph.addOutput('connection_result', 'string') const exported = subgraph.asSerialisable() const restored = new Subgraph(new LGraph(), exported) @@ -388,8 +387,8 @@ describe("SubgraphSerialization - Data Integrity", () => { // Verify I/O connections can be established expect(restored.inputs.length).toBe(1) expect(restored.outputs.length).toBe(1) - expect(restored.inputs[0].name).toBe("connection_test") - expect(restored.outputs[0].name).toBe("connection_result") + expect(restored.inputs[0].name).toBe('connection_test') + expect(restored.outputs[0].name).toBe('connection_result') // Verify subgraph can be instantiated const instance = createTestSubgraphNode(restored) @@ -397,16 +396,16 @@ describe("SubgraphSerialization - Data Integrity", () => { expect(instance.outputs.length).toBe(1) }) - it("should preserve node positions and properties", () => { + it('should preserve node positions and properties', () => { const subgraph = createTestSubgraph({ nodeCount: 2 }) // Modify node positions if possible if (subgraph.nodes.length > 0) { const node = subgraph.nodes[0] - if ("pos" in node) { + if ('pos' in node) { node.pos = [100, 200] } - if ("size" in node) { + if ('size' in node) { node.size = [150, 80] } } @@ -424,7 +423,7 @@ describe("SubgraphSerialization - Data Integrity", () => { expect(restoredNode).toBeDefined() // Properties should be preserved if supported - if ("pos" in restoredNode && restoredNode.pos) { + if ('pos' in restoredNode && restoredNode.pos) { expect(Array.isArray(restoredNode.pos)).toBe(true) } } diff --git a/src/lib/litegraph/test/subgraph/SubgraphSlotConnections.test.ts b/src/lib/litegraph/test/subgraph/SubgraphSlotConnections.test.ts index 69362d198f..17bb58d3b5 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphSlotConnections.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphSlotConnections.test.ts @@ -1,27 +1,33 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from 'vitest' -import { LinkConnector } from "@/lib/litegraph/src/canvas/LinkConnector" -import { ToInputFromIoNodeLink } from "@/lib/litegraph/src/canvas/ToInputFromIoNodeLink" -import { SUBGRAPH_INPUT_ID } from "@/lib/litegraph/src/constants" -import { LGraphNode, type LinkNetwork } from "@/lib/litegraph/src/litegraph" -import { NodeInputSlot } from "@/lib/litegraph/src/node/NodeInputSlot" -import { NodeOutputSlot } from "@/lib/litegraph/src/node/NodeOutputSlot" -import { isSubgraphInput, isSubgraphOutput } from "@/lib/litegraph/src/subgraph/subgraphUtils" +import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector' +import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/canvas/ToInputFromIoNodeLink' +import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/constants' +import { LGraphNode, type LinkNetwork } from '@/lib/litegraph/src/litegraph' +import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot' +import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot' +import { + isSubgraphInput, + isSubgraphOutput +} from '@/lib/litegraph/src/subgraph/subgraphUtils' -import { createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers" +import { + createTestSubgraph, + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("Subgraph slot connections", () => { - describe("SubgraphInput connections", () => { - it("should connect to compatible regular input slots", () => { +describe('Subgraph slot connections', () => { + describe('SubgraphInput connections', () => { + it('should connect to compatible regular input slots', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "test_input", type: "number" }], + inputs: [{ name: 'test_input', type: 'number' }] }) const subgraphInput = subgraph.inputs[0] - const node = new LGraphNode("TestNode") - node.addInput("compatible_input", "number") - node.addInput("incompatible_input", "string") + const node = new LGraphNode('TestNode') + node.addInput('compatible_input', 'number') + node.addInput('incompatible_input', 'string') subgraph.add(node) const compatibleSlot = node.inputs[0] as NodeInputSlot @@ -44,12 +50,12 @@ describe("Subgraph slot connections", () => { // expect(subgraphOutput.isValidTarget(subgraphInput)).toBe(true) // }) - it("should not connect to another SubgraphInput", () => { + it('should not connect to another SubgraphInput', () => { const subgraph = createTestSubgraph({ inputs: [ - { name: "input1", type: "number" }, - { name: "input2", type: "number" }, - ], + { name: 'input1', type: 'number' }, + { name: 'input2', type: 'number' } + ] }) const subgraphInput1 = subgraph.inputs[0] @@ -58,15 +64,15 @@ describe("Subgraph slot connections", () => { expect(subgraphInput2.isValidTarget(subgraphInput1)).toBe(false) }) - it("should not connect to output slots", () => { + it('should not connect to output slots', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "test_input", type: "number" }], + inputs: [{ name: 'test_input', type: 'number' }] }) const subgraphInput = subgraph.inputs[0] - const node = new LGraphNode("TestNode") - node.addOutput("test_output", "number") + const node = new LGraphNode('TestNode') + node.addOutput('test_output', 'number') subgraph.add(node) const outputSlot = node.outputs[0] as NodeOutputSlot @@ -74,53 +80,56 @@ describe("Subgraph slot connections", () => { }) }) - describe("SubgraphOutput connections", () => { - it("should connect from compatible regular output slots", () => { + describe('SubgraphOutput connections', () => { + it('should connect from compatible regular output slots', () => { const subgraph = createTestSubgraph() - const node = new LGraphNode("TestNode") - node.addOutput("out", "number") + const node = new LGraphNode('TestNode') + node.addOutput('out', 'number') subgraph.add(node) - const subgraphOutput = subgraph.addOutput("result", "number") + const subgraphOutput = subgraph.addOutput('result', 'number') const nodeOutput = node.outputs[0] expect(subgraphOutput.isValidTarget(nodeOutput)).toBe(true) }) - it("should connect from SubgraphInput", () => { + it('should connect from SubgraphInput', () => { const subgraph = createTestSubgraph() - const subgraphInput = subgraph.addInput("value", "number") - const subgraphOutput = subgraph.addOutput("result", "number") + const subgraphInput = subgraph.addInput('value', 'number') + const subgraphOutput = subgraph.addOutput('result', 'number') expect(subgraphOutput.isValidTarget(subgraphInput)).toBe(true) }) - it("should not connect to another SubgraphOutput", () => { + it('should not connect to another SubgraphOutput', () => { const subgraph = createTestSubgraph() - const subgraphOutput1 = subgraph.addOutput("result1", "number") - const subgraphOutput2 = subgraph.addOutput("result2", "number") + const subgraphOutput1 = subgraph.addOutput('result1', 'number') + const subgraphOutput2 = subgraph.addOutput('result2', 'number') expect(subgraphOutput1.isValidTarget(subgraphOutput2)).toBe(false) }) }) - describe("LinkConnector dragging behavior", () => { - it("should drag existing link when dragging from input slot connected to subgraph input node", () => { + describe('LinkConnector dragging behavior', () => { + it('should drag existing link when dragging from input slot connected to subgraph input node', () => { // Create a subgraph with one input const subgraph = createTestSubgraph({ - inputs: [{ name: "input1", type: "number" }], + inputs: [{ name: 'input1', type: 'number' }] }) // Create a node inside the subgraph - const internalNode = new LGraphNode("InternalNode") + const internalNode = new LGraphNode('InternalNode') internalNode.id = 100 - internalNode.addInput("in", "number") + internalNode.addInput('in', 'number') subgraph.add(internalNode) // Connect the subgraph input to the internal node's input - const link = subgraph.inputNode.slots[0].connect(internalNode.inputs[0], internalNode) + const link = subgraph.inputNode.slots[0].connect( + internalNode.inputs[0], + internalNode + ) expect(link).toBeDefined() expect(link!.origin_id).toBe(SUBGRAPH_INPUT_ID) expect(link!.target_id).toBe(internalNode.id) @@ -137,7 +146,7 @@ describe("Subgraph slot connections", () => { // Verify that we're dragging the existing link expect(connector.isConnecting).toBe(true) - expect(connector.state.connectingTo).toBe("input") + expect(connector.state.connectingTo).toBe('input') expect(connector.state.draggingExistingLinks).toBe(true) // Check that we have exactly one render link @@ -155,19 +164,19 @@ describe("Subgraph slot connections", () => { }) }) - describe("Type compatibility", () => { - it("should respect type compatibility for SubgraphInput connections", () => { + describe('Type compatibility', () => { + it('should respect type compatibility for SubgraphInput connections', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "number_input", type: "number" }], + inputs: [{ name: 'number_input', type: 'number' }] }) const subgraphInput = subgraph.inputs[0] - const node = new LGraphNode("TestNode") - node.addInput("number_slot", "number") - node.addInput("string_slot", "string") - node.addInput("any_slot", "*") - node.addInput("boolean_slot", "boolean") + const node = new LGraphNode('TestNode') + node.addInput('number_slot', 'number') + node.addInput('string_slot', 'string') + node.addInput('any_slot', '*') + node.addInput('boolean_slot', 'boolean') subgraph.add(node) const numberSlot = node.inputs[0] as NodeInputSlot @@ -181,27 +190,27 @@ describe("Subgraph slot connections", () => { expect(booleanSlot.isValidTarget(subgraphInput)).toBe(false) }) - it("should respect type compatibility for SubgraphOutput connections", () => { + it('should respect type compatibility for SubgraphOutput connections', () => { const subgraph = createTestSubgraph() - const node = new LGraphNode("TestNode") - node.addOutput("out", "string") + const node = new LGraphNode('TestNode') + node.addOutput('out', 'string') subgraph.add(node) - const subgraphOutput = subgraph.addOutput("result", "number") + const subgraphOutput = subgraph.addOutput('result', 'number') const nodeOutput = node.outputs[0] expect(subgraphOutput.isValidTarget(nodeOutput)).toBe(false) }) - it("should handle wildcard SubgraphInput", () => { + it('should handle wildcard SubgraphInput', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "any_input", type: "*" }], + inputs: [{ name: 'any_input', type: '*' }] }) const subgraphInput = subgraph.inputs[0] - const node = new LGraphNode("TestNode") - node.addInput("number_slot", "number") + const node = new LGraphNode('TestNode') + node.addInput('number_slot', 'number') subgraph.add(node) const numberSlot = node.inputs[0] as NodeInputSlot @@ -210,12 +219,12 @@ describe("Subgraph slot connections", () => { }) }) - describe("Type guards", () => { - it("should correctly identify SubgraphInput", () => { + describe('Type guards', () => { + it('should correctly identify SubgraphInput', () => { const subgraph = createTestSubgraph() - const subgraphInput = subgraph.addInput("value", "number") - const node = new LGraphNode("TestNode") - node.addInput("in", "number") + const subgraphInput = subgraph.addInput('value', 'number') + const node = new LGraphNode('TestNode') + node.addInput('in', 'number') expect(isSubgraphInput(subgraphInput)).toBe(true) expect(isSubgraphInput(node.inputs[0])).toBe(false) @@ -225,11 +234,11 @@ describe("Subgraph slot connections", () => { expect(isSubgraphInput({})).toBe(false) }) - it("should correctly identify SubgraphOutput", () => { + it('should correctly identify SubgraphOutput', () => { const subgraph = createTestSubgraph() - const subgraphOutput = subgraph.addOutput("result", "number") - const node = new LGraphNode("TestNode") - node.addOutput("out", "number") + const subgraphOutput = subgraph.addOutput('result', 'number') + const node = new LGraphNode('TestNode') + node.addOutput('out', 'number') expect(isSubgraphOutput(subgraphOutput)).toBe(true) expect(isSubgraphOutput(node.outputs[0])).toBe(false) @@ -240,23 +249,23 @@ describe("Subgraph slot connections", () => { }) }) - describe("Nested subgraphs", () => { - it("should handle dragging from SubgraphInput in nested subgraphs", () => { + describe('Nested subgraphs', () => { + it('should handle dragging from SubgraphInput in nested subgraphs', () => { const parentSubgraph = createTestSubgraph({ - inputs: [{ name: "parent_input", type: "number" }], - outputs: [{ name: "parent_output", type: "number" }], + inputs: [{ name: 'parent_input', type: 'number' }], + outputs: [{ name: 'parent_output', type: 'number' }] }) const nestedSubgraph = createTestSubgraph({ - inputs: [{ name: "nested_input", type: "number" }], - outputs: [{ name: "nested_output", type: "number" }], + inputs: [{ name: 'nested_input', type: 'number' }], + outputs: [{ name: 'nested_output', type: 'number' }] }) const nestedSubgraphNode = createTestSubgraphNode(nestedSubgraph) parentSubgraph.add(nestedSubgraphNode) - const regularNode = new LGraphNode("TestNode") - regularNode.addInput("test_input", "number") + const regularNode = new LGraphNode('TestNode') + regularNode.addInput('test_input', 'number') nestedSubgraph.add(regularNode) const nestedSubgraphInput = nestedSubgraph.inputs[0] @@ -265,18 +274,18 @@ describe("Subgraph slot connections", () => { expect(regularNodeSlot.isValidTarget(nestedSubgraphInput)).toBe(true) }) - it("should handle multiple levels of nesting", () => { + it('should handle multiple levels of nesting', () => { const level1 = createTestSubgraph({ - inputs: [{ name: "level1_input", type: "string" }], + inputs: [{ name: 'level1_input', type: 'string' }] }) const level2 = createTestSubgraph({ - inputs: [{ name: "level2_input", type: "string" }], + inputs: [{ name: 'level2_input', type: 'string' }] }) const level3 = createTestSubgraph({ - inputs: [{ name: "level3_input", type: "string" }], - outputs: [{ name: "level3_output", type: "string" }], + inputs: [{ name: 'level3_input', type: 'string' }], + outputs: [{ name: 'level3_output', type: 'string' }] }) const level2Node = createTestSubgraphNode(level2) @@ -285,8 +294,8 @@ describe("Subgraph slot connections", () => { const level3Node = createTestSubgraphNode(level3) level2.add(level3Node) - const deepNode = new LGraphNode("DeepNode") - deepNode.addInput("deep_input", "string") + const deepNode = new LGraphNode('DeepNode') + deepNode.addInput('deep_input', 'string') level3.add(deepNode) const level3Input = level3.inputs[0] @@ -298,24 +307,24 @@ describe("Subgraph slot connections", () => { expect(level3Output.isValidTarget(level3Input)).toBe(true) }) - it("should maintain type checking across nesting levels", () => { + it('should maintain type checking across nesting levels', () => { const outer = createTestSubgraph({ - inputs: [{ name: "outer_number", type: "number" }], + inputs: [{ name: 'outer_number', type: 'number' }] }) const inner = createTestSubgraph({ inputs: [ - { name: "inner_number", type: "number" }, - { name: "inner_string", type: "string" }, - ], + { name: 'inner_number', type: 'number' }, + { name: 'inner_string', type: 'string' } + ] }) const innerNode = createTestSubgraphNode(inner) outer.add(innerNode) - const node = new LGraphNode("TestNode") - node.addInput("number_slot", "number") - node.addInput("string_slot", "string") + const node = new LGraphNode('TestNode') + node.addInput('number_slot', 'number') + node.addInput('string_slot', 'string') inner.add(node) const innerNumberInput = inner.inputs[0] diff --git a/src/lib/litegraph/test/subgraph/SubgraphSlotVisualFeedback.test.ts b/src/lib/litegraph/test/subgraph/SubgraphSlotVisualFeedback.test.ts index 86b267f26c..fe46448501 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphSlotVisualFeedback.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphSlotVisualFeedback.test.ts @@ -1,10 +1,10 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" +import { beforeEach, describe, expect, it, vi } from 'vitest' -import { LGraphNode } from "@/lib/litegraph/src/litegraph" +import { LGraphNode } from '@/lib/litegraph/src/litegraph' -import { createTestSubgraph } from "./fixtures/subgraphHelpers" +import { createTestSubgraph } from './fixtures/subgraphHelpers' -describe("SubgraphSlot visual feedback", () => { +describe('SubgraphSlot visual feedback', () => { let mockCtx: CanvasRenderingContext2D let mockColorContext: any let globalAlphaValues: number[] @@ -23,35 +23,35 @@ describe("SubgraphSlot visual feedback", () => { this._globalAlpha = value globalAlphaValues.push(value) }, - fillStyle: "", - strokeStyle: "", + fillStyle: '', + strokeStyle: '', lineWidth: 1, beginPath: vi.fn(), arc: vi.fn(), fill: vi.fn(), stroke: vi.fn(), rect: vi.fn(), - fillText: vi.fn(), + fillText: vi.fn() } mockCtx = mockContext as unknown as CanvasRenderingContext2D // Create a mock color context mockColorContext = { - defaultInputColor: "#FF0000", - defaultOutputColor: "#00FF00", - getConnectedColor: vi.fn().mockReturnValue("#0000FF"), - getDisconnectedColor: vi.fn().mockReturnValue("#AAAAAA"), + defaultInputColor: '#FF0000', + defaultOutputColor: '#00FF00', + getConnectedColor: vi.fn().mockReturnValue('#0000FF'), + getDisconnectedColor: vi.fn().mockReturnValue('#AAAAAA') } }) - it("should render SubgraphInput slots with full opacity when dragging from compatible slot", () => { + it('should render SubgraphInput slots with full opacity when dragging from compatible slot', () => { const subgraph = createTestSubgraph() - const node = new LGraphNode("TestNode") - node.addInput("in", "number") + const node = new LGraphNode('TestNode') + node.addInput('in', 'number') subgraph.add(node) // Add a subgraph input - const subgraphInput = subgraph.addInput("value", "number") + const subgraphInput = subgraph.addInput('value', 'number') // Simulate dragging from the subgraph input (which acts as output inside subgraph) const nodeInput = node.inputs[0] @@ -61,7 +61,7 @@ describe("SubgraphSlot visual feedback", () => { ctx: mockCtx, colorContext: mockColorContext, fromSlot: nodeInput, - editorAlpha: 1, + editorAlpha: 1 }) // Should render with full opacity (not 0.4) @@ -69,19 +69,19 @@ describe("SubgraphSlot visual feedback", () => { expect(globalAlphaValues).not.toContain(0.4) }) - it("should render SubgraphInput slots with 40% opacity when dragging from another SubgraphInput", () => { + it('should render SubgraphInput slots with 40% opacity when dragging from another SubgraphInput', () => { const subgraph = createTestSubgraph() // Add two subgraph inputs - const subgraphInput1 = subgraph.addInput("value1", "number") - const subgraphInput2 = subgraph.addInput("value2", "number") + const subgraphInput1 = subgraph.addInput('value1', 'number') + const subgraphInput2 = subgraph.addInput('value2', 'number') // Draw subgraphInput2 while dragging from subgraphInput1 (incompatible - both are outputs inside subgraph) subgraphInput2.draw({ ctx: mockCtx, colorContext: mockColorContext, fromSlot: subgraphInput1, - editorAlpha: 1, + editorAlpha: 1 }) // Should render with 40% opacity @@ -89,14 +89,14 @@ describe("SubgraphSlot visual feedback", () => { expect(globalAlphaValues).toContain(0.4) }) - it("should render SubgraphOutput slots with full opacity when dragging from compatible slot", () => { + it('should render SubgraphOutput slots with full opacity when dragging from compatible slot', () => { const subgraph = createTestSubgraph() - const node = new LGraphNode("TestNode") - node.addOutput("out", "number") + const node = new LGraphNode('TestNode') + node.addOutput('out', 'number') subgraph.add(node) // Add a subgraph output - const subgraphOutput = subgraph.addOutput("result", "number") + const subgraphOutput = subgraph.addOutput('result', 'number') // Simulate dragging from a node output const nodeOutput = node.outputs[0] @@ -106,7 +106,7 @@ describe("SubgraphSlot visual feedback", () => { ctx: mockCtx, colorContext: mockColorContext, fromSlot: nodeOutput, - editorAlpha: 1, + editorAlpha: 1 }) // Should render with full opacity (not 0.4) @@ -114,19 +114,19 @@ describe("SubgraphSlot visual feedback", () => { expect(globalAlphaValues).not.toContain(0.4) }) - it("should render SubgraphOutput slots with 40% opacity when dragging from another SubgraphOutput", () => { + it('should render SubgraphOutput slots with 40% opacity when dragging from another SubgraphOutput', () => { const subgraph = createTestSubgraph() // Add two subgraph outputs - const subgraphOutput1 = subgraph.addOutput("result1", "number") - const subgraphOutput2 = subgraph.addOutput("result2", "number") + const subgraphOutput1 = subgraph.addOutput('result1', 'number') + const subgraphOutput2 = subgraph.addOutput('result2', 'number') // Draw subgraphOutput2 while dragging from subgraphOutput1 (incompatible - both are inputs inside subgraph) subgraphOutput2.draw({ ctx: mockCtx, colorContext: mockColorContext, fromSlot: subgraphOutput1, - editorAlpha: 1, + editorAlpha: 1 }) // Should render with 40% opacity @@ -154,14 +154,14 @@ describe("SubgraphSlot visual feedback", () => { // expect(mockCtx.globalAlpha).toBe(1) // }) - it("should render slots with 40% opacity when dragging between incompatible types", () => { + it('should render slots with 40% opacity when dragging between incompatible types', () => { const subgraph = createTestSubgraph() - const node = new LGraphNode("TestNode") - node.addOutput("string_output", "string") + const node = new LGraphNode('TestNode') + node.addOutput('string_output', 'string') subgraph.add(node) // Add subgraph output with incompatible type - const subgraphOutput = subgraph.addOutput("result", "number") + const subgraphOutput = subgraph.addOutput('result', 'number') // Get the string output slot from the node const nodeStringOutput = node.outputs[0] @@ -171,7 +171,7 @@ describe("SubgraphSlot visual feedback", () => { ctx: mockCtx, colorContext: mockColorContext, fromSlot: nodeStringOutput, - editorAlpha: 1, + editorAlpha: 1 }) // Should render with 40% opacity due to type mismatch diff --git a/src/lib/litegraph/test/subgraph/SubgraphWidgetPromotion.test.ts b/src/lib/litegraph/test/subgraph/SubgraphWidgetPromotion.test.ts index fdd275e92c..20e3a4dbee 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphWidgetPromotion.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphWidgetPromotion.test.ts @@ -1,33 +1,36 @@ -import type { ISlotType } from "@/lib/litegraph/src/interfaces" -import type { TWidgetType } from "@/lib/litegraph/src/types/widgets" +import { describe, expect, it } from 'vitest' -import { describe, expect, it } from "vitest" +import type { ISlotType } from '@/lib/litegraph/src/interfaces' +import { LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph' +import type { TWidgetType } from '@/lib/litegraph/src/types/widgets' +import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget' -import { LGraphNode, Subgraph } from "@/lib/litegraph/src/litegraph" -import { BaseWidget } from "@/lib/litegraph/src/widgets/BaseWidget" - -import { createEventCapture, createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers" +import { + createEventCapture, + createTestSubgraph, + createTestSubgraphNode +} from './fixtures/subgraphHelpers' // Helper to create a node with a widget function createNodeWithWidget( title: string, - widgetType: TWidgetType = "number", + widgetType: TWidgetType = 'number', widgetValue: any = 42, - slotType: ISlotType = "number", - tooltip?: string, + slotType: ISlotType = 'number', + tooltip?: string ) { const node = new LGraphNode(title) - const input = node.addInput("value", slotType) - node.addOutput("out", slotType) + const input = node.addInput('value', slotType) + node.addOutput('out', slotType) const widget = new BaseWidget({ - name: "widget", + name: 'widget', type: widgetType, value: widgetValue, y: 0, - options: widgetType === "number" ? { min: 0, max: 100, step: 1 } : {}, + options: widgetType === 'number' ? { min: 0, max: 100, step: 1 } : {}, node, - tooltip, + tooltip }) node.widgets = [widget] input.widget = { name: widget.name } @@ -36,42 +39,60 @@ function createNodeWithWidget( } // Helper to connect subgraph input to node and create SubgraphNode -function setupPromotedWidget(subgraph: Subgraph, node: LGraphNode, slotIndex = 0) { +function setupPromotedWidget( + subgraph: Subgraph, + node: LGraphNode, + slotIndex = 0 +) { subgraph.add(node) subgraph.inputNode.slots[slotIndex].connect(node.inputs[slotIndex], node) return createTestSubgraphNode(subgraph) } -describe("SubgraphWidgetPromotion", () => { - describe("Widget Promotion Functionality", () => { - it("should promote widgets when connecting node to subgraph input", () => { +describe('SubgraphWidgetPromotion', () => { + describe('Widget Promotion Functionality', () => { + it('should promote widgets when connecting node to subgraph input', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "value", type: "number" }], + inputs: [{ name: 'value', type: 'number' }] }) - const { node } = createNodeWithWidget("Test Node") + const { node } = createNodeWithWidget('Test Node') const subgraphNode = setupPromotedWidget(subgraph, node) // The widget should be promoted to the subgraph node expect(subgraphNode.widgets).toHaveLength(1) - expect(subgraphNode.widgets[0].name).toBe("value") // Uses subgraph input name - expect(subgraphNode.widgets[0].type).toBe("number") + expect(subgraphNode.widgets[0].name).toBe('value') // Uses subgraph input name + expect(subgraphNode.widgets[0].type).toBe('number') expect(subgraphNode.widgets[0].value).toBe(42) }) - it("should promote all widget types", () => { + it('should promote all widget types', () => { const subgraph = createTestSubgraph({ inputs: [ - { name: "numberInput", type: "number" }, - { name: "stringInput", type: "string" }, - { name: "toggleInput", type: "boolean" }, - ], + { name: 'numberInput', type: 'number' }, + { name: 'stringInput', type: 'string' }, + { name: 'toggleInput', type: 'boolean' } + ] }) // Create nodes with different widget types - const { node: numberNode } = createNodeWithWidget("Number Node", "number", 100) - const { node: stringNode } = createNodeWithWidget("String Node", "string", "test", "string") - const { node: toggleNode } = createNodeWithWidget("Toggle Node", "toggle", true, "boolean") + const { node: numberNode } = createNodeWithWidget( + 'Number Node', + 'number', + 100 + ) + const { node: stringNode } = createNodeWithWidget( + 'String Node', + 'string', + 'test', + 'string' + ) + const { node: toggleNode } = createNodeWithWidget( + 'Toggle Node', + 'toggle', + true, + 'boolean' + ) // Setup all nodes subgraph.add(numberNode) @@ -89,25 +110,25 @@ describe("SubgraphWidgetPromotion", () => { // Check specific widget values expect(subgraphNode.widgets[0].value).toBe(100) - expect(subgraphNode.widgets[1].value).toBe("test") + expect(subgraphNode.widgets[1].value).toBe('test') expect(subgraphNode.widgets[2].value).toBe(true) }) - it("should fire widget-promoted event when widget is promoted", () => { + it('should fire widget-promoted event when widget is promoted', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input", type: "number" }], + inputs: [{ name: 'input', type: 'number' }] }) const eventCapture = createEventCapture(subgraph.events, [ - "widget-promoted", - "widget-demoted", + 'widget-promoted', + 'widget-demoted' ]) - const { node } = createNodeWithWidget("Test Node") + const { node } = createNodeWithWidget('Test Node') const subgraphNode = setupPromotedWidget(subgraph, node) // Check event was fired - const promotedEvents = eventCapture.getEventsByType("widget-promoted") + const promotedEvents = eventCapture.getEventsByType('widget-promoted') expect(promotedEvents).toHaveLength(1) expect(promotedEvents[0].detail.widget).toBeDefined() expect(promotedEvents[0].detail.subgraphNode).toBe(subgraphNode) @@ -115,22 +136,24 @@ describe("SubgraphWidgetPromotion", () => { eventCapture.cleanup() }) - it("should fire widget-demoted event when removing promoted widget", () => { + it('should fire widget-demoted event when removing promoted widget', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input", type: "number" }], + inputs: [{ name: 'input', type: 'number' }] }) - const { node } = createNodeWithWidget("Test Node") + const { node } = createNodeWithWidget('Test Node') const subgraphNode = setupPromotedWidget(subgraph, node) expect(subgraphNode.widgets).toHaveLength(1) - const eventCapture = createEventCapture(subgraph.events, ["widget-demoted"]) + const eventCapture = createEventCapture(subgraph.events, [ + 'widget-demoted' + ]) // Remove the widget - subgraphNode.removeWidgetByName("input") + subgraphNode.removeWidgetByName('input') // Check event was fired - const demotedEvents = eventCapture.getEventsByType("widget-demoted") + const demotedEvents = eventCapture.getEventsByType('widget-demoted') expect(demotedEvents).toHaveLength(1) expect(demotedEvents[0].detail.widget).toBeDefined() expect(demotedEvents[0].detail.subgraphNode).toBe(subgraphNode) @@ -141,35 +164,35 @@ describe("SubgraphWidgetPromotion", () => { eventCapture.cleanup() }) - it("should handle multiple widgets on same node", () => { + it('should handle multiple widgets on same node', () => { const subgraph = createTestSubgraph({ inputs: [ - { name: "input1", type: "number" }, - { name: "input2", type: "string" }, - ], + { name: 'input1', type: 'number' }, + { name: 'input2', type: 'string' } + ] }) // Create node with multiple widgets - const multiWidgetNode = new LGraphNode("Multi Widget Node") - const numInput = multiWidgetNode.addInput("num", "number") - const strInput = multiWidgetNode.addInput("str", "string") + const multiWidgetNode = new LGraphNode('Multi Widget Node') + const numInput = multiWidgetNode.addInput('num', 'number') + const strInput = multiWidgetNode.addInput('str', 'string') const widget1 = new BaseWidget({ - name: "widget1", - type: "number", + name: 'widget1', + type: 'number', value: 10, y: 0, options: {}, - node: multiWidgetNode, + node: multiWidgetNode }) const widget2 = new BaseWidget({ - name: "widget2", - type: "string", - value: "hello", + name: 'widget2', + type: 'string', + value: 'hello', y: 40, options: {}, - node: multiWidgetNode, + node: multiWidgetNode }) multiWidgetNode.widgets = [widget1, widget2] @@ -178,49 +201,57 @@ describe("SubgraphWidgetPromotion", () => { subgraph.add(multiWidgetNode) // Connect both inputs - subgraph.inputNode.slots[0].connect(multiWidgetNode.inputs[0], multiWidgetNode) - subgraph.inputNode.slots[1].connect(multiWidgetNode.inputs[1], multiWidgetNode) + subgraph.inputNode.slots[0].connect( + multiWidgetNode.inputs[0], + multiWidgetNode + ) + subgraph.inputNode.slots[1].connect( + multiWidgetNode.inputs[1], + multiWidgetNode + ) // Create SubgraphNode const subgraphNode = createTestSubgraphNode(subgraph) // Both widgets should be promoted expect(subgraphNode.widgets).toHaveLength(2) - expect(subgraphNode.widgets[0].name).toBe("input1") + expect(subgraphNode.widgets[0].name).toBe('input1') expect(subgraphNode.widgets[0].value).toBe(10) - expect(subgraphNode.widgets[1].name).toBe("input2") - expect(subgraphNode.widgets[1].value).toBe("hello") + expect(subgraphNode.widgets[1].name).toBe('input2') + expect(subgraphNode.widgets[1].value).toBe('hello') }) - it("should fire widget-demoted events when node is removed", () => { + it('should fire widget-demoted events when node is removed', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input", type: "number" }], + inputs: [{ name: 'input', type: 'number' }] }) - const { node } = createNodeWithWidget("Test Node") + const { node } = createNodeWithWidget('Test Node') const subgraphNode = setupPromotedWidget(subgraph, node) expect(subgraphNode.widgets).toHaveLength(1) - const eventCapture = createEventCapture(subgraph.events, ["widget-demoted"]) + const eventCapture = createEventCapture(subgraph.events, [ + 'widget-demoted' + ]) // Remove the subgraph node subgraphNode.onRemoved() // Should fire demoted events for all widgets - const demotedEvents = eventCapture.getEventsByType("widget-demoted") + const demotedEvents = eventCapture.getEventsByType('widget-demoted') expect(demotedEvents).toHaveLength(1) eventCapture.cleanup() }) - it("should not promote widget if input is not connected", () => { + it('should not promote widget if input is not connected', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input", type: "number" }], + inputs: [{ name: 'input', type: 'number' }] }) - const { node } = createNodeWithWidget("Test Node") + const { node } = createNodeWithWidget('Test Node') subgraph.add(node) // Don't connect - just create SubgraphNode @@ -230,12 +261,12 @@ describe("SubgraphWidgetPromotion", () => { expect(subgraphNode.widgets).toHaveLength(0) }) - it("should handle disconnection of promoted widget", () => { + it('should handle disconnection of promoted widget', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "input", type: "number" }], + inputs: [{ name: 'input', type: 'number' }] }) - const { node } = createNodeWithWidget("Test Node") + const { node } = createNodeWithWidget('Test Node') const subgraphNode = setupPromotedWidget(subgraph, node) expect(subgraphNode.widgets).toHaveLength(1) @@ -247,14 +278,20 @@ describe("SubgraphWidgetPromotion", () => { }) }) - describe("Tooltip Promotion", () => { - it("should preserve widget tooltip when promoting", () => { + describe('Tooltip Promotion', () => { + it('should preserve widget tooltip when promoting', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "value", type: "number" }], + inputs: [{ name: 'value', type: 'number' }] }) - const originalTooltip = "This is a test tooltip" - const { node } = createNodeWithWidget("Test Node", "number", 42, "number", originalTooltip) + const originalTooltip = 'This is a test tooltip' + const { node } = createNodeWithWidget( + 'Test Node', + 'number', + 42, + 'number', + originalTooltip + ) const subgraphNode = setupPromotedWidget(subgraph, node) // The promoted widget should preserve the original tooltip @@ -262,12 +299,12 @@ describe("SubgraphWidgetPromotion", () => { expect(subgraphNode.widgets[0].tooltip).toBe(originalTooltip) }) - it("should handle widgets with no tooltip", () => { + it('should handle widgets with no tooltip', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "value", type: "number" }], + inputs: [{ name: 'value', type: 'number' }] }) - const { node } = createNodeWithWidget("Test Node", "number", 42, "number") + const { node } = createNodeWithWidget('Test Node', 'number', 42, 'number') const subgraphNode = setupPromotedWidget(subgraph, node) // The promoted widget should have undefined tooltip @@ -275,37 +312,37 @@ describe("SubgraphWidgetPromotion", () => { expect(subgraphNode.widgets[0].tooltip).toBeUndefined() }) - it("should preserve tooltips for multiple promoted widgets", () => { + it('should preserve tooltips for multiple promoted widgets', () => { const subgraph = createTestSubgraph({ inputs: [ - { name: "input1", type: "number" }, - { name: "input2", type: "string" }, - ], + { name: 'input1', type: 'number' }, + { name: 'input2', type: 'string' } + ] }) // Create node with multiple widgets with different tooltips - const multiWidgetNode = new LGraphNode("Multi Widget Node") - const numInput = multiWidgetNode.addInput("num", "number") - const strInput = multiWidgetNode.addInput("str", "string") + const multiWidgetNode = new LGraphNode('Multi Widget Node') + const numInput = multiWidgetNode.addInput('num', 'number') + const strInput = multiWidgetNode.addInput('str', 'string') const widget1 = new BaseWidget({ - name: "widget1", - type: "number", + name: 'widget1', + type: 'number', value: 10, y: 0, options: {}, node: multiWidgetNode, - tooltip: "Number widget tooltip", + tooltip: 'Number widget tooltip' }) const widget2 = new BaseWidget({ - name: "widget2", - type: "string", - value: "hello", + name: 'widget2', + type: 'string', + value: 'hello', y: 40, options: {}, node: multiWidgetNode, - tooltip: "String widget tooltip", + tooltip: 'String widget tooltip' }) multiWidgetNode.widgets = [widget1, widget2] @@ -314,25 +351,37 @@ describe("SubgraphWidgetPromotion", () => { subgraph.add(multiWidgetNode) // Connect both inputs - subgraph.inputNode.slots[0].connect(multiWidgetNode.inputs[0], multiWidgetNode) - subgraph.inputNode.slots[1].connect(multiWidgetNode.inputs[1], multiWidgetNode) + subgraph.inputNode.slots[0].connect( + multiWidgetNode.inputs[0], + multiWidgetNode + ) + subgraph.inputNode.slots[1].connect( + multiWidgetNode.inputs[1], + multiWidgetNode + ) // Create SubgraphNode const subgraphNode = createTestSubgraphNode(subgraph) // Both widgets should preserve their tooltips expect(subgraphNode.widgets).toHaveLength(2) - expect(subgraphNode.widgets[0].tooltip).toBe("Number widget tooltip") - expect(subgraphNode.widgets[1].tooltip).toBe("String widget tooltip") + expect(subgraphNode.widgets[0].tooltip).toBe('Number widget tooltip') + expect(subgraphNode.widgets[1].tooltip).toBe('String widget tooltip') }) - it("should preserve original tooltip after promotion", () => { + it('should preserve original tooltip after promotion', () => { const subgraph = createTestSubgraph({ - inputs: [{ name: "value", type: "number" }], + inputs: [{ name: 'value', type: 'number' }] }) - const originalTooltip = "Original tooltip" - const { node } = createNodeWithWidget("Test Node", "number", 42, "number", originalTooltip) + const originalTooltip = 'Original tooltip' + const { node } = createNodeWithWidget( + 'Test Node', + 'number', + 42, + 'number', + originalTooltip + ) const subgraphNode = setupPromotedWidget(subgraph, node) const promotedWidget = subgraphNode.widgets[0] @@ -341,8 +390,8 @@ describe("SubgraphWidgetPromotion", () => { expect(promotedWidget.tooltip).toBe(originalTooltip) // The promoted widget should still function normally - expect(promotedWidget.name).toBe("value") // Uses subgraph input name - expect(promotedWidget.type).toBe("number") + expect(promotedWidget.name).toBe('value') // Uses subgraph input name + expect(promotedWidget.type).toBe('number') expect(promotedWidget.value).toBe(42) }) }) diff --git a/src/lib/litegraph/test/subgraph/fixtures/advancedEventHelpers.ts b/src/lib/litegraph/test/subgraph/fixtures/advancedEventHelpers.ts index cec8aae3b3..0c21b9ecbe 100644 --- a/src/lib/litegraph/test/subgraph/fixtures/advancedEventHelpers.ts +++ b/src/lib/litegraph/test/subgraph/fixtures/advancedEventHelpers.ts @@ -1,6 +1,6 @@ -import type { CapturedEvent } from "./subgraphHelpers" +import { expect } from 'vitest' -import { expect } from "vitest" +import type { CapturedEvent } from './subgraphHelpers' /** * Extended captured event with additional metadata not in the base infrastructure @@ -17,7 +17,7 @@ export interface ExtendedCapturedEvent extends CapturedEvent { */ export function createExtendedEventCapture( eventTarget: EventTarget, - eventTypes: string[], + eventTypes: string[] ) { const capturedEvents: ExtendedCapturedEvent[] = [] const listeners: Array<() => void> = [] @@ -30,7 +30,7 @@ export function createExtendedEventCapture( timestamp: Date.now(), defaultPrevented: event.defaultPrevented, bubbles: event.bubbles, - cancelable: event.cancelable, + cancelable: event.cancelable }) } @@ -40,17 +40,25 @@ export function createExtendedEventCapture( return { events: capturedEvents, - clear: () => { capturedEvents.length = 0 }, - cleanup: () => { for (const cleanup of listeners) cleanup() }, - getEventsByType: (type: string) => capturedEvents.filter(e => e.type === type), + clear: () => { + capturedEvents.length = 0 + }, + cleanup: () => { + for (const cleanup of listeners) cleanup() + }, + getEventsByType: (type: string) => + capturedEvents.filter((e) => e.type === type), getLatestEvent: () => capturedEvents.at(-1), getFirstEvent: () => capturedEvents[0], /** * Wait for a specific event type to be captured */ - async waitForEvent(type: string, timeoutMs: number = 1000): Promise> { - const existingEvent = capturedEvents.find(e => e.type === type) + async waitForEvent( + type: string, + timeoutMs: number = 1000 + ): Promise> { + const existingEvent = capturedEvents.find((e) => e.type === type) if (existingEvent) return existingEvent return new Promise((resolve, reject) => { @@ -60,7 +68,7 @@ export function createExtendedEventCapture( }, timeoutMs) const eventListener = (_event: Event) => { - const capturedEvent = capturedEvents.find(e => e.type === type) + const capturedEvent = capturedEvents.find((e) => e.type === type) if (capturedEvent) { clearTimeout(timeout) eventTarget.removeEventListener(type, eventListener) @@ -75,11 +83,18 @@ export function createExtendedEventCapture( /** * Wait for a sequence of events to occur in order */ - async waitForSequence(expectedSequence: string[], timeoutMs: number = 1000): Promise[]> { + async waitForSequence( + expectedSequence: string[], + timeoutMs: number = 1000 + ): Promise[]> { // Check if sequence is already complete if (capturedEvents.length >= expectedSequence.length) { - const actualSequence = capturedEvents.slice(0, expectedSequence.length).map(e => e.type) - if (JSON.stringify(actualSequence) === JSON.stringify(expectedSequence)) { + const actualSequence = capturedEvents + .slice(0, expectedSequence.length) + .map((e) => e.type) + if ( + JSON.stringify(actualSequence) === JSON.stringify(expectedSequence) + ) { return capturedEvents.slice(0, expectedSequence.length) } } @@ -87,15 +102,24 @@ export function createExtendedEventCapture( return new Promise((resolve, reject) => { const timeout = setTimeout(() => { cleanup() - const actual = capturedEvents.map(e => e.type).join(", ") - const expected = expectedSequence.join(", ") - reject(new Error(`Event sequence not completed within ${timeoutMs}ms. Expected: ${expected}, Got: ${actual}`)) + const actual = capturedEvents.map((e) => e.type).join(', ') + const expected = expectedSequence.join(', ') + reject( + new Error( + `Event sequence not completed within ${timeoutMs}ms. Expected: ${expected}, Got: ${actual}` + ) + ) }, timeoutMs) const checkSequence = () => { if (capturedEvents.length >= expectedSequence.length) { - const actualSequence = capturedEvents.slice(0, expectedSequence.length).map(e => e.type) - if (JSON.stringify(actualSequence) === JSON.stringify(expectedSequence)) { + const actualSequence = capturedEvents + .slice(0, expectedSequence.length) + .map((e) => e.type) + if ( + JSON.stringify(actualSequence) === + JSON.stringify(expectedSequence) + ) { cleanup() resolve(capturedEvents.slice(0, expectedSequence.length)) } @@ -119,7 +143,7 @@ export function createExtendedEventCapture( // Initial check in case events already exist checkSequence() }) - }, + } } } @@ -138,14 +162,14 @@ export interface MemoryLeakTestOptions { * Useful for testing that event listeners and references are properly cleaned up */ export function createMemoryLeakTest( - setupFn: () => { ref: WeakRef, cleanup: () => void }, - options: MemoryLeakTestOptions = {}, + setupFn: () => { ref: WeakRef; cleanup: () => void }, + options: MemoryLeakTestOptions = {} ) { const { cycles = 1, instancesPerCycle = 1, gcAfterEach = true, - maxMemoryGrowth = 0, + maxMemoryGrowth = 0 } = options return async () => { @@ -165,19 +189,21 @@ export function createMemoryLeakTest( if (gcAfterEach && global.gc) { global.gc() - await new Promise(resolve => setTimeout(resolve, 10)) + await new Promise((resolve) => setTimeout(resolve, 10)) } } // Final garbage collection if (global.gc) { global.gc() - await new Promise(resolve => setTimeout(resolve, 50)) + await new Promise((resolve) => setTimeout(resolve, 50)) // Check if objects were collected - const uncollectedRefs = refs.filter(ref => ref.deref() !== undefined) + const uncollectedRefs = refs.filter((ref) => ref.deref() !== undefined) if (uncollectedRefs.length > 0) { - console.warn(`${uncollectedRefs.length} objects were not garbage collected`) + console.warn( + `${uncollectedRefs.length} objects were not garbage collected` + ) } } @@ -187,7 +213,9 @@ export function createMemoryLeakTest( const memoryGrowth = finalMemory - initialMemory if (memoryGrowth > maxMemoryGrowth) { - throw new Error(`Memory growth ${memoryGrowth} bytes exceeds limit ${maxMemoryGrowth} bytes`) + throw new Error( + `Memory growth ${memoryGrowth} bytes exceeds limit ${maxMemoryGrowth} bytes` + ) } } @@ -214,7 +242,7 @@ export function createEventPerformanceMonitor() { measurements.push({ operation, duration: end - start, - timestamp: start, + timestamp: start }) return result @@ -223,22 +251,33 @@ export function createEventPerformanceMonitor() { getMeasurements: () => [...measurements], getAverageDuration: (operation: string) => { - const operationMeasurements = measurements.filter(m => m.operation === operation) + const operationMeasurements = measurements.filter( + (m) => m.operation === operation + ) if (operationMeasurements.length === 0) return 0 - const totalDuration = operationMeasurements.reduce((sum, m) => sum + m.duration, 0) + const totalDuration = operationMeasurements.reduce( + (sum, m) => sum + m.duration, + 0 + ) return totalDuration / operationMeasurements.length }, - clear: () => { measurements.length = 0 }, + clear: () => { + measurements.length = 0 + }, assertPerformance: (operation: string, maxDuration: number) => { const measurements = this.getMeasurements() - const relevantMeasurements = measurements.filter(m => m.operation === operation) + const relevantMeasurements = measurements.filter( + (m) => m.operation === operation + ) if (relevantMeasurements.length === 0) return - const avgDuration = relevantMeasurements.reduce((sum, m) => sum + m.duration, 0) / relevantMeasurements.length + const avgDuration = + relevantMeasurements.reduce((sum, m) => sum + m.duration, 0) / + relevantMeasurements.length expect(avgDuration).toBeLessThan(maxDuration) - }, + } } } diff --git a/src/lib/litegraph/test/subgraph/fixtures/subgraphFixtures.ts b/src/lib/litegraph/test/subgraph/fixtures/subgraphFixtures.ts index fd53bb204e..377a08a76e 100644 --- a/src/lib/litegraph/test/subgraph/fixtures/subgraphFixtures.ts +++ b/src/lib/litegraph/test/subgraph/fixtures/subgraphFixtures.ts @@ -5,17 +5,16 @@ * in their test files. Each fixture provides a clean, pre-configured subgraph * setup for different testing scenarios. */ +import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph' +import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode' -import { LGraph, Subgraph } from "@/lib/litegraph/src/litegraph" -import { SubgraphNode } from "@/lib/litegraph/src/subgraph/SubgraphNode" - -import { test } from "../../testExtensions" +import { test } from '../../testExtensions' import { createEventCapture, createNestedSubgraphs, createTestSubgraph, - createTestSubgraphNode, -} from "./subgraphHelpers" + createTestSubgraphNode +} from './subgraphHelpers' export interface SubgraphFixtures { /** A minimal subgraph with no inputs, outputs, or nodes */ @@ -58,72 +57,71 @@ export interface SubgraphFixtures { * ``` */ export const subgraphTest = test.extend({ - - emptySubgraph: async ({ }, use: (value: unknown) => Promise) => { + emptySubgraph: async ({}, use: (value: unknown) => Promise) => { const subgraph = createTestSubgraph({ - name: "Empty Test Subgraph", + name: 'Empty Test Subgraph', inputCount: 0, outputCount: 0, - nodeCount: 0, + nodeCount: 0 }) await use(subgraph) }, - simpleSubgraph: async ({ }, use: (value: unknown) => Promise) => { + simpleSubgraph: async ({}, use: (value: unknown) => Promise) => { const subgraph = createTestSubgraph({ - name: "Simple Test Subgraph", - inputs: [{ name: "input", type: "number" }], - outputs: [{ name: "output", type: "number" }], - nodeCount: 2, + name: 'Simple Test Subgraph', + inputs: [{ name: 'input', type: 'number' }], + outputs: [{ name: 'output', type: 'number' }], + nodeCount: 2 }) await use(subgraph) }, - complexSubgraph: async ({ }, use: (value: unknown) => Promise) => { + complexSubgraph: async ({}, use: (value: unknown) => Promise) => { const subgraph = createTestSubgraph({ - name: "Complex Test Subgraph", + name: 'Complex Test Subgraph', inputs: [ - { name: "data", type: "number" }, - { name: "control", type: "boolean" }, - { name: "text", type: "string" }, + { name: 'data', type: 'number' }, + { name: 'control', type: 'boolean' }, + { name: 'text', type: 'string' } ], outputs: [ - { name: "result", type: "number" }, - { name: "status", type: "boolean" }, + { name: 'result', type: 'number' }, + { name: 'status', type: 'boolean' } ], - nodeCount: 5, + nodeCount: 5 }) await use(subgraph) }, - nestedSubgraph: async ({ }, use: (value: unknown) => Promise) => { + nestedSubgraph: async ({}, use: (value: unknown) => Promise) => { const nested = createNestedSubgraphs({ depth: 3, nodesPerLevel: 2, inputsPerSubgraph: 1, - outputsPerSubgraph: 1, + outputsPerSubgraph: 1 }) await use(nested) }, - subgraphWithNode: async ({ }, use: (value: unknown) => Promise) => { + subgraphWithNode: async ({}, use: (value: unknown) => Promise) => { // Create the subgraph definition const subgraph = createTestSubgraph({ - name: "Subgraph With Node", - inputs: [{ name: "input", type: "*" }], - outputs: [{ name: "output", type: "*" }], - nodeCount: 1, + name: 'Subgraph With Node', + inputs: [{ name: 'input', type: '*' }], + outputs: [{ name: 'output', type: '*' }], + nodeCount: 1 }) // Create the parent graph and subgraph node instance const parentGraph = new LGraph() const subgraphNode = createTestSubgraphNode(subgraph, { pos: [200, 200], - size: [180, 80], + size: [180, 80] }) // Add the subgraph node to the parent graph @@ -132,32 +130,32 @@ export const subgraphTest = test.extend({ await use({ subgraph, subgraphNode, - parentGraph, + parentGraph }) }, - eventCapture: async ({ }, use: (value: unknown) => Promise) => { + eventCapture: async ({}, use: (value: unknown) => Promise) => { const subgraph = createTestSubgraph({ - name: "Event Test Subgraph", + name: 'Event Test Subgraph' }) // Set up event capture for all subgraph events const capture = createEventCapture(subgraph.events, [ - "adding-input", - "input-added", - "removing-input", - "renaming-input", - "adding-output", - "output-added", - "removing-output", - "renaming-output", + 'adding-input', + 'input-added', + 'removing-input', + 'renaming-input', + 'adding-output', + 'output-added', + 'removing-output', + 'renaming-output' ]) await use({ subgraph, capture }) // Cleanup event listeners capture.cleanup() - }, + } }) /** @@ -186,21 +184,20 @@ export interface EdgeCaseFixtures { * These tests may intentionally create invalid states. */ export const edgeCaseTest = subgraphTest.extend({ - - circularSubgraph: async ({ }, use: (value: unknown) => Promise) => { + circularSubgraph: async ({}, use: (value: unknown) => Promise) => { const rootGraph = new LGraph() // Create two subgraphs that will reference each other const subgraphA = createTestSubgraph({ - name: "Subgraph A", - inputs: [{ name: "input", type: "*" }], - outputs: [{ name: "output", type: "*" }], + name: 'Subgraph A', + inputs: [{ name: 'input', type: '*' }], + outputs: [{ name: 'output', type: '*' }] }) const subgraphB = createTestSubgraph({ - name: "Subgraph B", - inputs: [{ name: "input", type: "*" }], - outputs: [{ name: "output", type: "*" }], + name: 'Subgraph B', + inputs: [{ name: 'input', type: '*' }], + outputs: [{ name: 'output', type: '*' }] }) // Create instances (this doesn't create circular refs by itself) @@ -216,43 +213,43 @@ export const edgeCaseTest = subgraphTest.extend({ subgraphA, subgraphB, nodeA, - nodeB, + nodeB }) }, - deeplyNestedSubgraph: async ({ }, use: (value: unknown) => Promise) => { + deeplyNestedSubgraph: async ({}, use: (value: unknown) => Promise) => { // Create a very deep nesting structure (but not exceeding MAX_NESTED_SUBGRAPHS) const nested = createNestedSubgraphs({ depth: 50, // Deep but reasonable nodesPerLevel: 1, inputsPerSubgraph: 1, - outputsPerSubgraph: 1, + outputsPerSubgraph: 1 }) await use(nested) }, - maxIOSubgraph: async ({ }, use: (value: unknown) => Promise) => { + maxIOSubgraph: async ({}, use: (value: unknown) => Promise) => { // Create a subgraph with many inputs and outputs const inputs = Array.from({ length: 20 }, (_, i) => ({ name: `input_${i}`, - type: i % 2 === 0 ? "number" : "string" as const, + type: i % 2 === 0 ? 'number' : ('string' as const) })) const outputs = Array.from({ length: 20 }, (_, i) => ({ name: `output_${i}`, - type: i % 2 === 0 ? "number" : "string" as const, + type: i % 2 === 0 ? 'number' : ('string' as const) })) const subgraph = createTestSubgraph({ - name: "Max IO Subgraph", + name: 'Max IO Subgraph', inputs, outputs, - nodeCount: 10, + nodeCount: 10 }) await use(subgraph) - }, + } }) /** @@ -261,7 +258,7 @@ export const edgeCaseTest = subgraphTest.extend({ */ export function verifyFixtureIntegrity>( fixture: T, - expectedProperties: (keyof T)[], + expectedProperties: (keyof T)[] ): void { for (const prop of expectedProperties) { if (!(prop in fixture)) { @@ -285,9 +282,9 @@ export function createSubgraphSnapshot(subgraph: Subgraph) { outputCount: subgraph.outputs.length, nodeCount: subgraph.nodes.length, linkCount: subgraph.links.size, - inputs: subgraph.inputs.map(i => ({ name: i.name, type: i.type })), - outputs: subgraph.outputs.map(o => ({ name: o.name, type: o.type })), + inputs: subgraph.inputs.map((i) => ({ name: i.name, type: i.type })), + outputs: subgraph.outputs.map((o) => ({ name: o.name, type: o.type })), hasInputNode: !!subgraph.inputNode, - hasOutputNode: !!subgraph.outputNode, + hasOutputNode: !!subgraph.outputNode } } diff --git a/src/lib/litegraph/test/subgraph/fixtures/subgraphHelpers.ts b/src/lib/litegraph/test/subgraph/fixtures/subgraphHelpers.ts index 5b47184385..41de11e075 100644 --- a/src/lib/litegraph/test/subgraph/fixtures/subgraphHelpers.ts +++ b/src/lib/litegraph/test/subgraph/fixtures/subgraphHelpers.ts @@ -5,16 +5,17 @@ * These functions provide consistent ways to create test subgraphs, nodes, and * verify their behavior. */ +import { expect } from 'vitest' -import type { ISlotType, NodeId } from "@/lib/litegraph/src/litegraph" -import type { ExportedSubgraph, ExportedSubgraphInstance } from "@/lib/litegraph/src/types/serialisation" -import type { UUID } from "@/lib/litegraph/src/utils/uuid" - -import { expect } from "vitest" - -import { LGraph, LGraphNode, Subgraph } from "@/lib/litegraph/src/litegraph" -import { SubgraphNode } from "@/lib/litegraph/src/subgraph/SubgraphNode" -import { createUuidv4 } from "@/lib/litegraph/src/utils/uuid" +import type { ISlotType, NodeId } from '@/lib/litegraph/src/litegraph' +import { LGraph, LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph' +import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode' +import type { + ExportedSubgraph, + ExportedSubgraphInstance +} from '@/lib/litegraph/src/types/serialisation' +import type { UUID } from '@/lib/litegraph/src/utils/uuid' +import { createUuidv4 } from '@/lib/litegraph/src/utils/uuid' export interface TestSubgraphOptions { id?: UUID @@ -22,8 +23,8 @@ export interface TestSubgraphOptions { nodeCount?: number inputCount?: number outputCount?: number - inputs?: Array<{ name: string, type: ISlotType }> - outputs?: Array<{ name: string, type: ISlotType }> + inputs?: Array<{ name: string; type: ISlotType }> + outputs?: Array<{ name: string; type: ISlotType }> } export interface TestSubgraphNodeOptions { @@ -72,15 +73,21 @@ export interface CapturedEvent { * }) * ``` */ -export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph { +export function createTestSubgraph( + options: TestSubgraphOptions = {} +): Subgraph { // Validate options - cannot specify both inputs array and inputCount if (options.inputs && options.inputCount) { - throw new Error(`Cannot specify both 'inputs' array and 'inputCount'. Choose one approach. Received options: ${JSON.stringify(options)}`) + throw new Error( + `Cannot specify both 'inputs' array and 'inputCount'. Choose one approach. Received options: ${JSON.stringify(options)}` + ) } // Validate options - cannot specify both outputs array and outputCount if (options.outputs && options.outputCount) { - throw new Error(`Cannot specify both 'outputs' array and 'outputCount'. Choose one approach. Received options: ${JSON.stringify(options)}`) + throw new Error( + `Cannot specify both 'outputs' array and 'outputCount'. Choose one approach. Received options: ${JSON.stringify(options)}` + ) } const rootGraph = new LGraph() @@ -96,24 +103,24 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph // Subgraph-specific properties id: options.id || createUuidv4(), - name: options.name || "Test Subgraph", + name: options.name || 'Test Subgraph', // IO Nodes (required for subgraph functionality) inputNode: { id: -10, // SUBGRAPH_INPUT_ID bounding: [10, 100, 150, 126], // [x, y, width, height] - pinned: false, + pinned: false }, outputNode: { id: -20, // SUBGRAPH_OUTPUT_ID bounding: [400, 100, 140, 126], // [x, y, width, height] - pinned: false, + pinned: false }, // IO definitions - will be populated by addInput/addOutput calls inputs: [], outputs: [], - widgets: [], + widgets: [] } // Create the subgraph @@ -126,7 +133,7 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph } } else if (options.inputCount) { for (let i = 0; i < options.inputCount; i++) { - subgraph.addInput(`input_${i}`, "*") + subgraph.addInput(`input_${i}`, '*') } } @@ -137,7 +144,7 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph } } else if (options.outputCount) { for (let i = 0; i < options.outputCount; i++) { - subgraph.addOutput(`output_${i}`, "*") + subgraph.addOutput(`output_${i}`, '*') } } @@ -145,8 +152,8 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph if (options.nodeCount) { for (let i = 0; i < options.nodeCount; i++) { const node = new LGraphNode(`Test Node ${i}`) - node.addInput("in", "*") - node.addOutput("out", "*") + node.addInput('in', '*') + node.addOutput('out', '*') subgraph.add(node) } } @@ -172,7 +179,7 @@ export function createTestSubgraph(options: TestSubgraphOptions = {}): Subgraph */ export function createTestSubgraphNode( subgraph: Subgraph, - options: TestSubgraphNodeOptions = {}, + options: TestSubgraphNodeOptions = {} ): SubgraphNode { const parentGraph = new LGraph() @@ -185,7 +192,7 @@ export function createTestSubgraphNode( outputs: [], properties: {}, flags: {}, - mode: 0, + mode: 0 } return new SubgraphNode(parentGraph, subgraph, instanceData) @@ -206,7 +213,7 @@ export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) { depth = 2, nodesPerLevel = 2, inputsPerSubgraph = 1, - outputsPerSubgraph = 1, + outputsPerSubgraph = 1 } = options const rootGraph = new LGraph() @@ -221,14 +228,14 @@ export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) { name: `Level ${level} Subgraph`, nodeCount: nodesPerLevel, inputCount: inputsPerSubgraph, - outputCount: outputsPerSubgraph, + outputCount: outputsPerSubgraph }) subgraphs.push(subgraph) // Create instance in parent const subgraphNode = createTestSubgraphNode(subgraph, { - pos: [100 + level * 200, 100], + pos: [100 + level * 200, 100] }) if (currentParent instanceof LGraph) { @@ -248,7 +255,7 @@ export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) { subgraphs, subgraphNodes, depth, - leafSubgraph: subgraphs.at(-1), + leafSubgraph: subgraphs.at(-1) } } @@ -268,7 +275,7 @@ export function createNestedSubgraphs(options: NestedSubgraphOptions = {}) { */ export function assertSubgraphStructure( subgraph: Subgraph, - expected: SubgraphStructureExpectation, + expected: SubgraphStructureExpectation ): void { if (expected.inputCount !== undefined) { expect(subgraph.inputs.length).toBe(expected.inputCount) @@ -314,7 +321,7 @@ export function assertSubgraphStructure( */ export function verifyEventSequence( capturedEvents: CapturedEvent[], - expectedSequence: string[], + expectedSequence: string[] ): void { expect(capturedEvents.length).toBe(expectedSequence.length) @@ -325,7 +332,7 @@ export function verifyEventSequence( // Verify timestamps are in order for (let i = 1; i < capturedEvents.length; i++) { expect(capturedEvents[i].timestamp).toBeGreaterThanOrEqual( - capturedEvents[i - 1].timestamp, + capturedEvents[i - 1].timestamp ) } } @@ -336,7 +343,9 @@ export function verifyEventSequence( * @param overrides Properties to override in the default data * @returns ExportedSubgraph data structure */ -export function createTestSubgraphData(overrides: Partial = {}): ExportedSubgraph { +export function createTestSubgraphData( + overrides: Partial = {} +): ExportedSubgraph { return { version: 1, nodes: [], @@ -346,24 +355,24 @@ export function createTestSubgraphData(overrides: Partial = {} definitions: { subgraphs: [] }, id: createUuidv4(), - name: "Test Data Subgraph", + name: 'Test Data Subgraph', inputNode: { id: -10, bounding: [10, 100, 150, 126], - pinned: false, + pinned: false }, outputNode: { id: -20, bounding: [400, 100, 140, 126], - pinned: false, + pinned: false }, inputs: [], outputs: [], widgets: [], - ...overrides, + ...overrides } } @@ -373,29 +382,34 @@ export function createTestSubgraphData(overrides: Partial = {} * @param nodeCount Number of internal nodes to create * @returns Complex subgraph data structure */ -export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgraph { +export function createComplexSubgraphData( + nodeCount: number = 5 +): ExportedSubgraph { const nodes = [] - const links: Record = {} + const links: Record< + string, + { + id: number + origin_id: number + origin_slot: number + target_id: number + target_slot: number + type: string + } + > = {} // Create internal nodes for (let i = 0; i < nodeCount; i++) { nodes.push({ id: i + 1, // Start from 1 to avoid conflicts with IO nodes - type: "basic/test", + type: 'basic/test', pos: [100 + i * 150, 200], size: [120, 60], - inputs: [{ name: "in", type: "*", link: null }], - outputs: [{ name: "out", type: "*", links: [] }], + inputs: [{ name: 'in', type: '*', link: null }], + outputs: [{ name: 'out', type: '*', links: [] }], properties: { value: i }, flags: {}, - mode: 0, + mode: 0 }) } @@ -408,7 +422,7 @@ export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgra origin_slot: 0, target_id: i + 2, target_slot: 0, - type: "*", + type: '*' } } @@ -416,13 +430,13 @@ export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgra nodes, links, inputs: [ - { name: "input1", type: "number", pos: [0, 0] }, - { name: "input2", type: "string", pos: [0, 1] }, + { name: 'input1', type: 'number', pos: [0, 0] }, + { name: 'input2', type: 'string', pos: [0, 1] } ], outputs: [ - { name: "output1", type: "number", pos: [0, 0] }, - { name: "output2", type: "string", pos: [0, 1] }, - ], + { name: 'output1', type: 'number', pos: [0, 0] }, + { name: 'output2', type: 'string', pos: [0, 1] } + ] }) } @@ -434,7 +448,7 @@ export function createComplexSubgraphData(nodeCount: number = 5): ExportedSubgra */ export function createEventCapture( eventTarget: EventTarget, - eventTypes: string[], + eventTypes: string[] ) { const capturedEvents: CapturedEvent[] = [] const listeners: Array<() => void> = [] @@ -445,7 +459,7 @@ export function createEventCapture( capturedEvents.push({ type: eventType, detail: (event as CustomEvent).detail, - timestamp: Date.now(), + timestamp: Date.now() }) } @@ -455,12 +469,15 @@ export function createEventCapture( return { events: capturedEvents, - clear: () => { capturedEvents.length = 0 }, + clear: () => { + capturedEvents.length = 0 + }, cleanup: () => { // Remove all event listeners to prevent memory leaks for (const cleanup of listeners) cleanup() }, - getEventsByType: (type: string) => capturedEvents.filter(e => e.type === type), + getEventsByType: (type: string) => + capturedEvents.filter((e) => e.type === type) } } @@ -469,7 +486,10 @@ export function createEventCapture( * @param subgraph The subgraph to inspect * @param label Optional label for the log output */ -export function logSubgraphStructure(subgraph: Subgraph, label: string = "Subgraph"): void { +export function logSubgraphStructure( + subgraph: Subgraph, + label: string = 'Subgraph' +): void { console.log(`\n=== ${label} Structure ===`) console.log(`Name: ${subgraph.name}`) console.log(`ID: ${subgraph.id}`) @@ -479,15 +499,21 @@ export function logSubgraphStructure(subgraph: Subgraph, label: string = "Subgra console.log(`Links: ${subgraph.links.size}`) if (subgraph.inputs.length > 0) { - console.log("Input details:", subgraph.inputs.map(i => ({ name: i.name, type: i.type }))) + console.log( + 'Input details:', + subgraph.inputs.map((i) => ({ name: i.name, type: i.type })) + ) } if (subgraph.outputs.length > 0) { - console.log("Output details:", subgraph.outputs.map(o => ({ name: o.name, type: o.type }))) + console.log( + 'Output details:', + subgraph.outputs.map((o) => ({ name: o.name, type: o.type })) + ) } - console.log("========================\n") + console.log('========================\n') } // Re-export expect from vitest for convenience -export { expect } from "vitest" +export { expect } from 'vitest' diff --git a/src/lib/litegraph/test/subgraph/subgraphUtils.test.ts b/src/lib/litegraph/test/subgraph/subgraphUtils.test.ts index 56c45bc5a3..5d48d76926 100644 --- a/src/lib/litegraph/test/subgraph/subgraphUtils.test.ts +++ b/src/lib/litegraph/test/subgraph/subgraphUtils.test.ts @@ -1,24 +1,26 @@ -import type { UUID } from "@/lib/litegraph/src/utils/uuid" +import { describe, expect, it } from 'vitest' -import { describe, expect, it } from "vitest" - -import { LGraph } from "@/lib/litegraph/src/litegraph" +import { LGraph } from '@/lib/litegraph/src/litegraph' import { findUsedSubgraphIds, - getDirectSubgraphIds, -} from "@/lib/litegraph/src/subgraph/subgraphUtils" + getDirectSubgraphIds +} from '@/lib/litegraph/src/subgraph/subgraphUtils' +import type { UUID } from '@/lib/litegraph/src/utils/uuid' -import { createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers" +import { + createTestSubgraph, + createTestSubgraphNode +} from './fixtures/subgraphHelpers' -describe("subgraphUtils", () => { - describe("getDirectSubgraphIds", () => { - it("should return empty set for graph with no subgraph nodes", () => { +describe('subgraphUtils', () => { + describe('getDirectSubgraphIds', () => { + it('should return empty set for graph with no subgraph nodes', () => { const graph = new LGraph() const result = getDirectSubgraphIds(graph) expect(result.size).toBe(0) }) - it("should find single subgraph node", () => { + it('should find single subgraph node', () => { const graph = new LGraph() const subgraph = createTestSubgraph() const subgraphNode = createTestSubgraphNode(subgraph) @@ -29,10 +31,10 @@ describe("subgraphUtils", () => { expect(result.has(subgraph.id)).toBe(true) }) - it("should find multiple unique subgraph nodes", () => { + it('should find multiple unique subgraph nodes', () => { const graph = new LGraph() - const subgraph1 = createTestSubgraph({ name: "Subgraph 1" }) - const subgraph2 = createTestSubgraph({ name: "Subgraph 2" }) + const subgraph1 = createTestSubgraph({ name: 'Subgraph 1' }) + const subgraph2 = createTestSubgraph({ name: 'Subgraph 2' }) const node1 = createTestSubgraphNode(subgraph1) const node2 = createTestSubgraphNode(subgraph2) @@ -46,7 +48,7 @@ describe("subgraphUtils", () => { expect(result.has(subgraph2.id)).toBe(true) }) - it("should return unique IDs when same subgraph is used multiple times", () => { + it('should return unique IDs when same subgraph is used multiple times', () => { const graph = new LGraph() const subgraph = createTestSubgraph() @@ -62,8 +64,8 @@ describe("subgraphUtils", () => { }) }) - describe("findUsedSubgraphIds", () => { - it("should handle graph with no subgraphs", () => { + describe('findUsedSubgraphIds', () => { + it('should handle graph with no subgraphs', () => { const graph = new LGraph() const registry = new Map() @@ -71,10 +73,10 @@ describe("subgraphUtils", () => { expect(result.size).toBe(0) }) - it("should find nested subgraphs", () => { + it('should find nested subgraphs', () => { const rootGraph = new LGraph() - const subgraph1 = createTestSubgraph({ name: "Level 1" }) - const subgraph2 = createTestSubgraph({ name: "Level 2" }) + const subgraph1 = createTestSubgraph({ name: 'Level 1' }) + const subgraph2 = createTestSubgraph({ name: 'Level 2' }) // Add subgraph1 node to root const node1 = createTestSubgraphNode(subgraph1) @@ -86,7 +88,7 @@ describe("subgraphUtils", () => { const registry = new Map([ [subgraph1.id, subgraph1], - [subgraph2.id, subgraph2], + [subgraph2.id, subgraph2] ]) const result = findUsedSubgraphIds(rootGraph, registry) @@ -95,10 +97,10 @@ describe("subgraphUtils", () => { expect(result.has(subgraph2.id)).toBe(true) }) - it("should handle circular references without infinite loop", () => { + it('should handle circular references without infinite loop', () => { const rootGraph = new LGraph() - const subgraph1 = createTestSubgraph({ name: "Subgraph 1" }) - const subgraph2 = createTestSubgraph({ name: "Subgraph 2" }) + const subgraph1 = createTestSubgraph({ name: 'Subgraph 1' }) + const subgraph2 = createTestSubgraph({ name: 'Subgraph 2' }) // Add subgraph1 to root const node1 = createTestSubgraphNode(subgraph1) @@ -114,7 +116,7 @@ describe("subgraphUtils", () => { const registry = new Map([ [subgraph1.id, subgraph1], - [subgraph2.id, subgraph2], + [subgraph2.id, subgraph2] ]) const result = findUsedSubgraphIds(rootGraph, registry) @@ -123,10 +125,10 @@ describe("subgraphUtils", () => { expect(result.has(subgraph2.id)).toBe(true) }) - it("should handle missing subgraphs in registry gracefully", () => { + it('should handle missing subgraphs in registry gracefully', () => { const rootGraph = new LGraph() - const subgraph1 = createTestSubgraph({ name: "Subgraph 1" }) - const subgraph2 = createTestSubgraph({ name: "Subgraph 2" }) + const subgraph1 = createTestSubgraph({ name: 'Subgraph 1' }) + const subgraph2 = createTestSubgraph({ name: 'Subgraph 2' }) // Add both subgraph nodes const node1 = createTestSubgraphNode(subgraph1) diff --git a/src/lib/litegraph/test/testExtensions.ts b/src/lib/litegraph/test/testExtensions.ts index 7e6d59d400..b5317c5fa2 100644 --- a/src/lib/litegraph/test/testExtensions.ts +++ b/src/lib/litegraph/test/testExtensions.ts @@ -1,15 +1,21 @@ -import type { ISerialisedGraph, SerialisableGraph } from "../src/types/serialisation" +import { test as baseTest } from 'vitest' -import { test as baseTest } from "vitest" +import { LGraph } from '@/lib/litegraph/src/LGraph' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' -import { LGraph } from "@/lib/litegraph/src/LGraph" -import { LiteGraph } from "@/lib/litegraph/src/litegraph" - -import floatingBranch from "./assets/floatingBranch.json" -import floatingLink from "./assets/floatingLink.json" -import linkedNodes from "./assets/linkedNodes.json" -import reroutesComplex from "./assets/reroutesComplex.json" -import { basicSerialisableGraph, minimalSerialisableGraph, oldSchemaGraph } from "./assets/testGraphs" +import type { + ISerialisedGraph, + SerialisableGraph +} from '../src/types/serialisation' +import floatingBranch from './assets/floatingBranch.json' +import floatingLink from './assets/floatingLink.json' +import linkedNodes from './assets/linkedNodes.json' +import reroutesComplex from './assets/reroutesComplex.json' +import { + basicSerialisableGraph, + minimalSerialisableGraph, + oldSchemaGraph +} from './assets/testGraphs' interface LitegraphFixtures { minimalGraph: LGraph @@ -27,7 +33,7 @@ interface DirtyFixtures { } export const test = baseTest.extend({ - minimalGraph: async ({ }, use) => { + minimalGraph: async ({}, use) => { // Before each test function const serialisable = structuredClone(minimalSerialisableGraph) const lGraph = new LGraph(serialisable) @@ -37,24 +43,30 @@ export const test = baseTest.extend({ }, minimalSerialisableGraph: structuredClone(minimalSerialisableGraph), oldSchemaGraph: structuredClone(oldSchemaGraph), - floatingLinkGraph: structuredClone(floatingLink as unknown as ISerialisedGraph), + floatingLinkGraph: structuredClone( + floatingLink as unknown as ISerialisedGraph + ), linkedNodesGraph: structuredClone(linkedNodes as unknown as ISerialisedGraph), floatingBranchGraph: async ({}, use) => { - const cloned = structuredClone(floatingBranch as unknown as ISerialisedGraph) + const cloned = structuredClone( + floatingBranch as unknown as ISerialisedGraph + ) const graph = new LGraph(cloned) await use(graph) }, reroutesComplexGraph: async ({}, use) => { - const cloned = structuredClone(reroutesComplex as unknown as ISerialisedGraph) + const cloned = structuredClone( + reroutesComplex as unknown as ISerialisedGraph + ) const graph = new LGraph(cloned) await use(graph) - }, + } }) /** Test that use {@link DirtyFixtures}. One test per file. */ export const dirtyTest = test.extend({ basicSerialisableGraph: async ({}, use) => { - if (!basicSerialisableGraph.nodes) throw new Error("Invalid test object") + if (!basicSerialisableGraph.nodes) throw new Error('Invalid test object') // Register node types for (const node of basicSerialisableGraph.nodes) { @@ -62,5 +74,5 @@ export const dirtyTest = test.extend({ } await use(structuredClone(basicSerialisableGraph)) - }, + } }) diff --git a/src/lib/litegraph/test/utils/spaceDistribution.test.ts b/src/lib/litegraph/test/utils/spaceDistribution.test.ts index 47b3b0a686..088ba24dbe 100644 --- a/src/lib/litegraph/test/utils/spaceDistribution.test.ts +++ b/src/lib/litegraph/test/utils/spaceDistribution.test.ts @@ -1,40 +1,43 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from 'vitest' -import { distributeSpace, type SpaceRequest } from "@/lib/litegraph/src/utils/spaceDistribution" +import { + type SpaceRequest, + distributeSpace +} from '@/lib/litegraph/src/utils/spaceDistribution' -describe("distributeSpace", () => { - it("should distribute space according to minimum sizes when space is limited", () => { +describe('distributeSpace', () => { + it('should distribute space according to minimum sizes when space is limited', () => { const requests: SpaceRequest[] = [ { minSize: 100 }, { minSize: 100 }, - { minSize: 100 }, + { minSize: 100 } ] expect(distributeSpace(300, requests)).toEqual([100, 100, 100]) }) - it("should distribute extra space equally when no maxSize", () => { + it('should distribute extra space equally when no maxSize', () => { const requests: SpaceRequest[] = [{ minSize: 100 }, { minSize: 100 }] expect(distributeSpace(400, requests)).toEqual([200, 200]) }) - it("should respect maximum sizes", () => { + it('should respect maximum sizes', () => { const requests: SpaceRequest[] = [ { minSize: 100, maxSize: 150 }, - { minSize: 100 }, + { minSize: 100 } ] expect(distributeSpace(400, requests)).toEqual([150, 250]) }) - it("should handle empty requests array", () => { + it('should handle empty requests array', () => { expect(distributeSpace(1000, [])).toEqual([]) }) - it("should handle negative total space", () => { + it('should handle negative total space', () => { const requests: SpaceRequest[] = [{ minSize: 100 }, { minSize: 100 }] expect(distributeSpace(-100, requests)).toEqual([100, 100]) }) - it("should handle total space smaller than minimum sizes", () => { + it('should handle total space smaller than minimum sizes', () => { const requests: SpaceRequest[] = [{ minSize: 100 }, { minSize: 100 }] expect(distributeSpace(100, requests)).toEqual([100, 100]) }) diff --git a/src/lib/litegraph/test/utils/textUtils.test.ts b/src/lib/litegraph/test/utils/textUtils.test.ts index 2081c2f6f1..214c550ae0 100644 --- a/src/lib/litegraph/test/utils/textUtils.test.ts +++ b/src/lib/litegraph/test/utils/textUtils.test.ts @@ -1,58 +1,58 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from 'vitest' -import { truncateText } from "@/lib/litegraph/src/utils/textUtils" +import { truncateText } from '@/lib/litegraph/src/utils/textUtils' -describe("truncateText", () => { +describe('truncateText', () => { const createMockContext = (charWidth: number = 10) => { return { - measureText: vi.fn((text: string) => ({ width: text.length * charWidth })), + measureText: vi.fn((text: string) => ({ width: text.length * charWidth })) } as unknown as CanvasRenderingContext2D } - it("should return original text if it fits within maxWidth", () => { + it('should return original text if it fits within maxWidth', () => { const ctx = createMockContext() - const result = truncateText(ctx, "Short", 100) - expect(result).toBe("Short") + const result = truncateText(ctx, 'Short', 100) + expect(result).toBe('Short') }) - it("should return original text if maxWidth is 0 or negative", () => { + it('should return original text if maxWidth is 0 or negative', () => { const ctx = createMockContext() - expect(truncateText(ctx, "Text", 0)).toBe("Text") - expect(truncateText(ctx, "Text", -10)).toBe("Text") + expect(truncateText(ctx, 'Text', 0)).toBe('Text') + expect(truncateText(ctx, 'Text', -10)).toBe('Text') }) - it("should truncate text and add ellipsis when text is too long", () => { + it('should truncate text and add ellipsis when text is too long', () => { const ctx = createMockContext(10) // 10 pixels per character - const result = truncateText(ctx, "This is a very long text", 100) + const result = truncateText(ctx, 'This is a very long text', 100) // 100px total, "..." takes 30px, leaving 70px for text (7 chars) - expect(result).toBe("This is...") + expect(result).toBe('This is...') }) - it("should use custom ellipsis when provided", () => { + it('should use custom ellipsis when provided', () => { const ctx = createMockContext(10) - const result = truncateText(ctx, "This is a very long text", 100, "…") + const result = truncateText(ctx, 'This is a very long text', 100, '…') // 100px total, "…" takes 10px, leaving 90px for text (9 chars) - expect(result).toBe("This is a…") + expect(result).toBe('This is a…') }) - it("should return only ellipsis if available width is too small", () => { + it('should return only ellipsis if available width is too small', () => { const ctx = createMockContext(10) - const result = truncateText(ctx, "Text", 20) // Only room for 2 chars, but "..." needs 3 - expect(result).toBe("...") + const result = truncateText(ctx, 'Text', 20) // Only room for 2 chars, but "..." needs 3 + expect(result).toBe('...') }) - it("should handle empty text", () => { + it('should handle empty text', () => { const ctx = createMockContext() - const result = truncateText(ctx, "", 100) - expect(result).toBe("") + const result = truncateText(ctx, '', 100) + expect(result).toBe('') }) - it("should use binary search efficiently", () => { + it('should use binary search efficiently', () => { const ctx = createMockContext(10) - const longText = "A".repeat(100) // 100 characters + const longText = 'A'.repeat(100) // 100 characters const result = truncateText(ctx, longText, 200) // Room for 20 chars total - expect(result).toBe(`${"A".repeat(17)}...`) // 17 chars + "..." = 20 chars = 200px + expect(result).toBe(`${'A'.repeat(17)}...`) // 17 chars + "..." = 20 chars = 200px // Verify binary search efficiency - should not measure every possible substring // Binary search for 100 chars should take around log2(100) ≈ 7 iterations @@ -62,21 +62,21 @@ describe("truncateText", () => { expect(callCount).toBeGreaterThan(5) }) - it("should handle unicode characters correctly", () => { + it('should handle unicode characters correctly', () => { const ctx = createMockContext(10) - const result = truncateText(ctx, "Hello 👋 World", 80) + const result = truncateText(ctx, 'Hello 👋 World', 80) // Assuming each char (including emoji) is 10px, total is 130px // 80px total, "..." takes 30px, leaving 50px for text (5 chars) - expect(result).toBe("Hello...") + expect(result).toBe('Hello...') }) - it("should handle exact boundary cases", () => { + it('should handle exact boundary cases', () => { const ctx = createMockContext(10) // Text exactly fits - expect(truncateText(ctx, "Exact", 50)).toBe("Exact") // 5 chars = 50px + expect(truncateText(ctx, 'Exact', 50)).toBe('Exact') // 5 chars = 50px // Text is exactly 1 pixel too long - expect(truncateText(ctx, "Exact!", 50)).toBe("Ex...") // 6 chars = 60px, truncated + expect(truncateText(ctx, 'Exact!', 50)).toBe('Ex...') // 6 chars = 60px, truncated }) }) diff --git a/src/lib/litegraph/test/utils/widget.test.ts b/src/lib/litegraph/test/utils/widget.test.ts index 1eaedabb00..553540126b 100644 --- a/src/lib/litegraph/test/utils/widget.test.ts +++ b/src/lib/litegraph/test/utils/widget.test.ts @@ -1,43 +1,42 @@ -import type { IWidgetOptions } from "@/lib/litegraph/src/types/widgets" +import { describe, expect, test } from 'vitest' -import { describe, expect, test } from "vitest" +import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets' +import { getWidgetStep } from '@/lib/litegraph/src/utils/widget' -import { getWidgetStep } from "@/lib/litegraph/src/utils/widget" - -describe("getWidgetStep", () => { - test("should return step2 when available", () => { +describe('getWidgetStep', () => { + test('should return step2 when available', () => { const options: IWidgetOptions = { step2: 0.5, - step: 20, + step: 20 } expect(getWidgetStep(options)).toBe(0.5) }) - test("should calculate from step when step2 is not available", () => { + test('should calculate from step when step2 is not available', () => { const options: IWidgetOptions = { - step: 20, + step: 20 } expect(getWidgetStep(options)).toBe(2) // 20 * 0.1 = 2 }) - test("should use default step value of 10 when neither step2 nor step is provided", () => { + test('should use default step value of 10 when neither step2 nor step is provided', () => { const options: IWidgetOptions = {} expect(getWidgetStep(options)).toBe(1) // 10 * 0.1 = 1 }) // Zero value is not allowed for step, fallback to 1. - test("should handle zero values correctly", () => { + test('should handle zero values correctly', () => { const optionsWithZeroStep2: IWidgetOptions = { step2: 0, - step: 20, + step: 20 } expect(getWidgetStep(optionsWithZeroStep2)).toBe(2) const optionsWithZeroStep: IWidgetOptions = { - step: 0, + step: 0 } expect(getWidgetStep(optionsWithZeroStep)).toBe(1)