mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
npm run format
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}. */
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
>
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
export class RecursionError extends Error {
|
||||
constructor(subject: string) {
|
||||
super(subject)
|
||||
this.name = "RecursionError"
|
||||
this.name = 'RecursionError'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export function truncateText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
maxWidth: number,
|
||||
ellipsis: string = "...",
|
||||
ellipsis: string = '...'
|
||||
): string {
|
||||
const textWidth = ctx.measureText(text).width
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user