npm run format

This commit is contained in:
Benjamin Lu
2025-08-05 09:57:28 -04:00
parent 50feb27339
commit c53f197de2
129 changed files with 10575 additions and 7230 deletions

View File

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

View File

@@ -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<TValue = unknown> {
constructor: new (...args: ConstructorParameters<typeof ContextMenu<TValue>>) => ContextMenu<TValue>
constructor: new (
...args: ConstructorParameters<typeof ContextMenu<TValue>>
) => ContextMenu<TValue>
}
/**
@@ -30,7 +35,10 @@ export class ContextMenu<TValue = unknown> {
* - 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<TValue> | null)[], options: IContextMenuOptions<TValue>) {
constructor(
values: readonly (string | IContextMenuValue<TValue> | null)[],
options: IContextMenuOptions<TValue>
) {
options ||= {}
this.options = options
@@ -38,79 +46,83 @@ export class ContextMenu<TValue = unknown> {
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<TValue> = document.createElement("div")
let classes = "litegraph litecontextmenu litemenubar-panel"
const root: ContextMenuDivElement<TValue> = 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<TValue = unknown> {
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<TValue = unknown> {
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<TValue = unknown> {
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<TValue> | null,
options: IContextMenuOptions<TValue>,
options: IContextMenuOptions<TValue>
): HTMLElement {
options ||= {}
const element: ContextMenuDivElement<TValue> = document.createElement("div")
element.className = "litemenu-entry submenu"
const element: ContextMenuDivElement<TValue> = 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<TValue>, e: MouseEvent) {
@@ -273,13 +295,13 @@ export class ContextMenu<TValue = unknown> {
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<TValue = unknown> {
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<TValue = unknown> {
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<TValue = unknown> {
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<TValue = unknown> {
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<TValue = unknown> {
// returns the top most menu
getTopMenu(): ContextMenu<TValue> {
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<TValue = unknown> {
/** @deprecated Unused. */
static isCursorOverElement(
event: MouseEvent,
element: HTMLDivElement,
element: HTMLDivElement
): boolean {
const left = event.clientX
const top = event.clientY

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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<string, unknown> {
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<string> | IContextMenuValue<string | null> | null)[] {
getMenuOptions(): (
| IContextMenuValue<string>
| IContextMenuValue<string | null>
| 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 }
]
}

View File

@@ -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) {

File diff suppressed because it is too large Load Diff

View File

@@ -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<ReadonlyLinkNetwork, "getNodeById" | "links" | "getLink" | "inputNode" | "outputNode">
type BasicReadonlyNetwork = Pick<
ReadonlyLinkNetwork,
'getNodeById' | 'links' | 'getLink' | 'inputNode' | 'outputNode'
>
// this is the class in charge of storing link information
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
@@ -115,7 +124,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
}
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<SerialisableLLink> {
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<SerialisableLLink> {
data.origin_slot,
data.target_id,
data.target_slot,
data.parentId,
data.parentId
)
}
@@ -190,18 +199,16 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
* this reroute or the reroute before it. Otherwise, an empty array.
*/
static getReroutes(
network: Pick<ReadonlyLinkNetwork, "reroutes">,
linkSegment: LinkSegment,
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
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<ReadonlyLinkNetwork, "reroutes">,
linkSegment: LinkSegment,
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
linkSegment: LinkSegment
): Reroute | undefined {
return LLink.getReroutes(network, linkSegment).at(0)
}
@@ -215,9 +222,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
*/
static findNextReroute(
network: Pick<ReadonlyLinkNetwork, "reroutes">,
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
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<SerialisableLLink> {
* @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<SerialisableLLink> {
* @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<SerialisableLLink> {
* 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<SerialisableLLink> {
* @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<LinkId>, network: BasicReadonlyNetwork): ResolvedConnection[] {
static resolveMany(
linkIds: Iterable<LinkId>,
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<SerialisableLLink> {
* 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<SerialisableLLink> {
* @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<SerialisableLLink> {
* @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<SerialisableLLink> {
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<SerialisableLLink> {
this.origin_slot,
this.target_id,
this.target_slot,
this.type,
this.type
]
}
@@ -438,7 +485,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
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

View File

@@ -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<keyof typeof RenderShape>)[]
VALID_SHAPES = ['default', 'box', 'round', 'card'] satisfies (
| 'default'
| Lowercase<keyof typeof RenderShape>
)[]
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<unknown>,
options?: Dictionary<unknown>
): 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<number> = { "": 1 }
const categories: Dictionary<number> = { '': 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<T extends object | undefined | null>(obj: T, target?: T): WhenNullish<T, null> {
cloneObject<T extends object | undefined | null>(
obj: T,
target?: T
): WhenNullish<T, null> {
if (obj == null) return null as WhenNullish<T, null>
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)
)
}
}

View File

@@ -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<V> implements ProxyHandler<Map<number | string, V>> {
export class MapProxyHandler<V>
implements ProxyHandler<Map<number | string, V>>
{
getOwnPropertyDescriptor(
target: Map<number | string, V>,
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<number | string, V>, 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<V> implements ProxyHandler<Map<number | string, V>>
get(target: Map<number | string, V>, 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<number | string, V>, p: string | symbol, newValue: any): boolean {
if (typeof p === "symbol") return false
set(
target: Map<number | string, V>,
p: string | symbol,
newValue: any
): boolean {
if (typeof p === 'symbol') return false
const int = parseInt(p, 10)
target.set(!isNaN(int) ? int : p, newValue)

View File

@@ -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<SerialisableReroute> {
export class Reroute
implements Positionable, LinkSegment, Serialisable<SerialisableReroute>
{
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<Serialis
set pos(value: Point) {
if (!(value?.length >= 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<Serialis
/** Colour of the first link that rendered this reroute */
get colour(): CanvasColour {
return this._colour ?? "#18184d"
return this._colour ?? '#18184d'
}
/**
@@ -163,20 +166,14 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
const linkId = this.linkIds.values().next().value
return linkId === undefined
? undefined
: this.#network
.deref()
?.links
.get(linkId)
: this.#network.deref()?.links.get(linkId)
}
get firstFloatingLink(): LLink | undefined {
const linkId = this.floatingLinkIds.values().next().value
return linkId === undefined
? undefined
: this.#network
.deref()
?.floatingLinks
.get(linkId)
: this.#network.deref()?.floatingLinks.get(linkId)
}
/** @inheritdoc */
@@ -202,7 +199,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
pos?: Point,
parentId?: RerouteId,
linkIds?: Iterable<LinkId>,
floatingLinkIds?: Iterable<LinkId>,
floatingLinkIds?: Iterable<LinkId>
) {
this.#network = new WeakRef(network)
this.parentId = parentId
@@ -223,7 +220,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
parentId: RerouteId | undefined,
pos?: Point,
linkIds?: Iterable<LinkId>,
floating?: FloatingRerouteSlot,
floating?: FloatingRerouteSlot
): void {
this.parentId = parentId
if (pos) this.pos = pos
@@ -236,7 +233,10 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
* @param links Collection of valid links
* @returns true if any links remain after validation
*/
validateLinks(links: ReadonlyMap<LinkId, LLink>, floatingLinks: ReadonlyMap<LinkId, LLink>): boolean {
validateLinks(
links: ReadonlyMap<LinkId, LLink>,
floatingLinks: ReadonlyMap<LinkId, LLink>
): 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<Serialis
*/
findNextReroute(
withParentId: RerouteId,
visited = new Set<Reroute>(),
visited = new Set<Reroute>()
): 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<Serialis
return this.#network
.deref()
?.reroutes
.get(this.#parentId)
?.reroutes.get(this.#parentId)
?.findNextReroute(withParentId, visited)
}
@@ -300,7 +299,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
* Finds the output node and output slot of the first link passing through this reroute.
* @returns The output node and output slot of the first link passing through this reroute, or `undefined` if no link is found.
*/
findSourceOutput(): { node: LGraphNode, output: INodeOutputSlot } | undefined {
findSourceOutput():
| { node: LGraphNode; output: INodeOutputSlot }
| undefined {
const link = this.firstLink ?? this.firstFloatingLink
if (!link) return
@@ -309,7 +310,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
return {
node,
output: node.outputs[link.origin_slot],
output: node.outputs[link.origin_slot]
}
}
@@ -317,7 +318,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
* Finds the inputs and nodes of (floating) links passing through this reroute.
* @returns An array of objects containing the node and input slot of each link passing through this reroute.
*/
findTargetInputs(): { node: LGraphNode, input: INodeInputSlot, link: LLink }[] | undefined {
findTargetInputs():
| { node: LGraphNode; input: INodeInputSlot; link: LLink }[]
| undefined {
const network = this.#network.deref()
if (!network) return
@@ -335,7 +338,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
function addAllResults(
network: ReadonlyLinkNetwork,
linkIds: Iterable<LinkId>,
links: ReadonlyMap<LinkId, LLink>,
links: ReadonlyMap<LinkId, LLink>
) {
for (const linkId of linkIds) {
const link = links.get(linkId)
@@ -355,11 +358,11 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
* @param from Filters the links by the currently connected link side.
* @returns An array of floating links
*/
getFloatingLinks(from: "input" | "output"): LLink[] | undefined {
getFloatingLinks(from: 'input' | 'output'): LLink[] | undefined {
const floatingLinks = this.#network.deref()?.floatingLinks
if (!floatingLinks) return
const idProp = from === "input" ? "origin_id" : "target_id"
const idProp = from === 'input' ? 'origin_id' : 'target_id'
const out: LLink[] = []
for (const linkId of this.floatingLinkIds) {
@@ -375,10 +378,15 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
* @param output The new origin output slot
* @param index The slot index of {@link output}
*/
setFloatingLinkOrigin(node: LGraphNode, output: INodeOutputSlot, index: number) {
setFloatingLinkOrigin(
node: LGraphNode,
output: INodeOutputSlot,
index: number
) {
const network = this.#network.deref()
const floatingOutLinks = this.getFloatingLinks("output")
if (!floatingOutLinks) throw new Error("[setFloatingLinkOrigin]: Invalid network.")
const floatingOutLinks = this.getFloatingLinks('output')
if (!floatingOutLinks)
throw new Error('[setFloatingLinkOrigin]: Invalid network.')
if (!floatingOutLinks.length) return
output._floatingLinks ??= new Set()
@@ -387,10 +395,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
// Update cached floating links
output._floatingLinks.add(link)
network?.getNodeById(link.origin_id)
?.outputs[link.origin_slot]
?._floatingLinks
?.delete(link)
network
?.getNodeById(link.origin_id)
?.outputs[link.origin_slot]?._floatingLinks?.delete(link)
// Update the floating link
link.origin_id = node.id
@@ -426,7 +433,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
const floatingLink = network.floatingLinks.get(linkId)
if (!floatingLink) {
console.warn(`[Reroute.removeFloatingLink] Floating link not found: ${linkId}, ignoring and discarding ID.`)
console.warn(
`[Reroute.removeFloatingLink] Floating link not found: ${linkId}, ignoring and discarding ID.`
)
this.floatingLinkIds.delete(linkId)
return
}
@@ -458,7 +467,11 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
network.removeReroute(this.id)
}
calculateAngle(lastRenderTime: number, network: ReadonlyLinkNetwork, linkStart: Point): void {
calculateAngle(
lastRenderTime: number,
network: ReadonlyLinkNetwork,
linkStart: Point
): void {
// Ensure we run once per render
if (!(lastRenderTime > this.#lastRenderTime)) return
this.#lastRenderTime = lastRenderTime
@@ -484,11 +497,14 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
const originToReroute = Math.atan2(
this.#pos[1] - linkStart[1],
this.#pos[0] - linkStart[0],
this.#pos[0] - linkStart[0]
)
let diff = (originToReroute - sum) * 0.5
if (Math.abs(diff) > 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<Serialis
* @param linkIds The IDs of the links to calculate
* @param links The link container from the link network.
*/
function calculateAngles(linkIds: Iterable<LinkId>, links: ReadonlyMap<LinkId, LLink>) {
function calculateAngles(
linkIds: Iterable<LinkId>,
links: ReadonlyMap<LinkId, LLink>
) {
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<Serialis
ctx.arc(pos[0], pos[1], Reroute.radius, 0, 2 * Math.PI)
if (this.linkIds.size === 0) {
ctx.fillStyle = backgroundPattern ?? "#797979"
ctx.fillStyle = backgroundPattern ?? '#797979'
ctx.fill()
ctx.globalAlpha = globalAlpha * 0.33
}
ctx.fillStyle = this.colour
ctx.lineWidth = Reroute.radius * 0.1
ctx.strokeStyle = "rgb(0,0,0,0.5)"
ctx.strokeStyle = 'rgb(0,0,0,0.5)'
ctx.fill()
ctx.stroke()
ctx.fillStyle = "#ffffff55"
ctx.strokeStyle = "rgb(0,0,0,0.3)"
ctx.fillStyle = '#ffffff55'
ctx.strokeStyle = 'rgb(0,0,0,0.3)'
ctx.beginPath()
ctx.arc(pos[0], pos[1], Reroute.radius * 0.8, 0, 2 * Math.PI)
ctx.fill()
ctx.stroke()
if (this.selected) {
ctx.strokeStyle = "#fff"
ctx.strokeStyle = '#fff'
ctx.beginPath()
ctx.arc(pos[0], pos[1], Reroute.radius * 1.2, 0, 2 * Math.PI)
ctx.stroke()
@@ -649,7 +668,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
parentId,
pos: [pos[0], pos[1]],
linkIds: [...linkIds],
floating: this.floating ? { slotType: this.floating.slotType } : undefined,
floating: this.floating ? { slotType: this.floating.slotType } : undefined
}
}
}
@@ -731,14 +750,16 @@ class RerouteSlot {
*/
draw(ctx: CanvasRenderingContext2D): void {
const { fillStyle, strokeStyle, lineWidth } = ctx
const { showOutline, hovering, pos: [x, y] } = this
const {
showOutline,
hovering,
pos: [x, y]
} = this
if (!showOutline) return
try {
ctx.fillStyle = hovering
? this.#reroute.colour
: "rgba(127,127,127,0.3)"
ctx.strokeStyle = "rgb(0,0,0,0.5)"
ctx.fillStyle = hovering ? this.#reroute.colour : 'rgba(127,127,127,0.3)'
ctx.strokeStyle = 'rgb(0,0,0,0.5)'
ctx.lineWidth = 1
ctx.beginPath()
@@ -760,7 +781,11 @@ class RerouteSlot {
* @param id The ID of "this" reroute
* @returns The position of the next reroute or the input slot target, otherwise `undefined`.
*/
function getNextPos(network: ReadonlyLinkNetwork, link: LLink | undefined, id: RerouteId) {
function getNextPos(
network: ReadonlyLinkNetwork,
link: LLink | undefined,
id: RerouteId
) {
if (!link) return
const linkPos = LLink.findNextReroute(network, link, id)?.pos

View File

@@ -1,17 +1,23 @@
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 } from "@/lib/litegraph/src/interfaces"
import type { INodeInputSlot } from "@/lib/litegraph/src/interfaces"
import type { 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 {
SUBGRAPH_INPUT_ID,
SUBGRAPH_OUTPUT_ID
} from '@/lib/litegraph/src/constants'
import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget'
import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap'
import type {
INodeOutputSlot,
LinkNetwork
} from '@/lib/litegraph/src/interfaces'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { 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 { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from "@/lib/litegraph/src/constants"
import { LinkDirection } from "@/lib/litegraph/src/types/globalEnums"
import type { RenderLink } from './RenderLink'
/**
* Represents a floating link that is currently being dragged from one slot to another.
@@ -45,24 +51,30 @@ export class FloatingRenderLink 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
if (outputNodeId !== -1) {
// Output connected
const outputNode = network.getNodeById(outputNodeId) ?? undefined
if (!outputNode) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Output node [${outputNodeId}] not found.`)
if (!outputNode)
throw new Error(
`Creating DraggingRenderLink for link [${link.id}] failed: Output node [${outputNodeId}] not found.`
)
const outputSlot = outputNode?.outputs.at(outputIndex)
if (!outputSlot) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Output slot [${outputIndex}] not found.`)
if (!outputSlot)
throw new Error(
`Creating DraggingRenderLink for link [${link.id}] failed: Output slot [${outputIndex}] not found.`
)
this.outputNodeId = outputNodeId
this.outputNode = outputNode
@@ -80,10 +92,16 @@ export class FloatingRenderLink implements RenderLink {
} else {
// Input connected
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
@@ -101,15 +119,15 @@ export class FloatingRenderLink implements RenderLink {
}
canConnectToInput(): boolean {
return this.toType === "input"
return this.toType === 'input'
}
canConnectToOutput(): boolean {
return this.toType === "output"
return this.toType === 'output'
}
canConnectToReroute(reroute: Reroute): boolean {
if (this.toType === "input") {
if (this.toType === 'input') {
if (reroute.origin_id === this.inputNode?.id) return false
} else {
if (reroute.origin_id === this.outputNode?.id) return false
@@ -117,7 +135,11 @@ export class FloatingRenderLink implements RenderLink {
return true
}
connectToInput(node: LGraphNode, input: INodeInputSlot, _events?: CustomEventTarget<LinkConnectorEventMap>): void {
connectToInput(
node: LGraphNode,
input: INodeInputSlot,
_events?: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>): void {
connectToOutput(
node: LGraphNode,
output: INodeOutputSlot,
_events?: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>): void {
connectToSubgraphInput(
input: SubgraphInput,
_events?: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>): void {
connectToSubgraphOutput(
output: SubgraphOutput,
_events?: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>,
{ node: inputNode, input }: { node: LGraphNode; input: INodeInputSlot },
events: CustomEventTarget<LinkConnectorEventMap>
) {
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<LinkConnectorEventMap>,
events: CustomEventTarget<LinkConnectorEventMap>
) {
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)
}
}

View File

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

View File

@@ -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<LinkConnectorEventMap>()
@@ -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<K extends keyof LinkConnectorEventMap>(
eventName: K,
listener: Parameters<typeof this.events.addEventListener<K>>[1],
options?: Parameters<typeof this.events.addEventListener<K>>[2],
options?: Parameters<typeof this.events.addEventListener<K>>[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
}

View File

@@ -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<LinkConnectorEventMap>): LLink | null | undefined {
connectToInput(
inputNode: LGraphNode,
input: INodeInputSlot,
events: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>): void {
const newLink = output.connect(this.fromSlot, this.node, this.fromReroute?.id)
events?.dispatch("link-created", newLink)
connectToSubgraphOutput(
output: SubgraphOutput,
events?: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>,
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 {

View File

@@ -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<LinkConnectorEventMap>): void
abstract connectToOutput(node: LGraphNode, output: INodeOutputSlot, events?: CustomEventTarget<LinkConnectorEventMap>): void
abstract connectToSubgraphInput(input: SubgraphInput, events?: CustomEventTarget<LinkConnectorEventMap>): void
abstract connectToSubgraphOutput(output: SubgraphOutput, events?: CustomEventTarget<LinkConnectorEventMap>): void
abstract connectToRerouteInput(reroute: Reroute, { node, input, link }: { node: LGraphNode, input: INodeInputSlot, link: LLink }, events: CustomEventTarget<LinkConnectorEventMap>, originalReroutes: Reroute[]): void
abstract connectToRerouteOutput(reroute: Reroute, outputNode: LGraphNode, output: INodeOutputSlot, events: CustomEventTarget<LinkConnectorEventMap>): void
abstract connectToInput(
node: LGraphNode,
input: INodeInputSlot,
events?: CustomEventTarget<LinkConnectorEventMap>
): void
abstract connectToOutput(
node: LGraphNode,
output: INodeOutputSlot,
events?: CustomEventTarget<LinkConnectorEventMap>
): void
abstract connectToSubgraphInput(
input: SubgraphInput,
events?: CustomEventTarget<LinkConnectorEventMap>
): void
abstract connectToSubgraphOutput(
output: SubgraphOutput,
events?: CustomEventTarget<LinkConnectorEventMap>
): void
abstract connectToRerouteInput(
reroute: Reroute,
{
node,
input,
link
}: { node: LGraphNode; input: INodeInputSlot; link: LLink },
events: CustomEventTarget<LinkConnectorEventMap>,
originalReroutes: Reroute[]
): void
abstract connectToRerouteOutput(
reroute: Reroute,
outputNode: LGraphNode,
output: INodeOutputSlot,
events: CustomEventTarget<LinkConnectorEventMap>
): void
abstract disconnect(): boolean
}

View File

@@ -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<LinkConnectorEventMap>): LLink | null | undefined {
connectToOutput(
outputNode: LGraphNode,
output: INodeOutputSlot,
events: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>): void {
const newLink = input.connect(this.fromSlot, this.node, this.fromReroute?.id)
events?.dispatch("link-created", newLink)
connectToSubgraphInput(
input: SubgraphInput,
events?: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>,
events: CustomEventTarget<LinkConnectorEventMap>
): 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 {

View File

@@ -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<SubgraphInput | SubgraphOutput>
/** 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<LinkConnectorEventMap>): void
connectToOutput(node: LGraphNode, output: INodeOutputSlot, events?: CustomEventTarget<LinkConnectorEventMap>): void
connectToSubgraphInput(input: SubgraphInput, events?: CustomEventTarget<LinkConnectorEventMap>): void
connectToSubgraphOutput(output: SubgraphOutput, events?: CustomEventTarget<LinkConnectorEventMap>): void
connectToInput(
node: LGraphNode,
input: INodeInputSlot,
events?: CustomEventTarget<LinkConnectorEventMap>
): void
connectToOutput(
node: LGraphNode,
output: INodeOutputSlot,
events?: CustomEventTarget<LinkConnectorEventMap>
): void
connectToSubgraphInput(
input: SubgraphInput,
events?: CustomEventTarget<LinkConnectorEventMap>
): void
connectToSubgraphOutput(
output: SubgraphOutput,
events?: CustomEventTarget<LinkConnectorEventMap>
): void
connectToRerouteInput(
reroute: Reroute,
{ node, input, link }: { node: LGraphNode, input: INodeInputSlot, link: LLink },
{
node,
input,
link
}: { node: LGraphNode; input: INodeInputSlot; link: LLink },
events: CustomEventTarget<LinkConnectorEventMap>,
originalReroutes: Reroute[],
originalReroutes: Reroute[]
): void
connectToRerouteOutput(
reroute: Reroute,
outputNode: LGraphNode,
output: INodeOutputSlot,
events: CustomEventTarget<LinkConnectorEventMap>,
events: CustomEventTarget<LinkConnectorEventMap>
): void
}

View File

@@ -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<LinkConnectorEventMap>) {
connectToInput(
node: LGraphNode,
input: INodeInputSlot,
events: CustomEventTarget<LinkConnectorEventMap>
) {
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<LinkConnectorEventMap>,
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.')
}
}

View File

@@ -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<LinkConnectorEventMap>) {
connectToInput(
node: LGraphNode,
input: INodeInputSlot,
events: CustomEventTarget<LinkConnectorEventMap>
) {
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<LinkConnectorEventMap>) {
const newLink = output.connect(this.fromSlot, this.node, this.fromReroute?.id)
events.dispatch("link-created", newLink)
connectToSubgraphOutput(
output: SubgraphOutput,
events: CustomEventTarget<LinkConnectorEventMap>
) {
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<LinkConnectorEventMap>,
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.')
}
}

View File

@@ -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<LinkConnectorEventMap>) {
connectToOutput(
node: LGraphNode,
output: INodeOutputSlot,
events: CustomEventTarget<LinkConnectorEventMap>
) {
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<LinkConnectorEventMap>,
events: CustomEventTarget<LinkConnectorEventMap>
): 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.')
}
}

View File

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

View File

@@ -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<LinkConnectorEventMap>) {
connectToOutput(
node: LGraphNode,
output: INodeOutputSlot,
events: CustomEventTarget<LinkConnectorEventMap>
) {
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<LinkConnectorEventMap>): void {
const newLink = input.connect(this.fromSlot, this.node, this.fromReroute?.id)
events?.dispatch("link-created", newLink)
connectToSubgraphInput(
input: SubgraphInput,
events?: CustomEventTarget<LinkConnectorEventMap>
): 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<LinkConnectorEventMap>,
events: CustomEventTarget<LinkConnectorEventMap>
): 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.')
}
}

View File

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

View File

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

View File

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

View File

@@ -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<T> = {
readonly [K in keyof T]: ((this: EventTarget, ev: CustomEvent<T[K]>) => any) | EventListenerObject | null
readonly [K in keyof T]:
| ((this: EventTarget, ev: CustomEvent<T[K]>) => any)
| EventListenerObject
| null
}
/**
@@ -9,18 +12,18 @@ type EventListeners<T> = {
*/
export interface ICustomEventTarget<
EventMap extends Record<Keys, unknown>,
Keys extends keyof EventMap & string = keyof EventMap & string,
Keys extends keyof EventMap & string = keyof EventMap & string
> {
addEventListener<K extends Keys>(
type: K,
listener: EventListeners<EventMap>[K],
options?: boolean | AddEventListenerOptions,
options?: boolean | AddEventListenerOptions
): void
removeEventListener<K extends Keys>(
type: K,
listener: EventListeners<EventMap>[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, unknown>,
Keys extends keyof EventMap & string = keyof EventMap & string,
Keys extends keyof EventMap & string = keyof EventMap & string
> {
dispatch<T extends keyof NeverNever<EventMap>>(type: T, detail: EventMap[T]): boolean
dispatch<T extends keyof NeverNever<EventMap>>(
type: T,
detail: EventMap[T]
): boolean
dispatch<T extends keyof PickNevers<EventMap>>(type: T): boolean
}
@@ -75,10 +81,12 @@ export interface CustomEventDispatcher<
* ```
*/
export class CustomEventTarget<
EventMap extends Record<Keys, unknown>,
Keys extends keyof EventMap & string = keyof EventMap & string,
>
extends EventTarget implements ICustomEventTarget<EventMap, Keys> {
EventMap extends Record<Keys, unknown>,
Keys extends keyof EventMap & string = keyof EventMap & string
>
extends EventTarget
implements ICustomEventTarget<EventMap, Keys>
{
/**
* 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<T extends keyof NeverNever<EventMap>>(type: T, detail: EventMap[T]): boolean
dispatch<T extends keyof NeverNever<EventMap>>(
type: T,
detail: EventMap[T]
): boolean
dispatch<T extends keyof PickNevers<EventMap>>(type: T): boolean
dispatch<T extends keyof EventMap>(type: T, detail?: EventMap[T]) {
const event = new CustomEvent(type as string, { detail, cancelable: true })
@@ -96,7 +107,7 @@ export class CustomEventTarget<
override addEventListener<K extends Keys>(
type: K,
listener: EventListeners<EventMap>[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<K extends Keys>(
type: K,
listener: EventListeners<EventMap>[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}. */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Rectangle>,
| "setHeightBottomAnchored"
| "setWidthRightAnchored"
| "resizeTopLeft"
| "resizeBottomLeft"
| "resizeTopRight"
| "resizeBottomRight"
| "resizeBottomRight"
| "updateTo"
| 'setHeightBottomAnchored'
| 'setWidthRightAnchored'
| 'resizeTopLeft'
| 'resizeBottomLeft'
| 'resizeTopRight'
| 'resizeBottomRight'
| 'resizeBottomRight'
| 'updateTo'
>

View File

@@ -4,6 +4,6 @@
export class RecursionError extends Error {
constructor(subject: string) {
super(subject)
this.name = "RecursionError"
this.name = 'RecursionError'
}
}

View File

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

View File

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

View File

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

View File

@@ -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<T> = { [key: string]: T }
@@ -20,13 +21,21 @@ export type NullableProperties<T> = {
* 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, Result> = T & {} | (T extends null ? Result : T extends undefined ? Result : T & {})
export type WhenNullish<T, Result> =
| (T & {})
| (T extends null ? Result : T extends undefined ? Result : T & {})
/** A type with each of the {@link Properties} made optional. */
export type OptionalProps<T, Properties extends keyof T> = Omit<T, Properties> & { [K in Properties]?: T[K] }
export type OptionalProps<T, Properties extends keyof T> = Omit<
T,
Properties
> & { [K in Properties]?: T[K] }
/** A type with each of the {@link Properties} marked as required. */
export type RequiredProps<T, Properties extends keyof T> = Omit<T, Properties> & { [K in Properties]-?: T[K] }
export type RequiredProps<T, Properties extends keyof T> = 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<T1, T2> = {
@@ -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<T extends TypedArrays | TypedBigIntArrays> =
Omit<Readonly<T>, "fill" | "copyWithin" | "reverse" | "set" | "sort" | "subarray">
Omit<
Readonly<T>,
'fill' | 'copyWithin' | 'reverse' | 'set' | 'sort' | 'subarray'
>
/** Union of property names that are of type Match */
export type KeysOfType<T, Match> = Exclude<{ [P in keyof T]: T[P] extends Match ? P : never }[keyof T], undefined>
export type KeysOfType<T, Match> = 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<T, Match> = { [P in keyof T]: Extract<T[P], Match> }
@@ -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<TValue = unknown, TExtra = unknown> extends IContextMenuBase {
export interface IContextMenuOptions<TValue = unknown, TExtra = unknown>
extends IContextMenuBase {
ignore_item_callbacks?: boolean
parentMenu?: ContextMenu<TValue>
event?: MouseEvent
@@ -401,11 +420,15 @@ export interface IContextMenuOptions<TValue = unknown, TExtra = unknown> extends
options?: unknown,
event?: MouseEvent,
previous_menu?: ContextMenu<TValue>,
extra?: unknown,
extra?: unknown
): void | boolean
}
export interface IContextMenuValue<TValue = unknown, TExtra = unknown, TCallbackValue = unknown> 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<TValue = unknown, TExtra = unknown, TCallback
options?: unknown,
event?: MouseEvent,
previous_menu?: ContextMenu<TValue>,
extra?: TExtra,
extra?: TExtra
): void | boolean
}
export interface IContextMenuSubmenu<TValue = unknown> extends IContextMenuOptions<TValue> {
export interface IContextMenuSubmenu<TValue = unknown>
extends IContextMenuOptions<TValue> {
options: ConstructorParameters<typeof ContextMenu<TValue>>[0]
}
export interface ContextMenuDivElement<TValue = unknown> extends HTMLDivElement {
export interface ContextMenuDivElement<TValue = unknown>
extends HTMLDivElement {
value?: string | IContextMenuValue<TValue>
onclick_callback?: never
}
export type INodeSlotContextItem = [string, ISlotType, Partial<INodeInputSlot & INodeOutputSlot>]
export type INodeSlotContextItem = [
string,
ISlotType,
Partial<INodeInputSlot & INodeOutputSlot>
]
export interface DefaultConnectionColors {
getConnectedColor(type: ISlotType): CanvasColour
@@ -463,7 +492,8 @@ export type CallbackParams<T extends ((...args: any) => any) | undefined> =
* Shorthand for {@link ReturnType} of optional callbacks.
* @see {@link CallbackParams}
*/
export type CallbackReturn<T extends ((...args: any) => any) | undefined> = ReturnType<Exclude<T, undefined>>
export type CallbackReturn<T extends ((...args: any) => any) | undefined> =
ReturnType<Exclude<T, undefined>>
/**
* An object that can be hovered over.

View File

@@ -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<unknown> | undefined,
node: LGraphNode,
node: LGraphNode
) => boolean | void
export interface LinkReleaseContext {
@@ -88,17 +83,17 @@ export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
// 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'

View File

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

View File

@@ -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<INodeInputSlot, "boundingRect">, node: LGraphNode) {
constructor(
slot: OptionalProps<INodeInputSlot, 'boundingRect'>,
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<IDrawOptions, "doStroke" | "labelPosition">) {
override draw(
ctx: CanvasRenderingContext2D,
options: Omit<IDrawOptions, 'doStroke' | 'labelPosition'>
) {
const { textAlign } = ctx
ctx.textAlign = "left"
ctx.textAlign = 'left'
super.draw(ctx, {
...options,
labelPosition: LabelPosition.Right,
doStroke: false,
doStroke: false
})
ctx.textAlign = textAlign

View File

@@ -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<INodeOutputSlot, "boundingRect">, node: LGraphNode) {
constructor(
slot: OptionalProps<INodeOutputSlot, 'boundingRect'>,
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<IDrawOptions, "doStroke" | "labelPosition">) {
override draw(
ctx: CanvasRenderingContext2D,
options: Omit<IDrawOptions, 'doStroke' | 'labelPosition'>
) {
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

View File

@@ -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<INodeSlot, "boundingRect">, node: LGraphNode) {
constructor(
slot: OptionalProps<INodeSlot, 'boundingRect'>,
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<ISubgraphInput, "link" | "boundingRect"> = 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) {

View File

@@ -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. */

View File

@@ -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<ISerialisableNodeInput, ISerialisableNodeOutput>
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
}

View File

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

View File

@@ -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)) {

View File

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

View File

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

View File

@@ -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<ExecutableNodeDTO, "graph" | "node" | "subgraphNode">
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<typeof this.node.applyToGraph>): CallbackReturn<typeof this.node.applyToGraph>
applyToGraph?(
...args: CallbackParams<typeof this.node.applyToGraph>
): CallbackReturn<typeof this.node.applyToGraph>
/** 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<ExecutionId, ExecutableLGraphNode>,
/** 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<string>()): ResolvedInput | undefined {
resolveInput(
slot: number,
visited = new Set<string>()
): 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<string>): ResolvedInput | undefined {
resolveOutput(
slot: number,
type: ISlotType,
visited: Set<string>
): 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<string>): ResolvedInput | undefined {
#resolveSubgraphOutput(
slot: number,
type: ISlotType,
visited: Set<string>
): 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
)
}
}

View File

@@ -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"
export { Subgraph, type GraphOrSubgraph } from '@/lib/litegraph/src/LGraph'

View File

@@ -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<TSlot extends SubgraphInput | SubgraphOutput> implements Positionable, Hoverable, Serialisable<ExportedSubgraphIONode> {
export abstract class SubgraphIONodeBase<
TSlot extends SubgraphInput | SubgraphOutput
>
implements Positionable, Hoverable, Serialisable<ExportedSubgraphIONode>
{
static margin = 10
static minWidth = 100
static roundedRadius = 10
@@ -55,7 +75,7 @@ export abstract class SubgraphIONodeBase<TSlot extends SubgraphInput | SubgraphO
}
protected get sideStrokeStyle(): CanvasColour {
return this.isPointerOver ? "white" : "#efefef"
return this.isPointerOver ? 'white' : '#efefef'
}
abstract readonly slots: TSlot[]
@@ -63,7 +83,7 @@ export abstract class SubgraphIONodeBase<TSlot extends SubgraphInput | SubgraphO
constructor(
/** The subgraph that this node belongs to. */
readonly subgraph: Subgraph,
readonly subgraph: Subgraph
) {}
move(deltaX: number, deltaY: number): void {
@@ -76,7 +96,11 @@ export abstract class SubgraphIONodeBase<TSlot extends SubgraphInput | SubgraphO
return this.pinned ? false : snapPoint(this.pos, snapTo)
}
abstract onPointerDown(e: CanvasPointerEvent, pointer: CanvasPointer, linkConnector: LinkConnector): void
abstract onPointerDown(
e: CanvasPointerEvent,
pointer: CanvasPointer,
linkConnector: LinkConnector
): void
// #region Hoverable
@@ -88,7 +112,9 @@ export abstract class SubgraphIONodeBase<TSlot extends SubgraphInput | SubgraphO
onPointerMove(e: CanvasPointerEvent): CanvasItem {
const containsPoint = this.boundingRect.containsXy(e.canvasX, e.canvasY)
let underPointer = containsPoint ? CanvasItem.SubgraphIoNode : CanvasItem.Nothing
let underPointer = containsPoint
? CanvasItem.SubgraphIoNode
: CanvasItem.Nothing
if (containsPoint) {
if (!this.isPointerOver) this.onPointerEnter()
@@ -153,16 +179,13 @@ export abstract class SubgraphIONodeBase<TSlot extends SubgraphInput | SubgraphO
const options: IContextMenuValue[] = this.#getSlotMenuOptions(slot)
if (!(options.length > 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<TSlot extends SubgraphInput | SubgraphO
// Disconnect option if slot has connections
if (slot !== this.emptySlot && slot.linkIds.length > 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<TSlot extends SubgraphInput | SubgraphO
* @param slot The slot
* @param event The event that triggered the context menu.
*/
#onSlotMenuAction(selectedItem: IContextMenuValue, slot: TSlot, event: CanvasPointerEvent): void {
#onSlotMenuAction(
selectedItem: IContextMenuValue,
slot: TSlot,
event: CanvasPointerEvent
): void {
switch (selectedItem.value) {
// Disconnect all links from this output
case "disconnect":
slot.disconnect()
break
// Disconnect all links from this output
case 'disconnect':
slot.disconnect()
break
// Remove the slot
case "remove":
if (slot !== this.emptySlot) {
this.removeSlot(slot)
}
break
// Remove the slot
case 'remove':
if (slot !== this.emptySlot) {
this.removeSlot(slot)
}
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
// 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<TSlot extends SubgraphInput | SubgraphO
size[1] = currentY - y + roundedRadius
}
draw(ctx: CanvasRenderingContext2D, colorContext: DefaultConnectionColors, fromSlot?: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput, editorAlpha?: number): void {
draw(
ctx: CanvasRenderingContext2D,
colorContext: DefaultConnectionColors,
fromSlot?:
| INodeInputSlot
| INodeOutputSlot
| SubgraphInput
| SubgraphOutput,
editorAlpha?: number
): void {
const { lineWidth, strokeStyle, fillStyle, font, textBaseline } = ctx
this.drawProtected(ctx, colorContext, fromSlot, editorAlpha)
Object.assign(ctx, { lineWidth, strokeStyle, fillStyle, font, textBaseline })
Object.assign(ctx, {
lineWidth,
strokeStyle,
fillStyle,
font,
textBaseline
})
}
/** @internal Leaves {@link ctx} dirty. */
protected abstract drawProtected(ctx: CanvasRenderingContext2D, colorContext: DefaultConnectionColors, fromSlot?: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput, editorAlpha?: number): void
protected abstract drawProtected(
ctx: CanvasRenderingContext2D,
colorContext: DefaultConnectionColors,
fromSlot?:
| INodeInputSlot
| INodeOutputSlot
| SubgraphInput
| SubgraphOutput,
editorAlpha?: number
): void
/** @internal Leaves {@link ctx} dirty. */
protected drawSlots(ctx: CanvasRenderingContext2D, colorContext: DefaultConnectionColors, fromSlot?: INodeInputSlot | INodeOutputSlot | SubgraphInput | SubgraphOutput, editorAlpha?: number): void {
ctx.fillStyle = "#AAA"
ctx.font = "12px Arial"
ctx.textBaseline = "middle"
protected drawSlots(
ctx: CanvasRenderingContext2D,
colorContext: DefaultConnectionColors,
fromSlot?:
| INodeInputSlot
| INodeOutputSlot
| SubgraphInput
| SubgraphOutput,
editorAlpha?: number
): void {
ctx.fillStyle = '#AAA'
ctx.font = '12px Arial'
ctx.textBaseline = 'middle'
for (const slot of this.allSlots) {
slot.draw({ ctx, colorContext, fromSlot, editorAlpha })
@@ -278,7 +340,7 @@ export abstract class SubgraphIONodeBase<TSlot extends SubgraphInput | SubgraphO
return {
id: this.id,
bounding: this.boundingRect.export(),
pinned: this.pinned ? true : undefined,
pinned: this.pinned ? true : undefined
}
}
}

View File

@@ -1,18 +1,22 @@
import type { SubgraphInputNode } from "./SubgraphInputNode"
import type { SubgraphOutput } from "./SubgraphOutput"
import type { SubgraphInputEventMap } from "@/lib/litegraph/src/infrastructure/SubgraphInputEventMap"
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 { IBaseWidget } from "@/lib/litegraph/src/types/widgets"
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget'
import type { SubgraphInputEventMap } from '@/lib/litegraph/src/infrastructure/SubgraphInputEventMap'
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 type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { CustomEventTarget } from "@/lib/litegraph/src/infrastructure/CustomEventTarget"
import { LiteGraph } from "@/lib/litegraph/src/litegraph"
import { LLink } from "@/lib/litegraph/src/LLink"
import { NodeSlotType } from "@/lib/litegraph/src/types/globalEnums"
import { SubgraphSlot } from "./SubgraphSlotBase"
import { isNodeSlot, isSubgraphOutput } from "./subgraphUtils"
import type { SubgraphInputNode } from './SubgraphInputNode'
import type { SubgraphOutput } from './SubgraphOutput'
import { SubgraphSlot } from './SubgraphSlotBase'
import { isNodeSlot, isSubgraphOutput } from './subgraphUtils'
/**
* An input "slot" from a parent graph into a subgraph.
@@ -41,12 +45,20 @@ export class SubgraphInput extends SubgraphSlot {
this.#widgetRef = widget ? new WeakRef(widget) : undefined
}
override connect(slot: INodeInputSlot, node: LGraphNode, afterRerouteId?: RerouteId): LLink | undefined {
override connect(
slot: INodeInputSlot,
node: LGraphNode,
afterRerouteId?: RerouteId
): LLink | undefined {
const { subgraph } = this.parent
// Allow nodes to block connection
const inputIndex = node.inputs.indexOf(slot)
if (node.onConnectInput?.(inputIndex, this.type, this, this.parent, -1) === false) return
if (
node.onConnectInput?.(inputIndex, this.type, this, this.parent, -1) ===
false
)
return
// if (slot instanceof SubgraphOutput) {
// // Subgraph IO nodes have no special handling at present.
@@ -71,12 +83,15 @@ export class SubgraphInput extends SubgraphSlot {
const inputWidget = node.getWidgetFromSlot(slot)
if (inputWidget) {
if (!this.matchesWidget(inputWidget)) {
console.warn("Target input has invalid widget.", slot, node)
console.warn('Target input has invalid widget.', slot, node)
return
}
this._widget ??= inputWidget
this.events.dispatch("input-connected", { input: slot, widget: inputWidget })
this.events.dispatch('input-connected', {
input: slot,
widget: inputWidget
})
}
const link = new LLink(
@@ -86,7 +101,7 @@ export class SubgraphInput extends SubgraphSlot {
this.parent.slots.indexOf(this),
node.id,
inputIndex,
afterRerouteId,
afterRerouteId
)
// Add to graph links list
@@ -116,13 +131,7 @@ export class SubgraphInput extends SubgraphSlot {
}
subgraph._version++
node.onConnectionsChange?.(
NodeSlotType.INPUT,
inputIndex,
true,
link,
slot,
)
node.onConnectionsChange?.(NodeSlotType.INPUT, inputIndex, true, link, slot)
subgraph.afterChange()
@@ -141,7 +150,7 @@ export class SubgraphInput extends SubgraphSlot {
for (const linkId of this.linkIds) {
const link = subgraph.getLink(linkId)
if (!link) {
console.error("Link not found", linkId)
console.error('Link not found', linkId)
continue
}
@@ -153,19 +162,21 @@ export class SubgraphInput extends SubgraphSlot {
// Invalid widget name
if (!widgetNamePojo.name) {
console.warn("Invalid widget name", widgetNamePojo)
console.warn('Invalid widget name', widgetNamePojo)
continue
}
const widget = resolved.inputNode.widgets.find(w => 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)) {

View File

@@ -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<SubgraphInput> implements Positionable {
export class SubgraphInputNode
extends SubgraphIONodeBase<SubgraphInput>
implements Positionable
{
readonly id: NodeId = SUBGRAPH_INPUT_ID
readonly emptySlot: EmptySubgraphInput = new EmptySubgraphInput(this)
@@ -35,11 +43,18 @@ export class SubgraphInputNode extends SubgraphIONodeBase<SubgraphInput> 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<SubgraphInput> 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<SubgraphInput> 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<SubgraphInput> impleme
outputIndex,
inputNode.id,
inputIndex,
afterRerouteId,
afterRerouteId
)
}
@@ -99,7 +124,7 @@ export class SubgraphInputNode extends SubgraphIONodeBase<SubgraphInput> 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<SubgraphInput> 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<SubgraphInput> 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<SubgraphInput> 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<SubgraphInput> 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<SubgraphInput> 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

View File

@@ -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<ISubgraphInput>) {
#addSubgraphInputListeners(
subgraphInput: SubgraphInput,
input: INodeInputSlot & Partial<ISubgraphInput>
) {
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<SubgraphInput>, input: INodeInputSlot, widget: Readonly<IBaseWidget>) {
#setWidget(
subgraphInput: Readonly<SubgraphInput>,
input: INodeInputSlot,
widget: Readonly<IBaseWidget>
) {
// 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<TInput extends Partial<ISubgraphInput>>(name: string, type: ISlotType, inputProperties: TInput = {} as TInput): INodeInputSlot & TInput {
override addInput<TInput extends Partial<ISubgraphInput>>(
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<SubgraphNode>(),
visited = new Set<SubgraphNode>()
): 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) {

View File

@@ -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)) {

View File

@@ -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<SubgraphOutput> implements Positionable {
export class SubgraphOutputNode
extends SubgraphIONodeBase<SubgraphOutput>
implements Positionable
{
readonly id: NodeId = SUBGRAPH_OUTPUT_ID
readonly emptySlot: EmptySubgraphOutput = new EmptySubgraphOutput(this)
@@ -35,11 +43,18 @@ export class SubgraphOutputNode extends SubgraphIONodeBase<SubgraphOutput> 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<SubgraphOutput> 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<SubgraphOutput> 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<SubgraphOutput> 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<SubgraphOutput> 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

View File

@@ -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<SubgraphIO> {
export abstract class SubgraphSlot
extends SlotBase
implements SubgraphIO, Hoverable, Serialisable<SubgraphIO>
{
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
}
}
}

View File

@@ -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<LGraphNode>
@@ -26,7 +36,9 @@ export interface FilteredItems {
unknown: Set<Positionable>
}
export function splitPositionables(items: Iterable<Positionable>): FilteredItems {
export function splitPositionables(
items: Iterable<Positionable>
): FilteredItems {
const nodes = new Set<LGraphNode>()
const reroutes = new Set<Reroute>()
const groups = new Set<LGraphGroup>()
@@ -37,24 +49,24 @@ export function splitPositionables(items: Iterable<Positionable>): 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<Positionable>): FilteredItems
groups,
subgraphInputNodes,
subgraphOutputNodes,
unknown,
unknown
}
}
@@ -76,7 +88,10 @@ interface BoundaryLinks {
boundaryOutputLinks: LLink[]
}
export function getBoundaryLinks(graph: LGraph, items: Set<Positionable>): BoundaryLinks {
export function getBoundaryLinks(
graph: LGraph,
items: Set<Positionable>
): BoundaryLinks {
const internalLinks: LLink[] = []
const boundaryLinks: LLink[] = []
const boundaryInputLinks: LLink[] = []
@@ -151,7 +166,9 @@ export function getBoundaryLinks(graph: LGraph, items: Set<Positionable>): 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<Positionable>): 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<Positionable>): 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<LGraphNode>): 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<LGraphNode>): ISerialisedNode[] {
* @returns A map of grouped connections.
*/
export function groupResolvedByOutput(
resolvedConnections: ResolvedConnection[],
resolvedConnections: ResolvedConnection[]
): Map<SubgraphIO | INodeOutputSlot | object, ResolvedConnection[]> {
const groupedByOutput: ReturnType<typeof groupResolvedByOutput> = 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<UUID> {
*/
export function findUsedSubgraphIds(
rootGraph: GraphOrSubgraph,
subgraphRegistry: Map<UUID, GraphOrSubgraph>,
subgraphRegistry: Map<UUID, GraphOrSubgraph>
): Set<UUID> {
const usedSubgraphIds = new Set<UUID>()
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)
)
}

View File

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

View File

@@ -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<CanvasPointerExtensions>,
LegacyMouseEvent {}
export interface CanvasMouseEvent
extends MouseEvent,
Readonly<CanvasPointerExtensions>,
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
}

View File

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

View File

@@ -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<INodeInputSlot, "boundingRect" | "widget"> & {
export type ISerialisableNodeInput = Omit<
INodeInputSlot,
'boundingRect' | 'widget'
> & {
widget?: { name: string }
}
export type ISerialisableNodeOutput = Omit<INodeOutputSlot, "boundingRect" | "_data"> & {
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<ISerialisedNode, "properties" | "showAdvanced">
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<INodeSlot, "boundingRect" | "nameLocked" | "locked" | "removable" | "_floatingLinks">
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 */

View File

@@ -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<TValues = unknown[]> {
on?: string
@@ -27,7 +27,7 @@ export interface IWidgetOptions<TValues = unknown[]> {
socketless?: boolean
values?: TValues
callback?: IWidget["callback"]
callback?: IWidget['callback']
}
export interface IWidgetSliderOptions extends IWidgetOptions<number[]> {
@@ -66,62 +66,75 @@ export type IWidget =
| IButtonWidget
| IKnobWidget
export interface IBooleanWidget extends IBaseWidget<boolean, "toggle"> {
type: "toggle"
export interface IBooleanWidget extends IBaseWidget<boolean, 'toggle'> {
type: 'toggle'
value: boolean
}
/** Any widget that uses a numeric backing */
export interface INumericWidget extends IBaseWidget<number, "number"> {
type: "number"
export interface INumericWidget extends IBaseWidget<number, 'number'> {
type: 'number'
value: number
}
export interface ISliderWidget extends IBaseWidget<number, "slider", IWidgetSliderOptions> {
type: "slider"
export interface ISliderWidget
extends IBaseWidget<number, 'slider', IWidgetSliderOptions> {
type: 'slider'
value: number
marker?: number
}
export interface IKnobWidget extends IBaseWidget<number, "knob", IWidgetKnobOptions> {
type: "knob"
export interface IKnobWidget
extends IBaseWidget<number, 'knob', IWidgetKnobOptions> {
type: 'knob'
value: number
options: IWidgetKnobOptions
}
/** Avoids the type issues with the legacy IComboWidget type */
export interface IStringComboWidget extends IBaseWidget<string, "combo", RequiredProps<IWidgetOptions<string[]>, "values">> {
type: "combo"
export interface IStringComboWidget
extends IBaseWidget<
string,
'combo',
RequiredProps<IWidgetOptions<string[]>, 'values'>
> {
type: 'combo'
value: string
}
type ComboWidgetValues = string[] | Record<string, string> | ((widget?: IComboWidget, node?: LGraphNode) => string[])
type ComboWidgetValues =
| string[]
| Record<string, string>
| ((widget?: IComboWidget, node?: LGraphNode) => string[])
/** A combo-box widget (dropdown, select, etc) */
export interface IComboWidget extends IBaseWidget<
string | number,
"combo",
RequiredProps<IWidgetOptions<ComboWidgetValues>, "values">
> {
type: "combo"
export interface IComboWidget
extends IBaseWidget<
string | number,
'combo',
RequiredProps<IWidgetOptions<ComboWidgetValues>, 'values'>
> {
type: 'combo'
value: string | number
}
/** A widget with a string value */
export interface IStringWidget extends IBaseWidget<string, "string" | "text", IWidgetOptions<string[]>> {
type: "string" | "text"
export interface IStringWidget
extends IBaseWidget<string, 'string' | 'text', IWidgetOptions<string[]>> {
type: 'string' | 'text'
value: string
}
export interface IButtonWidget extends IBaseWidget<string | undefined, "button"> {
type: "button"
export interface IButtonWidget
extends IBaseWidget<string | undefined, 'button'> {
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<string | object, "custom"> {
type: "custom"
export interface ICustomWidget extends IBaseWidget<string | object, 'custom'> {
type: 'custom'
value: string | object
}
@@ -130,8 +143,8 @@ export interface ICustomWidget extends IBaseWidget<string | object, "custom"> {
* 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<unknown> = IWidgetOptions<unknown>,
TOptions extends IWidgetOptions<unknown> = IWidgetOptions<unknown>
> {
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
}

View File

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

View File

@@ -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<Positionable>): Set<Positionable> {
export function getAllNestedItems(
items: ReadonlySet<Positionable>
): Set<Positionable> {
const allItems = new Set<Positionable>()
if (items) {
for (const item of items) addRecursively(item, allItems)
}
return allItems
function addRecursively(item: Positionable, flatSet: Set<Positionable>): void {
function addRecursively(
item: Positionable,
flatSet: Set<Positionable>
): void {
if (flatSet.has(item) || item.pinned) return
flatSet.add(item)
if (item.children) {
@@ -32,14 +37,19 @@ export function getAllNestedItems(items: ReadonlySet<Positionable>): Set<Positio
* @param items The items to search through
* @returns The first node found in {@link items}, otherwise `undefined`
*/
export function findFirstNode(items: Iterable<Positionable>): LGraphNode | undefined {
export function findFirstNode(
items: Iterable<Positionable>
): 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<T extends { type: ISlotType }> = { index: number, slot: T } | undefined
type FreeSlotResult<T extends { type: ISlotType }> =
| { 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<T extends { type: ISlotType }> = { index: number, slot: T }
export function findFreeSlotOfType<T extends { type: ISlotType }>(
slots: T[],
type: ISlotType,
hasNoLinks: (slot: T) => boolean,
hasNoLinks: (slot: T) => boolean
) {
if (!slots?.length) return
@@ -87,7 +99,7 @@ export function findFreeSlotOfType<T extends { type: ISlotType }>(
}
// 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 }

View File

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

View File

@@ -1,5 +1,8 @@
export function omitBy<T extends object>(obj: T, predicate: (value: any) => boolean): Partial<T> {
export function omitBy<T extends object>(
obj: T,
predicate: (value: any) => boolean
): Partial<T> {
return Object.fromEntries(
Object.entries(obj).filter(([_key, value]) => !predicate(value)),
Object.entries(obj).filter(([_key, value]) => !predicate(value))
) as Partial<T>
}

View File

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

View File

@@ -10,7 +10,7 @@ export function truncateText(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
ellipsis: string = "...",
ellipsis: string = '...'
): string {
const textWidth = ctx.measureText(text).width

View File

@@ -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<P, C extends P, Args extends unknown[]>(
* 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
)
}

View File

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

View File

@@ -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<unknown>): number {
return options.step2 || ((options.step || 10) * 0.1)
return options.step2 || (options.step || 10) * 0.1
}

View File

@@ -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<TWidget extends IBaseWidget = IBaseWidget> extends BaseWidget<TWidget> {
export abstract class BaseSteppedWidget<
TWidget extends IBaseWidget = IBaseWidget
> extends BaseWidget<TWidget> {
/**
* 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<TWidget extends IBaseWidget = IBaseWidge
ctx.fill()
}
override drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions) {
override drawWidget(
ctx: CanvasRenderingContext2D,
options: DrawWidgetOptions
) {
// Store original context attributes
const { fillStyle, strokeStyle, textAlign } = ctx

View File

@@ -1,11 +1,15 @@
import type { Point } from "@/lib/litegraph/src/interfaces"
import type { CanvasPointer, LGraphCanvas, LGraphNode, Size } from "@/lib/litegraph/src/litegraph"
import type { CanvasPointerEvent } from "@/lib/litegraph/src/types/events"
import type { IBaseWidget } from "@/lib/litegraph/src/types/widgets"
import { drawTextInArea } from "@/lib/litegraph/src/draw"
import { Rectangle } from "@/lib/litegraph/src/infrastructure/Rectangle"
import { LiteGraph } from "@/lib/litegraph/src/litegraph"
import { drawTextInArea } from '@/lib/litegraph/src/draw'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { Point } from '@/lib/litegraph/src/interfaces'
import type {
CanvasPointer,
LGraphCanvas,
LGraphNode,
Size
} from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
export interface DrawWidgetOptions {
/** The width of the node where this widget will be displayed. */
@@ -29,7 +33,9 @@ export interface WidgetEventOptions {
canvas: LGraphCanvas
}
export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget> implements IBaseWidget {
export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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<TWidget extends IBaseWidget = IBaseWidget> 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++
}

View File

@@ -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<IBooleanWidget> implements IBooleanWidget {
override type = "toggle" as const
export class BooleanWidget
extends BaseWidget<IBooleanWidget>
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<IBooleanWidget> 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)
}

View File

@@ -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<IButtonWidget> implements IButtonWidget {
override type = "button" as const
export class ButtonWidget
extends BaseWidget<IButtonWidget>
implements IButtonWidget
{
override type = 'button' as const
clicked: boolean
constructor(widget: IButtonWidget, node: LGraphNode) {
@@ -17,10 +24,10 @@ export class ButtonWidget extends BaseWidget<IButtonWidget> 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<IButtonWidget> 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<IButtonWidget> 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)
}

View File

@@ -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<string, string> | ((widget?: ComboWidget, node?: LGraphNode) => string[])
type Values =
| string[]
| Record<string, string>
| ((widget?: ComboWidget, node?: LGraphNode) => string[])
function toArray(values: Values): string[] {
return Array.isArray(values) ? values : Object.keys(values)
}
export class ComboWidget extends BaseSteppedWidget<IStringComboWidget | IComboWidget> implements IComboWidget {
override type = "combo" as const
export class ComboWidget
extends BaseSteppedWidget<IStringComboWidget | IComboWidget>
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<IStringComboWidget | IComboWi
#canUseButton(increment: boolean): boolean {
const { values } = this.options
// If using legacy duck-typed method, false is the most permissive return value
if (typeof values === "function") return false
if (typeof values === 'function') return false
const valuesArray = toArray(values)
if (!(valuesArray.length > 1)) return false
@@ -92,16 +98,15 @@ export class ComboWidget extends BaseSteppedWidget<IStringComboWidget | IComboWi
// avoids double click event
options.canvas.last_mouseclick = 0
const foundIndex = typeof values === "object"
? indexedValues.indexOf(String(this.value)) + delta
// @ts-expect-error handle non-string values
: indexedValues.indexOf(this.value) + delta
const foundIndex =
typeof values === 'object'
? indexedValues.indexOf(String(this.value)) + delta
: // @ts-expect-error handle non-string values
indexedValues.indexOf(this.value) + delta
const index = clamp(foundIndex, 0, indexedValues.length - 1)
const value = Array.isArray(values)
? values[index]
: index
const value = Array.isArray(values) ? values[index] : index
this.setValue(value, options)
}
@@ -110,8 +115,10 @@ export class ComboWidget extends BaseSteppedWidget<IStringComboWidget | IComboWi
const width = this.width || node.size[0]
// Deprecated functionality (warning as of v0.14.5)
if (typeof this.options.values === "function") {
warnDeprecated("Using a function for values is deprecated. Use an array of unique values instead.")
if (typeof this.options.values === 'function') {
warnDeprecated(
'Using a function for values is deprecated. Use an array of unique values instead.'
)
}
// Determine if clicked on left/right arrows
@@ -127,15 +134,13 @@ export class ComboWidget extends BaseSteppedWidget<IStringComboWidget | IComboWi
new LiteGraph.ContextMenu(text_values, {
scale: Math.max(1, canvas.ds.scale),
event: e,
className: "dark",
className: 'dark',
callback: (value: string) => {
this.setValue(
values != values_list
? text_values.indexOf(value)
: value,
{ e, node, canvas },
values != values_list ? text_values.indexOf(value) : value,
{ e, node, canvas }
)
},
}
})
}
}

View File

@@ -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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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<IKnobWidget> 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)

View File

@@ -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<TWidget extends IBaseWidget = IBaseWidget> extends BaseWidget<TWidget> implements IBaseWidget {
export class LegacyWidget<TWidget extends IBaseWidget = IBaseWidget>
extends BaseWidget<TWidget>
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.'
)
}
}

View File

@@ -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<INumericWidget> implements INumericWidget {
override type = "number" as const
export class NumberWidget
extends BaseSteppedWidget<INumericWidget>
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<INumericWidget> 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<INumericWidget> 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
})
}
}

View File

@@ -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<ISliderWidget> implements ISliderWidget {
override type = "slider" as const
export class SliderWidget
extends BaseWidget<ISliderWidget>
implements ISliderWidget
{
override type = 'slider' as const
marker?: number
@@ -14,10 +20,10 @@ export class SliderWidget extends BaseWidget<ISliderWidget> 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<ISliderWidget> 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<ISliderWidget> 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<ISliderWidget> 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<ISliderWidget> 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)

View File

@@ -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<IStringWidget> implements IStringWidget {
export class TextWidget
extends BaseWidget<IStringWidget>
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<IStringWidget> 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<IStringWidget> 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<IStringWidget> implements IStringWidg
}
},
e,
this.options?.multiline ?? false,
this.options?.multiline ?? false
)
}
}

View File

@@ -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<TWidget extends IWidget | IBaseWidget>(
widget: TWidget,
node: LGraphNode,
wrapLegacyWidgets?: true,
): WidgetTypeMap[TWidget["type"]]
wrapLegacyWidgets?: true
): WidgetTypeMap[TWidget['type']]
export function toConcreteWidget<TWidget extends IWidget | IBaseWidget>(
widget: TWidget,
node: LGraphNode,
wrapLegacyWidgets: false): WidgetTypeMap[TWidget["type"]] | undefined
wrapLegacyWidgets: false
): WidgetTypeMap[TWidget['type']] | undefined
export function toConcreteWidget<TWidget extends IWidget | IBaseWidget>(
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<TWidget extends IWidget | IBaseWidget>(
const narrowedWidget = widget as RemoveBaseWidgetType<TWidget>
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<TWidget extends IWidget | IBaseWidget>(
/** 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>): 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>
): 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<LGraphNode, "boundingRect"> & { 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<LGraphNode, "boundingRect"> & { 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<LGraphNode, "boundingRect"> & { 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<LGraphNode, "boundingRect"> & { 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()

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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<typeof vi.fn>
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<TestContext>({
@@ -34,13 +44,17 @@ const test = baseTest.extend<TestContext>({
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<typeof vi.fn>) => Promise<void>) => {
setConnectingLinks: async (
{},
use: (mock: ReturnType<typeof vi.fn>) => Promise<void>
) => {
const mock = vi.fn()
await use(mock)
},
@@ -51,32 +65,34 @@ const test = baseTest.extend<TestContext>({
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)

Some files were not shown because too many files have changed in this diff Show More