Revert branch to cb6e80a645 (#257)

This commit is contained in:
Chenlei Hu
2024-11-03 09:12:47 -05:00
committed by GitHub
parent 9b0f572ca1
commit 7c0240857c
27 changed files with 14195 additions and 13800 deletions

View File

@@ -1,7 +1,5 @@
{
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"trailingComma": "none",
"printWidth": 80
}
"singleQuote": false,
"semi": true,
"tabWidth": 2
}

View File

@@ -23,7 +23,6 @@
"build": "tsc && vite build",
"dev": "vite",
"preview": "vite preview",
"watch": "vite build --watch",
"release": "node scripts/release.js",
"test": "jest",
"deprecated-test:allVersions": "./utils/test.sh",

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
import type { IContextMenuOptions, IContextMenuValue } from './interfaces'
import { LiteGraph } from './litegraph'
import type { IContextMenuOptions, IContextMenuValue } from "./interfaces"
import { LiteGraph } from "./litegraph"
interface ContextMenuDivElement extends HTMLDivElement {
value?: IContextMenuValue | string
onclick_callback?: never
closing_timer?: number
value?: IContextMenuValue | string
onclick_callback?: never
closing_timer?: number
}
export interface ContextMenu {
constructor: new (...args: ConstructorParameters<typeof ContextMenu>) => ContextMenu
constructor: new (...args: ConstructorParameters<typeof ContextMenu>) => ContextMenu
}
/**
@@ -24,352 +24,354 @@ export interface ContextMenu {
* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position
*/
export class ContextMenu {
options?: IContextMenuOptions
parentMenu?: ContextMenu
root: ContextMenuDivElement
current_submenu?: ContextMenu
lock?: boolean
options?: IContextMenuOptions
parentMenu?: ContextMenu
root: ContextMenuDivElement
current_submenu?: ContextMenu
lock?: boolean
// TODO: Interface for values requires functionality change - currently accepts an array of strings, functions, objects, nulls, or undefined.
constructor(values: (IContextMenuValue | string)[], options: IContextMenuOptions) {
options ||= {}
this.options = options
// TODO: Interface for values requires functionality change - currently accepts an array of strings, functions, objects, nulls, or undefined.
constructor(values: (IContextMenuValue | string)[], options: IContextMenuOptions) {
options ||= {}
this.options = options
//to link a menu with its parent
const parent = options.parentMenu
if (parent) {
if (!(parent instanceof ContextMenu)) {
console.error('parentMenu must be of class ContextMenu, ignoring it')
options.parentMenu = null
} else {
this.parentMenu = parent
this.parentMenu.lock = true
this.parentMenu.current_submenu = this
}
if (parent.options?.className === 'dark') {
options.className = 'dark'
}
//to link a menu with its parent
const parent = options.parentMenu
if (parent) {
if (!(parent instanceof ContextMenu)) {
console.error("parentMenu must be of class ContextMenu, ignoring it")
options.parentMenu = null
} else {
this.parentMenu = parent
this.parentMenu.lock = true
this.parentMenu.current_submenu = this
}
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
if (eventClass !== "MouseEvent" &&
eventClass !== "CustomEvent" &&
eventClass !== "PointerEvent") {
console.error(`Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. (${eventClass})`)
options.event = null
}
const root: ContextMenuDivElement = document.createElement("div")
let classes = "litegraph litecontextmenu litemenubar-panel"
if (options.className) classes += " " + options.className
root.className = classes
root.style.minWidth = "100"
root.style.minHeight = "100"
// TODO: Fix use of timer in place of events
root.style.pointerEvents = "none"
setTimeout(function () {
root.style.pointerEvents = "auto"
}, 100) //delay so the mouse up event is not caught by this element
//this prevents the default context browser menu to open in case this menu was created when pressing right button
LiteGraph.pointerListenerAdd(root, "up",
function (e: MouseEvent) {
//console.log("pointerevents: ContextMenu up root prevent");
e.preventDefault()
return true
},
true
)
root.addEventListener(
"contextmenu",
function (e: MouseEvent) {
//right button
if (e.button != 2) return false
e.preventDefault()
return false
},
true
)
LiteGraph.pointerListenerAdd(root, "down",
(e: MouseEvent) => {
//console.log("pointerevents: ContextMenu down");
if (e.button == 2) {
this.close()
e.preventDefault()
return true
}
},
true
)
function on_mouse_wheel(e: WheelEvent) {
const pos = parseInt(root.style.top)
root.style.top =
(pos + e.deltaY * options.scroll_speed).toFixed() + "px"
e.preventDefault()
return true
}
if (!options.scroll_speed) {
options.scroll_speed = 0.1
}
root.addEventListener("wheel", on_mouse_wheel, true)
this.root = root
//title
if (options.title) {
const element = document.createElement("div")
element.className = "litemenu-title"
element.innerHTML = options.title
root.appendChild(element)
}
//entries
for (let i = 0; i < values.length; i++) {
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 as null | undefined
}
this.addItem(name, value, options)
}
LiteGraph.pointerListenerAdd(root, "enter", function () {
if (root.closing_timer) {
clearTimeout(root.closing_timer)
}
})
//insert before checking position
const ownerDocument = (options.event?.target as Node).ownerDocument
const root_document = ownerDocument || document
if (root_document.fullscreenElement)
root_document.fullscreenElement.appendChild(root)
else
root_document.body.appendChild(root)
//compute best position
let left = options.left || 0
let top = options.top || 0
if (options.event) {
left = options.event.clientX - 10
top = options.event.clientY - 10
if (options.title) top -= 20
if (parent) {
const rect = parent.root.getBoundingClientRect()
left = rect.left + rect.width
}
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%; }")
if (body_rect.width && left > body_rect.width - root_rect.width - 10)
left = body_rect.width - root_rect.width - 10
if (body_rect.height && top > body_rect.height - root_rect.height - 10)
top = body_rect.height - root_rect.height - 10
}
root.style.left = left + "px"
root.style.top = top + "px"
if (options.scale)
root.style.transform = `scale(${options.scale})`
}
//use strings because comparing classes between windows doesnt work
const eventClass = options.event ? options.event.constructor.name : null
if (
eventClass !== 'MouseEvent' &&
eventClass !== 'CustomEvent' &&
eventClass !== 'PointerEvent'
) {
console.error(
`Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. (${eventClass})`,
)
options.event = null
addItem(name: string, value: IContextMenuValue | string, options: IContextMenuOptions): HTMLElement {
options ||= {}
const element: ContextMenuDivElement = document.createElement("div")
element.className = "litemenu-entry submenu"
let disabled = false
if (value === null) {
element.classList.add("separator")
} else {
if (typeof value === "string") {
element.innerHTML = name
} else {
element.innerHTML = value?.title ?? name
if (value.disabled) {
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")
}
if (value.className)
element.className += " " + value.className
}
element.value = value
element.setAttribute("role", "menuitem")
if (typeof value === "function") {
element.dataset["value"] = name
element.onclick_callback = value
} else {
element.dataset["value"] = String(value)
}
}
this.root.appendChild(element)
if (!disabled) element.addEventListener("click", inner_onclick)
if (!disabled && options.autoopen)
LiteGraph.pointerListenerAdd(element, "enter", inner_over)
const setAriaExpanded = () => {
const entries = this.root.querySelectorAll("div.litemenu-entry.has_submenu")
if (entries) {
for (let i = 0; i < entries.length; i++) {
entries[i].setAttribute("aria-expanded", "false")
}
}
element.setAttribute("aria-expanded", "true")
}
function inner_over(this: ContextMenuDivElement, e: MouseEvent) {
const value = this.value
if (!value || !(value as IContextMenuValue).has_submenu) return
//if it is a submenu, autoopen like the item was clicked
inner_onclick.call(this, e)
setAriaExpanded()
}
//menu option clicked
const that = this
function inner_onclick(this: ContextMenuDivElement, e: MouseEvent) {
const value = this.value
let close_parent = true
that.current_submenu?.close(e)
if ((value as IContextMenuValue)?.has_submenu || (value as IContextMenuValue)?.submenu) setAriaExpanded()
//global callback
if (options.callback) {
const r = options.callback.call(
this,
value,
options,
e,
that,
options.node
)
if (r === true) close_parent = false
}
//special cases
if (typeof value === "object") {
if (value.callback &&
!options.ignore_item_callbacks &&
value.disabled !== true) {
//item callback
const r = value.callback.call(
this,
value,
options,
e,
that,
options.extra
)
if (r === true) close_parent = false
}
if (value.submenu) {
if (!value.submenu.options)
throw "ContextMenu submenu needs options"
new that.constructor(value.submenu.options, {
callback: value.submenu.callback,
event: e,
parentMenu: that,
ignore_item_callbacks: value.submenu.ignore_item_callbacks,
title: value.submenu.title,
extra: value.submenu.extra,
autoopen: options.autoopen
})
close_parent = false
}
}
if (close_parent && !that.lock)
that.close()
}
return element
}
const root: ContextMenuDivElement = document.createElement('div')
let classes = 'litegraph litecontextmenu litemenubar-panel'
if (options.className) classes += ' ' + options.className
root.className = classes
root.style.minWidth = '100'
root.style.minHeight = '100'
// TODO: Fix use of timer in place of events
root.style.pointerEvents = 'none'
setTimeout(function () {
root.style.pointerEvents = 'auto'
}, 100) //delay so the mouse up event is not caught by this element
close(e?: MouseEvent, ignore_parent_menu?: boolean): void {
this.root.parentNode?.removeChild(this.root)
if (this.parentMenu && !ignore_parent_menu) {
this.parentMenu.lock = false
this.parentMenu.current_submenu = null
if (e === undefined) {
this.parentMenu.close()
} else if (e &&
!ContextMenu.isCursorOverElement(e, this.parentMenu.root)) {
ContextMenu.trigger(this.parentMenu.root, LiteGraph.pointerevents_method + "leave", e)
}
}
this.current_submenu?.close(e, true)
//this prevents the default context browser menu to open in case this menu was created when pressing right button
LiteGraph.pointerListenerAdd(
root,
'up',
function (e: MouseEvent) {
//console.log("pointerevents: ContextMenu up root prevent");
e.preventDefault()
return true
},
true,
)
root.addEventListener(
'contextmenu',
function (e: MouseEvent) {
//right button
if (e.button != 2) return false
e.preventDefault()
if (this.root.closing_timer)
clearTimeout(this.root.closing_timer)
}
//this code is used to trigger events easily (used in the context menu mouseleave
static trigger(element: HTMLDivElement, event_name: string, params: MouseEvent, origin?: unknown): CustomEvent {
const evt = document.createEvent("CustomEvent")
evt.initCustomEvent(event_name, true, true, params) //canBubble, cancelable, detail
// @ts-expect-error
evt.srcElement = origin
if (element.dispatchEvent) element.dispatchEvent(evt)
// @ts-expect-error
else if (element.__events) element.__events.dispatchEvent(evt)
//else nothing seems binded here so nothing to do
return evt
}
//returns the top most menu
getTopMenu(): ContextMenu {
return this.options.parentMenu
? this.options.parentMenu.getTopMenu()
: this
}
getFirstEvent(): MouseEvent {
return this.options.parentMenu
? this.options.parentMenu.getFirstEvent()
: this.options.event
}
static isCursorOverElement(event: MouseEvent, element: HTMLDivElement): boolean {
const left = event.clientX
const top = event.clientY
const rect = element.getBoundingClientRect()
if (!rect) return false
if (top > rect.top &&
top < rect.top + rect.height &&
left > rect.left &&
left < rect.left + rect.width) {
return true
}
return false
},
true
)
LiteGraph.pointerListenerAdd(
root,
'down',
(e: MouseEvent) => {
//console.log("pointerevents: ContextMenu down");
if (e.button == 2) {
this.close()
e.preventDefault()
return true
}
},
true
)
function on_mouse_wheel(e: WheelEvent) {
const pos = parseInt(root.style.top)
root.style.top = (pos + e.deltaY * options.scroll_speed).toFixed() + 'px'
e.preventDefault()
return true
}
if (!options.scroll_speed) {
options.scroll_speed = 0.1
}
root.addEventListener('wheel', on_mouse_wheel, true)
this.root = root
//title
if (options.title) {
const element = document.createElement('div')
element.className = 'litemenu-title'
element.innerHTML = options.title
root.appendChild(element)
}
//entries
for (let i = 0; i < values.length; i++) {
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 as null | undefined)
}
this.addItem(name, value, options)
}
LiteGraph.pointerListenerAdd(root, 'enter', function () {
if (root.closing_timer) {
clearTimeout(root.closing_timer)
}
})
//insert before checking position
const ownerDocument = (options.event?.target as Node).ownerDocument
const root_document = ownerDocument || document
if (root_document.fullscreenElement) {
root_document.fullscreenElement.appendChild(root)
} else {
root_document.body.appendChild(root)
}
//compute best position
let left = options.left || 0
let top = options.top || 0
if (options.event) {
left = options.event.clientX - 10
top = options.event.clientY - 10
if (options.title) top -= 20
if (parent) {
const rect = parent.root.getBoundingClientRect()
left = rect.left + rect.width
}
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%; }')
if (body_rect.width && left > body_rect.width - root_rect.width - 10) {
left = body_rect.width - root_rect.width - 10
}
if (body_rect.height && top > body_rect.height - root_rect.height - 10) {
top = body_rect.height - root_rect.height - 10
}
}
root.style.left = left + 'px'
root.style.top = top + 'px'
if (options.scale) {
root.style.transform = `scale(${options.scale})`
}
}
addItem(
name: string,
value: IContextMenuValue | string,
options: IContextMenuOptions,
): HTMLElement {
options ||= {}
const element: ContextMenuDivElement = document.createElement('div')
element.className = 'litemenu-entry submenu'
let disabled = false
if (value === null) {
element.classList.add('separator')
} else {
if (typeof value === 'string') {
element.innerHTML = name
} else {
element.innerHTML = value?.title ?? name
if (value.disabled) {
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')
}
if (value.className) element.className += ' ' + value.className
}
element.value = value
element.setAttribute('role', 'menuitem')
if (typeof value === 'function') {
element.dataset['value'] = name
element.onclick_callback = value
} else {
element.dataset['value'] = String(value)
}
}
this.root.appendChild(element)
if (!disabled) element.addEventListener('click', inner_onclick)
if (!disabled && options.autoopen) LiteGraph.pointerListenerAdd(element, 'enter', inner_over)
const setAriaExpanded = () => {
const entries = this.root.querySelectorAll('div.litemenu-entry.has_submenu')
if (entries) {
for (let i = 0; i < entries.length; i++) {
entries[i].setAttribute('aria-expanded', 'false')
}
}
element.setAttribute('aria-expanded', 'true')
}
function inner_over(this: ContextMenuDivElement, e: MouseEvent) {
const value = this.value
if (!value || !(value as IContextMenuValue).has_submenu) return
//if it is a submenu, autoopen like the item was clicked
inner_onclick.call(this, e)
setAriaExpanded()
}
//menu option clicked
const that = this
function inner_onclick(this: ContextMenuDivElement, e: MouseEvent) {
const value = this.value
let close_parent = true
that.current_submenu?.close(e)
if ((value as IContextMenuValue)?.has_submenu || (value as IContextMenuValue)?.submenu)
setAriaExpanded()
//global callback
if (options.callback) {
const r = options.callback.call(this, value, options, e, that, options.node)
if (r === true) close_parent = false
}
//special cases
if (typeof value === 'object') {
if (value.callback && !options.ignore_item_callbacks && value.disabled !== true) {
//item callback
const r = value.callback.call(this, value, options, e, that, options.extra)
if (r === true) close_parent = false
}
if (value.submenu) {
if (!value.submenu.options) throw 'ContextMenu submenu needs options'
new that.constructor(value.submenu.options, {
callback: value.submenu.callback,
event: e,
parentMenu: that,
ignore_item_callbacks: value.submenu.ignore_item_callbacks,
title: value.submenu.title,
extra: value.submenu.extra,
autoopen: options.autoopen,
})
close_parent = false
}
}
if (close_parent && !that.lock) that.close()
}
return element
}
close(e?: MouseEvent, ignore_parent_menu?: boolean): void {
this.root.parentNode?.removeChild(this.root)
if (this.parentMenu && !ignore_parent_menu) {
this.parentMenu.lock = false
this.parentMenu.current_submenu = null
if (e === undefined) {
this.parentMenu.close()
} else if (e && !ContextMenu.isCursorOverElement(e, this.parentMenu.root)) {
ContextMenu.trigger(this.parentMenu.root, LiteGraph.pointerevents_method + 'leave', e)
}
}
this.current_submenu?.close(e, true)
if (this.root.closing_timer) {
clearTimeout(this.root.closing_timer)
}
}
//this code is used to trigger events easily (used in the context menu mouseleave
static trigger(
element: HTMLDivElement,
event_name: string,
params: MouseEvent,
origin?: unknown,
): CustomEvent {
const evt = document.createEvent('CustomEvent')
evt.initCustomEvent(event_name, true, true, params) //canBubble, cancelable, detail
// @ts-expect-error
evt.srcElement = origin
if (element.dispatchEvent) element.dispatchEvent(evt)
// @ts-expect-error
else if (element.__events) element.__events.dispatchEvent(evt)
//else nothing seems binded here so nothing to do
return evt
}
//returns the top most menu
getTopMenu(): ContextMenu {
return this.options.parentMenu ? this.options.parentMenu.getTopMenu() : this
}
getFirstEvent(): MouseEvent {
return this.options.parentMenu ? this.options.parentMenu.getFirstEvent() : this.options.event
}
static isCursorOverElement(event: MouseEvent, element: HTMLDivElement): boolean {
const left = event.clientX
const top = event.clientY
const rect = element.getBoundingClientRect()
if (!rect) return false
if (
top > rect.top &&
top < rect.top + rect.height &&
left > rect.left &&
left < rect.left + rect.width
) {
return true
}
return false
}
}

View File

@@ -1,178 +1,173 @@
import type { Point, Rect } from './interfaces'
import { clamp, LGraphCanvas } from './litegraph'
import { distance } from './measure'
import type { Point, Rect } from "./interfaces"
import { clamp, LGraphCanvas } from "./litegraph"
import { distance } from "./measure"
//used by some widgets to render a curve editor
export class CurveEditor {
points: Point[]
selected: number
nearest: number
size: Rect
must_update: boolean
margin: number
_nearest: number
points: Point[]
selected: number
nearest: number
size: Rect
must_update: boolean
margin: number
_nearest: number
constructor(points: Point[]) {
this.points = points
this.selected = -1
this.nearest = -1
this.size = null //stores last size used
this.must_update = true
this.margin = 5
}
static sampleCurve(f: number, points: Point[]): number {
if (!points) return
for (let i = 0; i < points.length - 1; ++i) {
const p = points[i]
const pn = points[i + 1]
if (pn[0] < f) continue
const r = pn[0] - p[0]
if (Math.abs(r) < 0.00001) return p[1]
const local_f = (f - p[0]) / r
return p[1] * (1.0 - local_f) + pn[1] * local_f
}
return 0
}
draw(
ctx: CanvasRenderingContext2D,
size: Rect,
graphcanvas?: LGraphCanvas,
background_color?: string,
line_color?: string,
inactive = false,
): void {
const points = this.points
if (!points) return
this.size = size
const w = size[0] - this.margin * 2
const h = size[1] - this.margin * 2
line_color = line_color || '#666'
ctx.save()
ctx.translate(this.margin, this.margin)
if (background_color) {
ctx.fillStyle = '#111'
ctx.fillRect(0, 0, w, h)
ctx.fillStyle = '#222'
ctx.fillRect(w * 0.5, 0, 1, h)
ctx.strokeStyle = '#333'
ctx.strokeRect(0, 0, w, h)
}
ctx.strokeStyle = line_color
if (inactive) ctx.globalAlpha = 0.5
ctx.beginPath()
for (let i = 0; i < points.length; ++i) {
const p = points[i]
ctx.lineTo(p[0] * w, (1.0 - p[1]) * h)
}
ctx.stroke()
ctx.globalAlpha = 1
if (!inactive)
for (let i = 0; i < points.length; ++i) {
const p = points[i]
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()
}
ctx.restore()
}
//localpos is mouse in curve editor space
onMouseDown(localpos: Point, graphcanvas: LGraphCanvas): boolean {
const points = this.points
if (!points) return
if (localpos[1] < 0) return
//this.captureInput(true);
const w = this.size[0] - this.margin * 2
const h = this.size[1] - this.margin * 2
const x = localpos[0] - this.margin
const y = localpos[1] - this.margin
const pos: Point = [x, y]
const max_dist = 30 / graphcanvas.ds.scale
//search closer one
this.selected = this.getCloserPoint(pos, max_dist)
//create one
if (this.selected == -1) {
const point: Point = [x / w, 1 - y / h]
points.push(point)
points.sort(function (a, b) {
return a[0] - b[0]
})
this.selected = points.indexOf(point)
this.must_update = true
}
if (this.selected != -1) return true
}
onMouseMove(localpos: Point, graphcanvas: LGraphCanvas): void {
const points = this.points
if (!points) return
const s = this.selected
if (s < 0) return
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]
const max_dist = 30 / graphcanvas.ds.scale
this._nearest = this.getCloserPoint(curvepos, max_dist)
const point = points[s]
if (point) {
const is_edge_point = s == 0 || s == points.length - 1
if (
!is_edge_point &&
(localpos[0] < -10 ||
localpos[0] > this.size[0] + 10 ||
localpos[1] < -10 ||
localpos[1] > this.size[1] + 10)
) {
points.splice(s, 1)
constructor(points: Point[]) {
this.points = points
this.selected = -1
return
}
if (!is_edge_point)
//not edges
point[0] = clamp(x, 0, 1)
else point[0] = s == 0 ? 0 : 1
point[1] = 1.0 - clamp(y, 0, 1)
points.sort(function (a, b) {
return a[0] - b[0]
})
this.selected = points.indexOf(point)
this.must_update = true
this.nearest = -1
this.size = null //stores last size used
this.must_update = true
this.margin = 5
}
}
// Former params: localpos, graphcanvas
onMouseUp(): boolean {
this.selected = -1
return false
}
getCloserPoint(pos: Point, max_dist: number): number {
const points = this.points
if (!points) return -1
max_dist = max_dist || 30
const w = this.size[0] - this.margin * 2
const h = this.size[1] - this.margin * 2
const num = points.length
const p2: Point = [0, 0]
let min_dist = 1000000
let closest = -1
for (let i = 0; i < num; ++i) {
const p = points[i]
p2[0] = p[0] * w
p2[1] = (1.0 - p[1]) * h
const dist = distance(pos, p2)
if (dist > min_dist || dist > max_dist) continue
closest = i
min_dist = dist
static sampleCurve(f: number, points: Point[]): number {
if (!points)
return
for (let i = 0; i < points.length - 1; ++i) {
const p = points[i]
const pn = points[i + 1]
if (pn[0] < f)
continue
const r = (pn[0] - p[0])
if (Math.abs(r) < 0.00001)
return p[1]
const local_f = (f - p[0]) / r
return p[1] * (1.0 - local_f) + pn[1] * local_f
}
return 0
}
draw(ctx: CanvasRenderingContext2D, size: Rect, graphcanvas?: LGraphCanvas, background_color?: string, line_color?: string, inactive = false): void {
const points = this.points
if (!points)
return
this.size = size
const w = size[0] - this.margin * 2
const h = size[1] - this.margin * 2
line_color = line_color || "#666"
ctx.save()
ctx.translate(this.margin, this.margin)
if (background_color) {
ctx.fillStyle = "#111"
ctx.fillRect(0, 0, w, h)
ctx.fillStyle = "#222"
ctx.fillRect(w * 0.5, 0, 1, h)
ctx.strokeStyle = "#333"
ctx.strokeRect(0, 0, w, h)
}
ctx.strokeStyle = line_color
if (inactive)
ctx.globalAlpha = 0.5
ctx.beginPath()
for (let i = 0; i < points.length; ++i) {
const p = points[i]
ctx.lineTo(p[0] * w, (1.0 - p[1]) * h)
}
ctx.stroke()
ctx.globalAlpha = 1
if (!inactive)
for (let i = 0; i < points.length; ++i) {
const p = points[i]
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()
}
ctx.restore()
}
//localpos is mouse in curve editor space
onMouseDown(localpos: Point, graphcanvas: LGraphCanvas): boolean {
const points = this.points
if (!points)
return
if (localpos[1] < 0)
return
//this.captureInput(true);
const w = this.size[0] - this.margin * 2
const h = this.size[1] - this.margin * 2
const x = localpos[0] - this.margin
const y = localpos[1] - this.margin
const pos: Point = [x, y]
const max_dist = 30 / graphcanvas.ds.scale
//search closer one
this.selected = this.getCloserPoint(pos, max_dist)
//create one
if (this.selected == -1) {
const point: Point = [x / w, 1 - y / h]
points.push(point)
points.sort(function (a, b) { return a[0] - b[0] })
this.selected = points.indexOf(point)
this.must_update = true
}
if (this.selected != -1)
return true
}
onMouseMove(localpos: Point, graphcanvas: LGraphCanvas): void {
const points = this.points
if (!points)
return
const s = this.selected
if (s < 0)
return
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)]
const max_dist = 30 / graphcanvas.ds.scale
this._nearest = this.getCloserPoint(curvepos, max_dist)
const point = points[s]
if (point) {
const is_edge_point = s == 0 || s == points.length - 1
if (!is_edge_point && (localpos[0] < -10 || localpos[0] > this.size[0] + 10 || localpos[1] < -10 || localpos[1] > this.size[1] + 10)) {
points.splice(s, 1)
this.selected = -1
return
}
if (!is_edge_point) //not edges
point[0] = clamp(x, 0, 1)
else
point[0] = s == 0 ? 0 : 1
point[1] = 1.0 - clamp(y, 0, 1)
points.sort(function (a, b) { return a[0] - b[0] })
this.selected = points.indexOf(point)
this.must_update = true
}
}
// Former params: localpos, graphcanvas
onMouseUp(): boolean {
this.selected = -1
return false
}
getCloserPoint(pos: Point, max_dist: number): number {
const points = this.points
if (!points)
return -1
max_dist = max_dist || 30
const w = (this.size[0] - this.margin * 2)
const h = (this.size[1] - this.margin * 2)
const num = points.length
const p2: Point = [0, 0]
let min_dist = 1000000
let closest = -1
for (let i = 0; i < num; ++i) {
const p = points[i]
p2[0] = p[0] * w
p2[1] = (1.0 - p[1]) * h
const dist = distance(pos, p2)
if (dist > min_dist || dist > max_dist)
continue
closest = i
min_dist = dist
}
return closest
}
return closest
}
}

View File

@@ -1,221 +1,228 @@
import type { Point, Rect, Rect32 } from './interfaces'
import type { CanvasMouseEvent } from './types/events'
import { LiteGraph } from './litegraph'
import type { Point, Rect, Rect32 } from "./interfaces"
import type { CanvasMouseEvent } from "./types/events"
import { LiteGraph } from "./litegraph"
export class DragAndScale {
/** Maximum scale (zoom in) */
max_scale: number
/** Minimum scale (zoom out) */
min_scale: number
offset: Point
scale: number
enabled: boolean
last_mouse: Point
element?: HTMLCanvasElement
visible_area: Rect32
_binded_mouse_callback
dragging?: boolean
viewport?: Rect
/** Maximum scale (zoom in) */
max_scale: number
/** Minimum scale (zoom out) */
min_scale: number
offset: Point
scale: number
enabled: boolean
last_mouse: Point
element?: HTMLCanvasElement
visible_area: Rect32
_binded_mouse_callback
dragging?: boolean
viewport?: Rect
onredraw?(das: DragAndScale): void
/** @deprecated */
onmouse?(e: unknown): boolean
onredraw?(das: DragAndScale): void
/** @deprecated */
onmouse?(e: unknown): boolean
constructor(element?: HTMLCanvasElement, skip_events?: boolean) {
this.offset = new Float32Array([0, 0])
this.scale = 1
this.max_scale = 10
this.min_scale = 0.1
this.onredraw = null
this.enabled = true
this.last_mouse = [0, 0]
this.element = null
this.visible_area = new Float32Array(4)
constructor(element?: HTMLCanvasElement, skip_events?: boolean) {
this.offset = new Float32Array([0, 0])
this.scale = 1
this.max_scale = 10
this.min_scale = 0.1
this.onredraw = null
this.enabled = true
this.last_mouse = [0, 0]
this.element = null
this.visible_area = new Float32Array(4)
if (element) {
this.element = element
if (!skip_events) {
this.bindEvents(element)
}
}
}
/** @deprecated Has not been kept up to date */
bindEvents(element: Node): void {
this.last_mouse = new Float32Array(2)
this._binded_mouse_callback = this.onMouse.bind(this)
LiteGraph.pointerListenerAdd(element, 'down', this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(element, 'move', this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(element, 'up', this._binded_mouse_callback)
element.addEventListener('mousewheel', this._binded_mouse_callback, false)
element.addEventListener('wheel', this._binded_mouse_callback, false)
}
computeVisibleArea(viewport: Rect): void {
if (!this.element) {
this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0
return
}
let width = this.element.width
let height = this.element.height
let startx = -this.offset[0]
let starty = -this.offset[1]
if (viewport) {
startx += viewport[0] / this.scale
starty += viewport[1] / this.scale
width = viewport[2]
height = viewport[3]
}
const endx = startx + width / this.scale
const endy = starty + height / this.scale
this.visible_area[0] = startx
this.visible_area[1] = starty
this.visible_area[2] = endx - startx
this.visible_area[3] = endy - starty
}
/** @deprecated Has not been kept up to date */
onMouse(e: CanvasMouseEvent) {
if (!this.enabled) {
return
}
const canvas = this.element
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
// FIXME: "canvasx" / y are not referenced anywhere - wrong case
// @ts-expect-error Incorrect case
e.canvasx = x
// @ts-expect-error Incorrect case
e.canvasy = y
e.dragging = this.dragging
const is_inside =
!this.viewport ||
(this.viewport &&
x >= this.viewport[0] &&
x < this.viewport[0] + this.viewport[2] &&
y >= this.viewport[1] &&
y < this.viewport[1] + this.viewport[3])
let ignore = false
if (this.onmouse) {
ignore = this.onmouse(e)
}
if (e.type == LiteGraph.pointerevents_method + 'down' && is_inside) {
this.dragging = true
LiteGraph.pointerListenerRemove(canvas, 'move', this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(document, 'move', this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(document, 'up', this._binded_mouse_callback)
} else if (e.type == LiteGraph.pointerevents_method + 'move') {
if (!ignore) {
const deltax = x - this.last_mouse[0]
const deltay = y - this.last_mouse[1]
if (this.dragging) {
this.mouseDrag(deltax, deltay)
if (element) {
this.element = element
if (!skip_events) {
this.bindEvents(element)
}
}
}
} else if (e.type == LiteGraph.pointerevents_method + 'up') {
this.dragging = false
LiteGraph.pointerListenerRemove(document, 'move', this._binded_mouse_callback)
LiteGraph.pointerListenerRemove(document, 'up', this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(canvas, 'move', this._binded_mouse_callback)
} else if (
is_inside &&
(e.type == 'mousewheel' || e.type == 'wheel' || e.type == 'DOMMouseScroll')
) {
// @ts-expect-error Deprecated
e.eventType = 'mousewheel'
// @ts-expect-error Deprecated
if (e.type == 'wheel') e.wheel = -e.deltaY
// @ts-expect-error Deprecated
else e.wheel = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60
//from stack overflow
// @ts-expect-error Deprecated
e.delta = e.wheelDelta
? // @ts-expect-error Deprecated
e.wheelDelta / 40
: e.deltaY
? -e.deltaY / 3
: 0
// @ts-expect-error Deprecated
this.changeDeltaScale(1.0 + e.delta * 0.05)
}
this.last_mouse[0] = x
this.last_mouse[1] = y
/** @deprecated Has not been kept up to date */
bindEvents(element: Node): void {
this.last_mouse = new Float32Array(2)
if (is_inside) {
e.preventDefault()
e.stopPropagation()
return false
}
}
this._binded_mouse_callback = this.onMouse.bind(this)
toCanvasContext(ctx: CanvasRenderingContext2D): void {
ctx.scale(this.scale, this.scale)
ctx.translate(this.offset[0], this.offset[1])
}
LiteGraph.pointerListenerAdd(element, "down", this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(element, "move", this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(element, "up", this._binded_mouse_callback)
convertOffsetToCanvas(pos: Point): Point {
return [(pos[0] + this.offset[0]) * this.scale, (pos[1] + this.offset[1]) * this.scale]
}
convertCanvasToOffset(pos: Point, out?: Point): Point {
out = out || [0, 0]
out[0] = pos[0] / this.scale - this.offset[0]
out[1] = pos[1] / this.scale - this.offset[1]
return out
}
/** @deprecated Has not been kept up to date */
mouseDrag(x: number, y: number): void {
this.offset[0] += x / this.scale
this.offset[1] += y / this.scale
this.onredraw?.(this)
}
changeScale(value: number, zooming_center?: Point): void {
if (value < this.min_scale) {
value = this.min_scale
} else if (value > this.max_scale) {
value = this.max_scale
element.addEventListener(
"mousewheel",
this._binded_mouse_callback,
false
)
element.addEventListener("wheel", this._binded_mouse_callback, false)
}
if (value == this.scale) return
if (!this.element) return
computeVisibleArea(viewport: Rect): void {
if (!this.element) {
this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0
return
}
let width = this.element.width
let height = this.element.height
let startx = -this.offset[0]
let starty = -this.offset[1]
if (viewport) {
startx += viewport[0] / this.scale
starty += viewport[1] / this.scale
width = viewport[2]
height = viewport[3]
}
const endx = startx + width / this.scale
const endy = starty + height / this.scale
this.visible_area[0] = startx
this.visible_area[1] = starty
this.visible_area[2] = endx - startx
this.visible_area[3] = endy - starty
}
const rect = this.element.getBoundingClientRect()
if (!rect) return
/** @deprecated Has not been kept up to date */
onMouse(e: CanvasMouseEvent) {
if (!this.enabled) {
return
}
zooming_center = zooming_center || [rect.width * 0.5, rect.height * 0.5]
const center = this.convertCanvasToOffset(zooming_center)
this.scale = value
if (Math.abs(this.scale - 1) < 0.01) this.scale = 1
const canvas = this.element
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
// FIXME: "canvasx" / y are not referenced anywhere - wrong case
// @ts-expect-error Incorrect case
e.canvasx = x
// @ts-expect-error Incorrect case
e.canvasy = y
e.dragging = this.dragging
const new_center = this.convertCanvasToOffset(zooming_center)
const delta_offset = [new_center[0] - center[0], new_center[1] - center[1]]
const is_inside = !this.viewport || (this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]))
this.offset[0] += delta_offset[0]
this.offset[1] += delta_offset[1]
let ignore = false
if (this.onmouse) {
ignore = this.onmouse(e)
}
this.onredraw?.(this)
}
if (e.type == LiteGraph.pointerevents_method + "down" && is_inside) {
this.dragging = true
LiteGraph.pointerListenerRemove(canvas, "move", this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(document, "move", this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(document, "up", this._binded_mouse_callback)
} else if (e.type == LiteGraph.pointerevents_method + "move") {
if (!ignore) {
const deltax = x - this.last_mouse[0]
const deltay = y - this.last_mouse[1]
if (this.dragging) {
this.mouseDrag(deltax, deltay)
}
}
} else if (e.type == LiteGraph.pointerevents_method + "up") {
this.dragging = false
LiteGraph.pointerListenerRemove(document, "move", this._binded_mouse_callback)
LiteGraph.pointerListenerRemove(document, "up", this._binded_mouse_callback)
LiteGraph.pointerListenerAdd(canvas, "move", this._binded_mouse_callback)
} else if (is_inside &&
(e.type == "mousewheel" ||
e.type == "wheel" ||
e.type == "DOMMouseScroll")) {
// @ts-expect-error Deprecated
e.eventType = "mousewheel"
// @ts-expect-error Deprecated
if (e.type == "wheel") e.wheel = -e.deltaY
// @ts-expect-error Deprecated
else e.wheel = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60
changeDeltaScale(value: number, zooming_center?: Point): void {
this.changeScale(this.scale * value, zooming_center)
}
//from stack overflow
// @ts-expect-error Deprecated
e.delta = e.wheelDelta
// @ts-expect-error Deprecated
? e.wheelDelta / 40
: e.deltaY
? -e.deltaY / 3
: 0
// @ts-expect-error Deprecated
this.changeDeltaScale(1.0 + e.delta * 0.05)
}
reset(): void {
this.scale = 1
this.offset[0] = 0
this.offset[1] = 0
}
this.last_mouse[0] = x
this.last_mouse[1] = y
if (is_inside) {
e.preventDefault()
e.stopPropagation()
return false
}
}
toCanvasContext(ctx: CanvasRenderingContext2D): void {
ctx.scale(this.scale, this.scale)
ctx.translate(this.offset[0], this.offset[1])
}
convertOffsetToCanvas(pos: Point): Point {
return [
(pos[0] + this.offset[0]) * this.scale,
(pos[1] + this.offset[1]) * this.scale
]
}
convertCanvasToOffset(pos: Point, out?: Point): Point {
out = out || [0, 0]
out[0] = pos[0] / this.scale - this.offset[0]
out[1] = pos[1] / this.scale - this.offset[1]
return out
}
/** @deprecated Has not been kept up to date */
mouseDrag(x: number, y: number): void {
this.offset[0] += x / this.scale
this.offset[1] += y / this.scale
this.onredraw?.(this)
}
changeScale(value: number, zooming_center?: Point): void {
if (value < this.min_scale) {
value = this.min_scale
} else if (value > this.max_scale) {
value = this.max_scale
}
if (value == this.scale) return
if (!this.element) return
const rect = this.element.getBoundingClientRect()
if (!rect) return
zooming_center = zooming_center || [
rect.width * 0.5,
rect.height * 0.5
]
const center = this.convertCanvasToOffset(zooming_center)
this.scale = value
if (Math.abs(this.scale - 1) < 0.01) this.scale = 1
const new_center = this.convertCanvasToOffset(zooming_center)
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]
this.onredraw?.(this)
}
changeDeltaScale(value: number, zooming_center?: Point): void {
this.changeScale(this.scale * value, zooming_center)
}
reset(): void {
this.scale = 1
this.offset[0] = 0
this.offset[1] = 0
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,90 @@
export enum BadgePosition {
TopLeft = 'top-left',
TopRight = 'top-right',
TopLeft = "top-left",
TopRight = "top-right",
}
export interface LGraphBadgeOptions {
text: string
fgColor?: string
bgColor?: string
fontSize?: number
padding?: number
height?: number
cornerRadius?: number
text: string;
fgColor?: string;
bgColor?: string;
fontSize?: number;
padding?: number;
height?: number;
cornerRadius?: number;
}
export class LGraphBadge {
text: string
fgColor: string
bgColor: string
fontSize: number
padding: number
height: number
cornerRadius: number
text: string;
fgColor: string;
bgColor: string;
fontSize: number;
padding: number;
height: number;
cornerRadius: number;
constructor({
text,
fgColor = 'white',
bgColor = '#0F1F0F',
fgColor = "white",
bgColor = "#0F1F0F",
fontSize = 12,
padding = 6,
height = 20,
cornerRadius = 5,
}: LGraphBadgeOptions) {
this.text = text
this.fgColor = fgColor
this.bgColor = bgColor
this.fontSize = fontSize
this.padding = padding
this.height = height
this.cornerRadius = cornerRadius
this.text = text;
this.fgColor = fgColor;
this.bgColor = bgColor;
this.fontSize = fontSize;
this.padding = padding;
this.height = height;
this.cornerRadius = cornerRadius;
}
get visible() {
return this.text.length > 0
return this.text.length > 0;
}
getWidth(ctx: CanvasRenderingContext2D) {
if (!this.visible) return 0
if (!this.visible) return 0;
ctx.save()
ctx.font = `${this.fontSize}px sans-serif`
const textWidth = ctx.measureText(this.text).width
ctx.restore()
return textWidth + this.padding * 2
ctx.save();
ctx.font = `${this.fontSize}px sans-serif`;
const textWidth = ctx.measureText(this.text).width;
ctx.restore();
return textWidth + this.padding * 2;
}
draw(ctx: CanvasRenderingContext2D, x: number, y: number): void {
if (!this.visible) return
draw(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
): void {
if (!this.visible) return;
ctx.save()
ctx.font = `${this.fontSize}px sans-serif`
const badgeWidth = this.getWidth(ctx)
const badgeX = 0
ctx.save();
ctx.font = `${this.fontSize}px sans-serif`;
const badgeWidth = this.getWidth(ctx);
const badgeX = 0;
// Draw badge background
ctx.fillStyle = this.bgColor
ctx.beginPath()
ctx.fillStyle = this.bgColor;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(x + badgeX, y, badgeWidth, this.height, this.cornerRadius)
ctx.roundRect(x + badgeX, y, badgeWidth, this.height, this.cornerRadius);
} else {
// Fallback for browsers that don't support roundRect
ctx.rect(x + badgeX, y, badgeWidth, this.height)
ctx.rect(x + badgeX, y, badgeWidth, this.height);
}
ctx.fill()
ctx.fill();
// Draw badge text
ctx.fillStyle = this.fgColor
ctx.fillText(this.text, x + badgeX + this.padding, y + this.height - this.padding)
ctx.fillStyle = this.fgColor;
ctx.fillText(
this.text,
x + badgeX + this.padding,
y + this.height - this.padding
);
ctx.restore()
ctx.restore();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,242 +1,253 @@
import type { IContextMenuValue, Point, Size } from './interfaces'
import type { LGraph } from './LGraph'
import type { ISerialisedGroup } from './types/serialisation'
import { LiteGraph } from './litegraph'
import { LGraphCanvas } from './LGraphCanvas'
import { isInsideRectangle, overlapBounding } from './measure'
import { LGraphNode } from './LGraphNode'
import { RenderShape, TitleMode } from './types/globalEnums'
import type { IContextMenuValue, Point, Size } from "./interfaces"
import type { LGraph } from "./LGraph"
import type { ISerialisedGroup } from "./types/serialisation"
import { LiteGraph } from "./litegraph"
import { LGraphCanvas } from "./LGraphCanvas"
import { isInsideRectangle, overlapBounding } from "./measure"
import { LGraphNode } from "./LGraphNode"
import { RenderShape, TitleMode } from "./types/globalEnums"
export interface IGraphGroupFlags extends Record<string, unknown> {
pinned?: true
pinned?: true
}
export class LGraphGroup {
color: string
title: string
font?: string
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
_bounding: Float32Array = new Float32Array([10, 10, 140, 80])
_pos: Point = this._bounding.subarray(0, 2)
_size: Size = this._bounding.subarray(2, 4)
_nodes: LGraphNode[] = []
graph: LGraph | null = null
flags: IGraphGroupFlags = {}
selected?: boolean
color: string
title: string
font?: string
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
_bounding: Float32Array = new Float32Array([10, 10, 140, 80])
_pos: Point = this._bounding.subarray(0, 2)
_size: Size = this._bounding.subarray(2, 4)
_nodes: LGraphNode[] = []
graph: LGraph | null = null
flags: IGraphGroupFlags = {}
selected?: boolean
constructor(title?: string) {
this.title = title || 'Group'
this.color = LGraphCanvas.node_colors.pale_blue ? LGraphCanvas.node_colors.pale_blue.groupcolor : '#AAA'
}
/** Position of the group, as x,y co-ordinates in graph space */
get pos() {
return this._pos
}
set pos(v) {
if (!v || v.length < 2) return
this._pos[0] = v[0]
this._pos[1] = v[1]
}
/** Size of the group, as width,height in graph units */
get size() {
return this._size
}
set size(v) {
if (!v || v.length < 2) return
this._size[0] = Math.max(140, v[0])
this._size[1] = Math.max(80, v[1])
}
get nodes() {
return this._nodes
}
get titleHeight() {
return this.font_size * 1.4
}
get pinned() {
return !!this.flags.pinned
}
pin(): void {
this.flags.pinned = true
}
unpin(): void {
delete this.flags.pinned
}
configure(o: ISerialisedGroup): void {
this.title = o.title
this._bounding.set(o.bounding)
this.color = o.color
this.flags = o.flags || this.flags
if (o.font_size) this.font_size = o.font_size
}
serialize(): ISerialisedGroup {
const b = this._bounding
return {
title: this.title,
bounding: [Math.round(b[0]), Math.round(b[1]), Math.round(b[2]), Math.round(b[3])],
color: this.color,
font_size: this.font_size,
flags: this.flags,
constructor(title?: string) {
this.title = title || "Group"
this.color = LGraphCanvas.node_colors.pale_blue
? LGraphCanvas.node_colors.pale_blue.groupcolor
: "#AAA"
}
}
/**
* Draws the group on the canvas
* @param {LGraphCanvas} graphCanvas
* @param {CanvasRenderingContext2D} ctx
*/
draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void {
const padding = 4
ctx.fillStyle = this.color
ctx.strokeStyle = this.color
const [x, y] = this._pos
const [width, height] = this._size
ctx.globalAlpha = 0.25 * graphCanvas.editor_alpha
ctx.beginPath()
ctx.rect(x + 0.5, y + 0.5, width, height)
ctx.fill()
ctx.globalAlpha = graphCanvas.editor_alpha
ctx.stroke()
ctx.beginPath()
ctx.moveTo(x + width, y + height)
ctx.lineTo(x + width - 10, y + height)
ctx.lineTo(x + width, y + height - 10)
ctx.fill()
const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
ctx.font = font_size + 'px Arial'
ctx.textAlign = 'left'
ctx.fillText(this.title + (this.pinned ? '📌' : ''), x + padding, y + font_size)
if (LiteGraph.highlight_selected_group && this.selected) {
graphCanvas.drawSelectionBounding(ctx, this._bounding, {
shape: RenderShape.BOX,
title_height: this.titleHeight,
title_mode: TitleMode.NORMAL_TITLE,
fgcolor: this.color,
padding,
})
/** Position of the group, as x,y co-ordinates in graph space */
get pos() {
return this._pos
}
}
set pos(v) {
if (!v || v.length < 2) return
resize(width: number, height: number): void {
if (this.pinned) return
this._size[0] = width
this._size[1] = height
}
move(deltax: number, deltay: number, ignore_nodes = false): void {
if (this.pinned) return
this._pos[0] += deltax
this._pos[1] += deltay
if (ignore_nodes) return
for (let i = 0; i < this._nodes.length; ++i) {
const node = this._nodes[i]
node.pos[0] += deltax
node.pos[1] += deltay
this._pos[0] = v[0]
this._pos[1] = v[1]
}
}
recomputeInsideNodes(): void {
this._nodes.length = 0
const nodes = this.graph._nodes
const node_bounding = new Float32Array(4)
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i]
node.getBounding(node_bounding)
//out of the visible area
if (!overlapBounding(this._bounding, node_bounding)) continue
this._nodes.push(node)
/** Size of the group, as width,height in graph units */
get size() {
return this._size
}
}
set size(v) {
if (!v || v.length < 2) return
/**
* Add nodes to the group and adjust the group's position and size accordingly
* @param {LGraphNode[]} nodes - The nodes to add to the group
* @param {number} [padding=10] - The padding around the group
* @returns {void}
*/
addNodes(nodes: LGraphNode[], padding: number = 10): void {
if (!this._nodes && nodes.length === 0) return
this._size[0] = Math.max(140, v[0])
this._size[1] = Math.max(80, v[1])
}
const allNodes = [...(this._nodes || []), ...nodes]
get nodes() {
return this._nodes
}
const bounds = allNodes.reduce(
(acc, node) => {
const [x, y] = node.pos
const [width, height] = node.size
const isReroute = node.type === 'Reroute'
const isCollapsed = node.flags?.collapsed
get titleHeight() {
return this.font_size * 1.4
}
const top = y - (isReroute ? 0 : LiteGraph.NODE_TITLE_HEIGHT)
const bottom = isCollapsed ? top + LiteGraph.NODE_TITLE_HEIGHT : y + height
const right = isCollapsed && node._collapsed_width ? x + Math.round(node._collapsed_width) : x + width
get pinned() {
return !!this.flags.pinned
}
pin(): void {
this.flags.pinned = true
}
unpin(): void {
delete this.flags.pinned
}
configure(o: ISerialisedGroup): void {
this.title = o.title
this._bounding.set(o.bounding)
this.color = o.color
this.flags = o.flags || this.flags
if (o.font_size) this.font_size = o.font_size
}
serialize(): ISerialisedGroup {
const b = this._bounding
return {
left: Math.min(acc.left, x),
top: Math.min(acc.top, top),
right: Math.max(acc.right, right),
bottom: Math.max(acc.bottom, bottom),
title: this.title,
bounding: [
Math.round(b[0]),
Math.round(b[1]),
Math.round(b[2]),
Math.round(b[3])
],
color: this.color,
font_size: this.font_size,
flags: this.flags,
}
},
{ left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity },
)
}
this.pos = [bounds.left - padding, bounds.top - padding - this.titleHeight]
/**
* Draws the group on the canvas
* @param {LGraphCanvas} graphCanvas
* @param {CanvasRenderingContext2D} ctx
*/
draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void {
const padding = 4
this.size = [bounds.right - bounds.left + padding * 2, bounds.bottom - bounds.top + padding * 2 + this.titleHeight]
}
ctx.fillStyle = this.color
ctx.strokeStyle = this.color
const [x, y] = this._pos
const [width, height] = this._size
ctx.globalAlpha = 0.25 * graphCanvas.editor_alpha
ctx.beginPath()
ctx.rect(x + 0.5, y + 0.5, width, height)
ctx.fill()
ctx.globalAlpha = graphCanvas.editor_alpha
ctx.stroke()
getMenuOptions(): IContextMenuValue[] {
return [
{
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: 'Color',
has_submenu: true,
callback: LGraphCanvas.onMenuNodeColors,
},
{
content: 'Font size',
property: 'font_size',
type: 'Number',
callback: LGraphCanvas.onShowPropertyEditor,
},
null,
{ content: 'Remove', callback: LGraphCanvas.onMenuNodeRemove },
]
}
ctx.beginPath()
ctx.moveTo(x + width, y + height)
ctx.lineTo(x + width - 10, y + height)
ctx.lineTo(x + width, y + height - 10)
ctx.fill()
isPointInTitlebar(x: number, y: number): boolean {
const b = this._bounding
return isInsideRectangle(x, y, b[0], b[1], b[2], this.titleHeight)
}
const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
ctx.font = font_size + "px Arial"
ctx.textAlign = "left"
ctx.fillText(this.title + (this.pinned ? "📌" : ""), x + padding, y + font_size)
isPointInside = LGraphNode.prototype.isPointInside
setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas
if (LiteGraph.highlight_selected_group && this.selected) {
graphCanvas.drawSelectionBounding(ctx, this._bounding, {
shape: RenderShape.BOX,
title_height: this.titleHeight,
title_mode: TitleMode.NORMAL_TITLE,
fgcolor: this.color,
padding,
})
}
}
resize(width: number, height: number): void {
if (this.pinned) return
this._size[0] = width
this._size[1] = height
}
move(deltax: number, deltay: number, ignore_nodes = false): void {
if (this.pinned) return
this._pos[0] += deltax
this._pos[1] += deltay
if (ignore_nodes) return
for (let i = 0; i < this._nodes.length; ++i) {
const node = this._nodes[i]
node.pos[0] += deltax
node.pos[1] += deltay
}
}
recomputeInsideNodes(): void {
this._nodes.length = 0
const nodes = this.graph._nodes
const node_bounding = new Float32Array(4)
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i]
node.getBounding(node_bounding)
//out of the visible area
if (!overlapBounding(this._bounding, node_bounding))
continue
this._nodes.push(node)
}
}
/**
* Add nodes to the group and adjust the group's position and size accordingly
* @param {LGraphNode[]} nodes - The nodes to add to the group
* @param {number} [padding=10] - The padding around the group
* @returns {void}
*/
addNodes(nodes: LGraphNode[], padding: number = 10): void {
if (!this._nodes && nodes.length === 0) return
const allNodes = [...(this._nodes || []), ...nodes]
const bounds = allNodes.reduce((acc, node) => {
const [x, y] = node.pos
const [width, height] = node.size
const isReroute = node.type === "Reroute"
const isCollapsed = node.flags?.collapsed
const top = y - (isReroute ? 0 : LiteGraph.NODE_TITLE_HEIGHT)
const bottom = isCollapsed ? top + LiteGraph.NODE_TITLE_HEIGHT : y + height
const right = isCollapsed && node._collapsed_width ? x + Math.round(node._collapsed_width) : x + width
return {
left: Math.min(acc.left, x),
top: Math.min(acc.top, top),
right: Math.max(acc.right, right),
bottom: Math.max(acc.bottom, bottom)
}
}, { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity })
this.pos = [
bounds.left - padding,
bounds.top - padding - this.titleHeight
]
this.size = [
bounds.right - bounds.left + padding * 2,
bounds.bottom - bounds.top + padding * 2 + this.titleHeight
]
}
getMenuOptions(): IContextMenuValue[] {
return [
{
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: "Color",
has_submenu: true,
callback: LGraphCanvas.onMenuNodeColors
},
{
content: "Font size",
property: "font_size",
type: "Number",
callback: LGraphCanvas.onShowPropertyEditor
},
null,
{ content: "Remove", callback: LGraphCanvas.onMenuNodeRemove }
]
}
isPointInTitlebar(x: number, y: number): boolean {
const b = this._bounding
return isInsideRectangle(x, y, b[0], b[1], b[2], this.titleHeight)
}
isPointInside = LGraphNode.prototype.isPointInside
setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import type { CanvasColour, ISlotType } from './interfaces'
import type { NodeId } from './LGraphNode'
import type { Serialisable, SerialisableLLink } from './types/serialisation'
import type { CanvasColour, ISlotType } from "./interfaces"
import type { NodeId } from "./LGraphNode"
import type { Serialisable, SerialisableLLink } from "./types/serialisation"
export type LinkId = number | string
@@ -8,96 +8,101 @@ export type SerialisedLLinkArray = [LinkId, NodeId, number, NodeId, number, ISlo
//this is the class in charge of storing link information
export class LLink implements Serialisable<SerialisableLLink> {
/** Link ID */
id: LinkId
type: ISlotType
/** Output node ID */
origin_id: NodeId
/** Output slot index */
origin_slot: number
/** Input node ID */
target_id: NodeId
/** Input slot index */
target_slot: number
data?: number | string | boolean | { toToolTip?(): string }
_data?: unknown
/** Centre point of the link, calculated during render only - can be inaccurate */
_pos: Float32Array
/** @todo Clean up - never implemented in comfy. */
_last_time?: number
/** The last canvas 2D path that was used to render this link */
path?: Path2D
/** Link ID */
id: LinkId
type: ISlotType
/** Output node ID */
origin_id: NodeId
/** Output slot index */
origin_slot: number
/** Input node ID */
target_id: NodeId
/** Input slot index */
target_slot: number
data?: number | string | boolean | { toToolTip?(): string }
_data?: unknown
/** Centre point of the link, calculated during render only - can be inaccurate */
_pos: Float32Array
/** @todo Clean up - never implemented in comfy. */
_last_time?: number
/** The last canvas 2D path that was used to render this link */
path?: Path2D
#color?: CanvasColour
/** Custom colour for this link only */
public get color(): CanvasColour {
return this.#color
}
public set color(value: CanvasColour) {
this.#color = value === '' ? null : value
}
constructor(id: LinkId, type: ISlotType, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number) {
this.id = id
this.type = type
this.origin_id = origin_id
this.origin_slot = origin_slot
this.target_id = target_id
this.target_slot = target_slot
this._data = null
this._pos = new Float32Array(2) //center
}
/** @deprecated Use {@link LLink.create} */
static createFromArray(data: SerialisedLLinkArray): LLink {
return new LLink(data[0], data[5], data[1], data[2], data[3], data[4])
}
/**
* LLink static factory: creates a new LLink from the provided data.
* @param data Serialised LLink data to create the link from
* @returns A new LLink
*/
static create(data: SerialisableLLink): LLink {
return new LLink(data.id, data.type, data.origin_id, data.origin_slot, data.target_id, data.target_slot)
}
configure(o: LLink | SerialisedLLinkArray) {
if (Array.isArray(o)) {
this.id = o[0]
this.origin_id = o[1]
this.origin_slot = o[2]
this.target_id = o[3]
this.target_slot = o[4]
this.type = o[5]
} else {
this.id = o.id
this.type = o.type
this.origin_id = o.origin_id
this.origin_slot = o.origin_slot
this.target_id = o.target_id
this.target_slot = o.target_slot
#color?: CanvasColour
/** Custom colour for this link only */
public get color(): CanvasColour { return this.#color }
public set color(value: CanvasColour) {
this.#color = value === "" ? null : value
}
}
/**
* @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array)
* @returns An array representing this LLink
*/
serialize(): SerialisedLLinkArray {
return [this.id, this.origin_id, this.origin_slot, this.target_id, this.target_slot, this.type]
}
constructor(id: LinkId, type: ISlotType, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number) {
this.id = id
this.type = type
this.origin_id = origin_id
this.origin_slot = origin_slot
this.target_id = target_id
this.target_slot = target_slot
asSerialisable(): SerialisableLLink {
const copy: SerialisableLLink = {
id: this.id,
origin_id: this.origin_id,
origin_slot: this.origin_slot,
target_id: this.target_id,
target_slot: this.target_slot,
type: this.type,
this._data = null
this._pos = new Float32Array(2) //center
}
/** @deprecated Use {@link LLink.create} */
static createFromArray(data: SerialisedLLinkArray): LLink {
return new LLink(data[0], data[5], data[1], data[2], data[3], data[4])
}
/**
* LLink static factory: creates a new LLink from the provided data.
* @param data Serialised LLink data to create the link from
* @returns A new LLink
*/
static create(data: SerialisableLLink): LLink {
return new LLink(data.id, data.type, data.origin_id, data.origin_slot, data.target_id, data.target_slot)
}
configure(o: LLink | SerialisedLLinkArray) {
if (Array.isArray(o)) {
this.id = o[0]
this.origin_id = o[1]
this.origin_slot = o[2]
this.target_id = o[3]
this.target_slot = o[4]
this.type = o[5]
} else {
this.id = o.id
this.type = o.type
this.origin_id = o.origin_id
this.origin_slot = o.origin_slot
this.target_id = o.target_id
this.target_slot = o.target_slot
}
}
/**
* @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array)
* @returns An array representing this LLink
*/
serialize(): SerialisedLLinkArray {
return [
this.id,
this.origin_id,
this.origin_slot,
this.target_id,
this.target_slot,
this.type
]
}
asSerialisable(): SerialisableLLink {
const copy: SerialisableLLink = {
id: this.id,
origin_id: this.origin_id,
origin_slot: this.origin_slot,
target_id: this.target_id,
target_slot: this.target_slot,
type: this.type
}
return copy
}
return copy
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +1,55 @@
/** 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>> {
getOwnPropertyDescriptor(target: Map<number | string, V>, p: string | symbol): PropertyDescriptor | undefined {
const value = this.get(target, p)
if (value)
return {
configurable: true,
enumerable: true,
value
}
}
getOwnPropertyDescriptor(target: Map<number | string, V>, p: string | symbol): PropertyDescriptor | undefined {
const value = this.get(target, p)
if (value) return {
configurable: true,
enumerable: true,
value
}
}
has(target: Map<number | string, V>, p: string | symbol): boolean {
if (typeof p === 'symbol') return false
has(target: Map<number | string, V>, p: string | symbol): boolean {
if (typeof p === "symbol") return false
const int = parseInt(p, 10)
return target.has(!isNaN(int) ? int : p)
}
const int = parseInt(p, 10)
return target.has(!isNaN(int) ? int : p)
}
ownKeys(target: Map<number | string, V>): ArrayLike<string | symbol> {
return [...target.keys()].map((x) => String(x))
}
ownKeys(target: Map<number | string, V>): ArrayLike<string | symbol> {
return [...target.keys()].map(x => String(x))
}
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
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
const int = parseInt(p, 10)
return target.get(!isNaN(int) ? int : p)
}
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)
return true
}
const int = parseInt(p, 10)
target.set(!isNaN(int) ? int : p, newValue)
return true
}
deleteProperty(target: Map<number | string, V>, p: string | symbol): boolean {
return target.delete(p as number | string)
}
deleteProperty(target: Map<number | string, V>, p: string | symbol): boolean {
return target.delete(p as number | string)
}
static bindAllMethods(map: Map<any, any>): void {
map.clear = map.clear.bind(map)
map.delete = map.delete.bind(map)
map.forEach = map.forEach.bind(map)
map.get = map.get.bind(map)
map.has = map.has.bind(map)
map.set = map.set.bind(map)
map.entries = map.entries.bind(map)
map.keys = map.keys.bind(map)
map.values = map.values.bind(map)
}
static bindAllMethods(map: Map<any, any>): void {
map.clear = map.clear.bind(map)
map.delete = map.delete.bind(map)
map.forEach = map.forEach.bind(map)
map.get = map.get.bind(map)
map.has = map.has.bind(map)
map.set = map.set.bind(map)
map.entries = map.entries.bind(map)
map.keys = map.keys.bind(map)
map.values = map.values.bind(map)
}
}

View File

@@ -1,9 +1,9 @@
import type { Vector2 } from './litegraph'
import type { INodeSlot } from './interfaces'
import { LinkDirection, RenderShape } from './types/globalEnums'
import type { Vector2 } from "./litegraph";
import type { INodeSlot } from "./interfaces"
import { LinkDirection, RenderShape } from "./types/globalEnums"
export enum SlotType {
Array = 'array',
Array = "array",
Event = -1,
}
@@ -25,8 +25,8 @@ export enum SlotDirection {
}
export enum LabelPosition {
Left = 'left',
Right = 'right',
Left = "left",
Right = "right",
}
export function drawSlot(
@@ -34,7 +34,7 @@ export function drawSlot(
slot: Partial<INodeSlot>,
pos: Vector2,
{
label_color = '#AAA',
label_color = "#AAA",
label_position = LabelPosition.Right,
horizontal = false,
low_quality = false,
@@ -42,42 +42,44 @@ export function drawSlot(
do_stroke = false,
highlight = false,
}: {
label_color?: string
label_position?: LabelPosition
horizontal?: boolean
low_quality?: boolean
render_text?: boolean
do_stroke?: boolean
highlight?: boolean
} = {},
label_color?: string;
label_position?: LabelPosition;
horizontal?: boolean;
low_quality?: boolean;
render_text?: boolean;
do_stroke?: boolean;
highlight?: boolean;
} = {}
) {
// Save the current fillStyle and strokeStyle
const originalFillStyle = ctx.fillStyle
const originalStrokeStyle = ctx.strokeStyle
const originalLineWidth = ctx.lineWidth
const originalFillStyle = ctx.fillStyle;
const originalStrokeStyle = ctx.strokeStyle;
const originalLineWidth = ctx.lineWidth;
const slot_type = slot.type as SlotType
const slot_shape = (slot_type === SlotType.Array ? SlotShape.Grid : slot.shape) as SlotShape
const slot_type = slot.type as SlotType;
const slot_shape = (
slot_type === SlotType.Array ? SlotShape.Grid : slot.shape
) as SlotShape;
ctx.beginPath()
let doStroke = do_stroke
let doFill = true
ctx.beginPath();
let doStroke = do_stroke;
let doFill = true;
if (slot_type === SlotType.Event || slot_shape === SlotShape.Box) {
if (horizontal) {
ctx.rect(pos[0] - 5 + 0.5, pos[1] - 8 + 0.5, 10, 14)
ctx.rect(pos[0] - 5 + 0.5, pos[1] - 8 + 0.5, 10, 14);
} else {
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10);
}
} else if (slot_shape === SlotShape.Arrow) {
ctx.moveTo(pos[0] + 8, pos[1] + 0.5)
ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5)
ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5)
ctx.closePath()
ctx.moveTo(pos[0] + 8, pos[1] + 0.5);
ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5);
ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5);
ctx.closePath();
} else if (slot_shape === SlotShape.Grid) {
const gridSize = 3
const cellSize = 2
const spacing = 3
const gridSize = 3;
const cellSize = 2;
const spacing = 3;
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
@@ -86,58 +88,58 @@ export function drawSlot(
pos[1] - 4 + y * spacing,
cellSize,
cellSize
)
);
}
}
doStroke = false
doStroke = false;
} else {
// Default rendering for circle, hollow circle.
if (low_quality) {
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8)
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8);
} else {
let radius: number
let radius: number;
if (slot_shape === SlotShape.HollowCircle) {
doFill = false
doStroke = true
ctx.lineWidth = 3
ctx.strokeStyle = ctx.fillStyle
radius = highlight ? 4 : 3
doFill = false;
doStroke = true;
ctx.lineWidth = 3;
ctx.strokeStyle = ctx.fillStyle;
radius = highlight ? 4 : 3;
} else {
// Normal circle
radius = highlight ? 5 : 4
radius = highlight ? 5 : 4;
}
ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2)
ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2);
}
}
if (doFill) ctx.fill()
if (!low_quality && doStroke) ctx.stroke()
if (doFill) ctx.fill();
if (!low_quality && doStroke) ctx.stroke();
// render slot label
if (render_text) {
const text = slot.label != null ? slot.label : slot.name
const text = slot.label != null ? slot.label : slot.name;
if (text) {
// TODO: Finish impl. Highlight text on mouseover unless we're connecting links.
ctx.fillStyle = label_color
ctx.fillStyle = label_color;
if (label_position === LabelPosition.Right) {
if (horizontal || slot.dir == LinkDirection.UP) {
ctx.fillText(text, pos[0], pos[1] - 10)
ctx.fillText(text, pos[0], pos[1] - 10);
} else {
ctx.fillText(text, pos[0] + 10, pos[1] + 5)
ctx.fillText(text, pos[0] + 10, pos[1] + 5);
}
} else {
if (horizontal || slot.dir == LinkDirection.DOWN) {
ctx.fillText(text, pos[0], pos[1] - 8)
ctx.fillText(text, pos[0], pos[1] - 8);
} else {
ctx.fillText(text, pos[0] - 10, pos[1] + 5)
ctx.fillText(text, pos[0] - 10, pos[1] + 5);
}
}
}
}
// Restore the original fillStyle and strokeStyle
ctx.fillStyle = originalFillStyle
ctx.strokeStyle = originalStrokeStyle
ctx.lineWidth = originalLineWidth
ctx.fillStyle = originalFillStyle;
ctx.strokeStyle = originalStrokeStyle;
ctx.lineWidth = originalLineWidth;
}

View File

@@ -1,29 +1,29 @@
import type { ContextMenu } from './ContextMenu'
import type { LGraphNode } from './LGraphNode'
import type { LinkDirection, RenderShape } from './types/globalEnums'
import type { LinkId } from './LLink'
import type { ContextMenu } from "./ContextMenu"
import type { LGraphNode } from "./LGraphNode"
import type { LinkDirection, RenderShape } from "./types/globalEnums"
import type { LinkId } from "./LLink"
export type Dictionary<T> = { [key: string]: T }
/** Allows all properties to be null. The same as `Partial<T>`, but adds null instead of undefined. */
export type NullableProperties<T> = {
[P in keyof T]: T[P] | null
[P in keyof T]: T[P] | null
}
export type CanvasColour = string | CanvasGradient | CanvasPattern
export interface IInputOrOutput {
// If an input, this will be defined
input?: INodeInputSlot
// If an output, this will be defined
output?: INodeOutputSlot
// If an input, this will be defined
input?: INodeInputSlot
// If an output, this will be defined
output?: INodeOutputSlot
}
export interface IFoundSlot extends IInputOrOutput {
// Slot index
slot: number
// Centre point of the rendered slot connection
link_pos: Point
// Slot index
slot: number
// Centre point of the rendered slot connection
link_pos: Point
}
/** A point represented as `[x, y]` co-ordinates */
@@ -42,31 +42,13 @@ export type Rect = ArRect | Float32Array | Float64Array
export type Rect32 = Float32Array
/** A point represented as `[x, y]` co-ordinates that will not be modified */
export type ReadOnlyPoint =
| readonly [x: number, y: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
export type ReadOnlyPoint = readonly [x: number, y: number] | ReadOnlyTypedArray<Float32Array> | ReadOnlyTypedArray<Float64Array>
/** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */
export type ReadOnlyRect =
| readonly [x: number, y: number, width: number, height: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
export type ReadOnlyRect = readonly [x: number, y: number, width: number, height: number] | ReadOnlyTypedArray<Float32Array> | ReadOnlyTypedArray<Float64Array>
type TypedArrays =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
type TypedArrays = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array
type TypedBigIntArrays = BigInt64Array | BigUint64Array
type ReadOnlyTypedArray<T extends TypedArrays | TypedBigIntArrays> = Omit<
T,
'fill' | 'copyWithin' | 'reverse' | 'set' | 'sort' | 'subarray'
>
type ReadOnlyTypedArray<T extends TypedArrays | TypedBigIntArrays> = Omit<T, "fill" | "copyWithin" | "reverse" | "set" | "sort" | "subarray">
/** Union of property names that are of type Match */
export type KeysOfType<T, Match> = { [P in keyof T]: T[P] extends Match ? P : never }[keyof T]
@@ -78,102 +60,96 @@ export type PickByType<T, Match> = { [P in keyof T]: Extract<T[P], Match> }
export type MethodNames<T> = KeysOfType<T, ((...args: any) => any) | undefined>
export interface IBoundaryNodes {
top: LGraphNode
right: LGraphNode
bottom: LGraphNode
left: LGraphNode
top: LGraphNode
right: LGraphNode
bottom: LGraphNode
left: LGraphNode
}
export type Direction = 'top' | 'bottom' | 'left' | 'right'
export type Direction = "top" | "bottom" | "left" | "right"
export interface IOptionalSlotData<TSlot extends INodeInputSlot | INodeOutputSlot> {
content: string
value: TSlot
className?: string
content: string
value: TSlot
className?: string
}
export type ISlotType = number | string
export interface INodeSlot {
name: string
type: ISlotType
dir?: LinkDirection
removable?: boolean
shape?: RenderShape
not_subgraph_input?: boolean
color_off?: CanvasColour
color_on?: CanvasColour
label?: string
locked?: boolean
nameLocked?: boolean
pos?: Point
widget?: unknown
name: string
type: ISlotType
dir?: LinkDirection
removable?: boolean
shape?: RenderShape
not_subgraph_input?: boolean
color_off?: CanvasColour
color_on?: CanvasColour
label?: string
locked?: boolean
nameLocked?: boolean
pos?: Point
widget?: unknown
}
export interface INodeFlags {
skip_repeated_outputs?: boolean
allow_interaction?: boolean
pinned?: boolean
collapsed?: boolean
skip_repeated_outputs?: boolean
allow_interaction?: boolean
pinned?: boolean
collapsed?: boolean
}
export interface INodeInputSlot extends INodeSlot {
link: LinkId | null
not_subgraph_input?: boolean
link: LinkId | null
not_subgraph_input?: boolean
}
export interface INodeOutputSlot extends INodeSlot {
links: LinkId[] | null
_data?: unknown
slot_index?: number
not_subgraph_output?: boolean
links: LinkId[] | null
_data?: unknown
slot_index?: number
not_subgraph_output?: boolean
}
/** Links */
export interface ConnectingLink extends IInputOrOutput {
node: LGraphNode
slot: number
pos: Point
direction?: LinkDirection
node: LGraphNode
slot: number
pos: Point
direction?: LinkDirection
}
interface IContextMenuBase {
title?: string
className?: string
callback?(
value?: unknown,
options?: unknown,
event?: MouseEvent,
previous_menu?: ContextMenu,
node?: LGraphNode,
): void | boolean
title?: string
className?: string
callback?(value?: unknown, options?: unknown, event?: MouseEvent, previous_menu?: ContextMenu, node?: LGraphNode): void | boolean
}
/** ContextMenu */
export interface IContextMenuOptions extends IContextMenuBase {
ignore_item_callbacks?: boolean
parentMenu?: ContextMenu
event?: MouseEvent
extra?: unknown
scroll_speed?: number
left?: number
top?: number
scale?: string
node?: LGraphNode
autoopen?: boolean
ignore_item_callbacks?: boolean
parentMenu?: ContextMenu
event?: MouseEvent
extra?: unknown
scroll_speed?: number
left?: number
top?: number
scale?: string
node?: LGraphNode
autoopen?: boolean
}
export interface IContextMenuValue extends IContextMenuBase {
value?: string
content: string
has_submenu?: boolean
disabled?: boolean
submenu?: IContextMenuSubmenu
property?: string
type?: string
slot?: IFoundSlot
value?: string
content: string
has_submenu?: boolean
disabled?: boolean
submenu?: IContextMenuSubmenu
property?: string
type?: string
slot?: IFoundSlot
}
export interface IContextMenuSubmenu extends IContextMenuOptions {
options: ConstructorParameters<typeof ContextMenu>[0]
options: ConstructorParameters<typeof ContextMenu>[0]
}

View File

@@ -1,73 +1,32 @@
import type { Point, ConnectingLink } from './interfaces'
import type {
INodeSlot,
INodeInputSlot,
INodeOutputSlot,
CanvasColour,
Direction,
IBoundaryNodes,
IContextMenuOptions,
IContextMenuValue,
IFoundSlot,
IInputOrOutput,
INodeFlags,
IOptionalSlotData,
ISlotType,
KeysOfType,
MethodNames,
PickByType,
Rect,
Rect32,
Size,
} from './interfaces'
import type { SlotShape, LabelPosition, SlotDirection, SlotType } from './draw'
import type { IWidget } from './types/widgets'
import type { RenderShape, TitleMode } from './types/globalEnums'
import type { CanvasEventDetail } from './types/events'
import { LiteGraphGlobal } from './LiteGraphGlobal'
import { loadPolyfills } from './polyfills'
import type { Point, ConnectingLink } from "./interfaces"
import type { INodeSlot, INodeInputSlot, INodeOutputSlot, CanvasColour, Direction, IBoundaryNodes, IContextMenuOptions, IContextMenuValue, IFoundSlot, IInputOrOutput, INodeFlags, IOptionalSlotData, ISlotType, KeysOfType, MethodNames, PickByType, Rect, Rect32, Size } from "./interfaces"
import type { SlotShape, LabelPosition, SlotDirection, SlotType } from "./draw"
import type { IWidget } from "./types/widgets"
import type { RenderShape, TitleMode } from "./types/globalEnums"
import type { CanvasEventDetail } from "./types/events"
import { LiteGraphGlobal } from "./LiteGraphGlobal"
import { loadPolyfills } from "./polyfills"
import { LGraph } from './LGraph'
import { LGraphCanvas, type LGraphCanvasState } from './LGraphCanvas'
import { DragAndScale } from './DragAndScale'
import { LGraphNode } from './LGraphNode'
import { LGraphGroup } from './LGraphGroup'
import { LLink } from './LLink'
import { ContextMenu } from './ContextMenu'
import { CurveEditor } from './CurveEditor'
import { LGraphBadge, BadgePosition } from './LGraphBadge'
import { LGraph } from "./LGraph"
import { LGraphCanvas, type LGraphCanvasState } from "./LGraphCanvas"
import { DragAndScale } from "./DragAndScale"
import { LGraphNode } from "./LGraphNode"
import { LGraphGroup } from "./LGraphGroup"
import { LLink } from "./LLink"
import { ContextMenu } from "./ContextMenu"
import { CurveEditor } from "./CurveEditor"
import { LGraphBadge, BadgePosition } from "./LGraphBadge"
export const LiteGraph = new LiteGraphGlobal()
export { LGraph, LGraphCanvas, LGraphCanvasState, DragAndScale, LGraphNode, LGraphGroup, LLink, ContextMenu, CurveEditor }
export {
INodeSlot,
INodeInputSlot,
INodeOutputSlot,
ConnectingLink,
CanvasColour,
Direction,
IBoundaryNodes,
IContextMenuOptions,
IContextMenuValue,
IFoundSlot,
IInputOrOutput,
INodeFlags,
IOptionalSlotData,
ISlotType,
KeysOfType,
MethodNames,
PickByType,
Rect,
Rect32,
Size,
}
export { INodeSlot, INodeInputSlot, INodeOutputSlot, ConnectingLink, CanvasColour, Direction, IBoundaryNodes, IContextMenuOptions, IContextMenuValue, IFoundSlot, IInputOrOutput, INodeFlags, IOptionalSlotData, ISlotType, KeysOfType, MethodNames, PickByType, Rect, Rect32, Size }
export { IWidget }
export { LGraphBadge, BadgePosition }
export { SlotShape, LabelPosition, SlotDirection, SlotType }
export function clamp(v: number, a: number, b: number): number {
return a > v ? a : b < v ? b : v
}
return a > v ? a : b < v ? b : v
};
// Load legacy polyfills
loadPolyfills()
@@ -81,69 +40,67 @@ export type Vector2 = Point
export type Vector4 = [number, number, number, number]
export interface IContextMenuItem {
content: string
callback?: ContextMenuEventListener
/** Used as innerHTML for extra child element */
title?: string
disabled?: boolean
has_submenu?: boolean
submenu?: {
options: IContextMenuItem[]
} & IContextMenuOptions
className?: string
content: string
callback?: ContextMenuEventListener
/** Used as innerHTML for extra child element */
title?: string
disabled?: boolean
has_submenu?: boolean
submenu?: {
options: IContextMenuItem[]
} & IContextMenuOptions
className?: string
}
export type ContextMenuEventListener = (
value: IContextMenuItem,
options: IContextMenuOptions,
event: MouseEvent,
parentMenu: ContextMenu | undefined,
node: LGraphNode,
value: IContextMenuItem,
options: IContextMenuOptions,
event: MouseEvent,
parentMenu: ContextMenu | undefined,
node: LGraphNode
) => boolean | void
export interface LinkReleaseContext {
node_to?: LGraphNode
node_from?: LGraphNode
slot_from: INodeSlot
type_filter_in?: string
type_filter_out?: string
node_to?: LGraphNode
node_from?: LGraphNode
slot_from: INodeSlot
type_filter_in?: string
type_filter_out?: string
}
export interface LinkReleaseContextExtended {
links: ConnectingLink[]
links: ConnectingLink[]
}
/** @deprecated Confirm no downstream consumers, then remove. */
export type LiteGraphCanvasEventType = 'empty-release' | 'empty-double-click' | 'group-double-click'
export type LiteGraphCanvasEventType = "empty-release" | "empty-double-click" | "group-double-click"
export interface LiteGraphCanvasEvent extends CustomEvent<CanvasEventDetail> {}
export interface LiteGraphCanvasEvent extends CustomEvent<CanvasEventDetail> { }
export interface LiteGraphCanvasGroupEvent
extends CustomEvent<{
subType: 'group-double-click'
export interface LiteGraphCanvasGroupEvent extends CustomEvent<{
subType: "group-double-click"
originalEvent: MouseEvent
group: LGraphGroup
}> {}
}> { }
/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */
export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
title?: string
type?: string
size?: Size
min_height?: number
slot_start_y?: number
widgets_info?: any
collapsable?: boolean
color?: string
bgcolor?: string
shape?: RenderShape
title_mode?: TitleMode
title_color?: string
title_text_color?: string
desc?: string
nodeData: any
new (): T
title?: string
type?: string
size?: Size
min_height?: number
slot_start_y?: number
widgets_info?: any
collapsable?: boolean
color?: string
bgcolor?: string
shape?: RenderShape
title_mode?: TitleMode
title_color?: string
title_text_color?: string
nodeData: any
new(): T
}
// End backwards compat

View File

@@ -1,5 +1,5 @@
import type { Point, ReadOnlyPoint, ReadOnlyRect } from './interfaces'
import { LinkDirection } from './types/globalEnums'
import type { Point, ReadOnlyPoint, ReadOnlyRect } from "./interfaces"
import { LinkDirection } from "./types/globalEnums"
/**
* Calculates the distance between two points (2D vector)
@@ -8,9 +8,9 @@ import { LinkDirection } from './types/globalEnums'
* @returns Distance between point {@link a} & {@link b}
*/
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])
)
return Math.sqrt(
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])
)
}
/**
@@ -21,9 +21,7 @@ export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
* @returns Distance2 (squared) between point {@link a} & {@link b}
*/
export function dist2(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
return (
(b[0] - a[0]) * (b[0] - a[0])) + ((b[1] - a[1]) * (b[1] - a[1])
)
return ((b[0] - a[0]) * (b[0] - a[0])) + ((b[1] - a[1]) * (b[1] - a[1]))
}
/**
@@ -33,12 +31,10 @@ export function dist2(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isPointInRectangle(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean {
return (
rect[0] < point[0] &&
rect[0] + rect[2] > point[0] &&
rect[1] < point[1] &&
rect[1] + rect[3] > point[1]
)
return rect[0] < point[0]
&& rect[0] + rect[2] > point[0]
&& rect[1] < point[1]
&& rect[1] + rect[3] > point[1]
}
/**
@@ -52,7 +48,10 @@ export function isPointInRectangle(point: ReadOnlyPoint, rect: ReadOnlyRect): bo
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isInsideRectangle(x: number, y: number, left: number, top: number, width: 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
}
/**
@@ -63,8 +62,8 @@ export function isInsideRectangle(x: number, y: number, left: number, top: numbe
* @returns `true` if the point is roughly inside the octagon centred on 0,0 with specified radius
*/
export function isSortaInsideOctagon(x: number, y: number, radius: number): boolean {
const sum = Math.min(radius, Math.abs(x)) + Math.min(radius, Math.abs(y))
return sum < radius * 0.75
const sum = Math.min(radius, Math.abs(x)) + Math.min(radius, Math.abs(y))
return sum < radius * 0.75
}
/**
@@ -74,17 +73,17 @@ export function isSortaInsideOctagon(x: number, y: number, radius: number): bool
* @returns `true` if rectangles overlap, otherwise `false`
*/
export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
const bBottom = b[1] + b[3]
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
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]
) ? false : true
return a[0] > bRight
|| a[1] > bBottom
|| aRight < b[0]
|| aBottom < b[1]
? false
: true
}
/**
@@ -94,9 +93,9 @@ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
* @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)
return isInsideRectangle(centreX, centreY, a[0], a[1], a[2], a[3])
const centreX = b[0] + (b[2] * 0.5)
const centreY = b[1] + (b[3] * 0.5)
return isInsideRectangle(centreX, centreY, a[0], a[1], a[2], a[3])
}
/**
@@ -106,17 +105,15 @@ export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
* @returns `true` if {@link a} wholly contains {@link b}, otherwise `false`
*/
export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
const bBottom = b[1] + b[3]
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
const bBottom = b[1] + b[3]
return (
a[0] < b[0] &&
a[1] < b[1] &&
aRight > bRight &&
aBottom > bBottom
)
return a[0] < b[0]
&& a[1] < b[1]
&& aRight > bRight
&& aBottom > bBottom
}
/**
@@ -126,85 +123,85 @@ export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
* @param out The {@link Point} to add the offset to
*/
export function addDirectionalOffset(amount: number, direction: LinkDirection, 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
// LinkDirection.CENTER: Nothing to do.
}
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
// LinkDirection.CENTER: Nothing to do.
}
}
/**
* Rotates an offset in 90° increments.
*
*
* Swaps/flips axis values of a 2D vector offset - effectively rotating {@link offset} by 90°
* @param offset The zero-based offset to rotate
* @param from Direction to rotate from
* @param to Direction to rotate to
*/
export function rotateLink(offset: Point, from: LinkDirection, to: LinkDirection): void {
let x: number
let y: number
let x: number
let y: number
// Normalise to left
switch (from) {
case to:
case LinkDirection.CENTER:
case LinkDirection.NONE:
// Nothing to do
return
// Normalise to left
switch (from) {
case to:
case LinkDirection.CENTER:
case LinkDirection.NONE:
// 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
// Apply new direction
switch (to) {
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
}
}
/**
@@ -217,13 +214,11 @@ export function rotateLink(offset: Point, from: LinkDirection, to: LinkDirection
* @returns 0 if all three points are in a straight line, a negative value if point is to the left of the projected line, or positive if the point is to the right
*/
export function getOrientation(lineStart: ReadOnlyPoint, lineEnd: ReadOnlyPoint, x: 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]))
}
/**
*
*
* @param out The array to store the point in
* @param a Start point
* @param b End point
@@ -232,20 +227,20 @@ export function getOrientation(lineStart: ReadOnlyPoint, lineEnd: ReadOnlyPoint,
* @param t Time: factor of distance to travel along the curve (e.g 0.25 is 25% along the curve)
*/
export function findPointOnCurve(
out: Point,
a: ReadOnlyPoint,
b: ReadOnlyPoint,
controlA: ReadOnlyPoint,
controlB: ReadOnlyPoint,
t: number = 0.5
out: Point,
a: ReadOnlyPoint,
b: ReadOnlyPoint,
controlA: ReadOnlyPoint,
controlB: ReadOnlyPoint,
t: number = 0.5,
): void {
const iT = 1 - t
const iT = 1 - t
const c1 = iT * iT * iT
const c2 = 3 * (iT * iT) * t
const c3 = 3 * iT * (t * t)
const c4 = t * t * t
const c1 = iT * iT * iT
const c2 = 3 * (iT * iT) * t
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])
}

View File

@@ -1,80 +1,85 @@
//API *************************************************
//like rect but rounded corners
declare global {
interface Window {
webkitRequestAnimationFrame?: (callback: FrameRequestCallback) => number
mozRequestAnimationFrame?: (callback: FrameRequestCallback) => number
}
}
export function loadPolyfills() {
if (typeof window != 'undefined' && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) {
if (typeof (window) != "undefined" && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) {
// @ts-expect-error Slightly broken polyfill - radius_low not impl. anywhere
window.CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, radius, radius_low) {
let top_left_radius = 0
let top_right_radius = 0
let bottom_left_radius = 0
let bottom_right_radius = 0
window.CanvasRenderingContext2D.prototype.roundRect = function (
x,
y,
w,
h,
radius,
radius_low
) {
let top_left_radius = 0;
let top_right_radius = 0;
let bottom_left_radius = 0;
let bottom_right_radius = 0;
if (radius === 0) {
this.rect(x, y, w, h)
return
}
if (radius === 0) {
this.rect(x, y, w, h);
return;
}
if (radius_low === undefined) {
radius_low = radius
}
if (radius_low === undefined)
radius_low = radius;
//make it compatible with official one
if (radius != null && radius.constructor === Array) {
if (radius.length == 1) {
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]
} else if (radius.length == 4) {
top_left_radius = radius[0]
top_right_radius = radius[1]
bottom_left_radius = radius[2]
bottom_right_radius = radius[3]
} else return
} //old using numbers
else {
top_left_radius = radius || 0
top_right_radius = radius || 0
bottom_left_radius = radius_low || 0
bottom_right_radius = radius_low || 0
}
//make it compatible with official one
if (radius != null && radius.constructor === Array) {
if (radius.length == 1)
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];
}
else if (radius.length == 4) {
top_left_radius = radius[0];
top_right_radius = radius[1];
bottom_left_radius = radius[2];
bottom_right_radius = radius[3];
}
else
return;
}
else //old using numbers
{
top_left_radius = radius || 0;
top_right_radius = radius || 0;
bottom_left_radius = radius_low || 0;
bottom_right_radius = radius_low || 0;
}
//top right
this.moveTo(x + top_left_radius, y)
this.lineTo(x + w - top_right_radius, y)
this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius)
//top right
this.moveTo(x + top_left_radius, y);
this.lineTo(x + w - top_right_radius, y);
this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius);
//bottom right
this.lineTo(x + w, y + h - bottom_right_radius)
this.quadraticCurveTo(x + w, y + h, x + w - bottom_right_radius, y + h)
//bottom right
this.lineTo(x + w, y + h - bottom_right_radius);
this.quadraticCurveTo(
x + w,
y + h,
x + w - bottom_right_radius,
y + h
);
//bottom left
this.lineTo(x + bottom_right_radius, y + h)
this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius)
//bottom left
this.lineTo(x + bottom_right_radius, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius);
//top left
this.lineTo(x, y + bottom_left_radius)
this.quadraticCurveTo(x, y, x + top_left_radius, y)
}
} //if
//top left
this.lineTo(x, y + bottom_left_radius);
this.quadraticCurveTo(x, y, x + top_left_radius, y);
};
}//if
if (typeof window != 'undefined' && !window['requestAnimationFrame']) {
const RAF = (
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60)
}
) as typeof window.requestAnimationFrame
window.requestAnimationFrame = RAF
}
if (typeof window != "undefined" && !window["requestAnimationFrame"]) {
window.requestAnimationFrame =
// @ts-expect-error Legacy code
window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
}
}

View File

@@ -4,7 +4,7 @@
* @returns String(value) or null
*/
export function stringOrNull(value: unknown): string | null {
return value == null ? null : String(value)
return value == null ? null : String(value)
}
/**
@@ -13,5 +13,5 @@ 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)
}

View File

@@ -2,102 +2,93 @@
* Event interfaces for event extension
*/
import type { ConnectingLink, LinkReleaseContextExtended } from '@/litegraph'
import type { IWidget } from '@/types/widgets'
import type { LGraphNode } from '@/LGraphNode'
import type { LGraphGroup } from '@/LGraphGroup'
import type { ConnectingLink, LinkReleaseContextExtended } from "@/litegraph"
import type { IWidget } from "@/types/widgets"
import type { LGraphNode } from "@/LGraphNode"
import type { LGraphGroup } from "@/LGraphGroup"
/** For Canvas*Event - adds graph space co-ordinates (property names are shipped) */
export interface ICanvasPosition {
/** X co-ordinate of the event, in graph space (NOT canvas space) */
canvasX?: number
/** Y co-ordinate of the event, in graph space (NOT canvas space) */
canvasY?: number
/** X co-ordinate of the event, in graph space (NOT canvas space) */
canvasX?: number
/** Y co-ordinate of the event, in graph space (NOT canvas space) */
canvasY?: number
}
/** For Canvas*Event */
export interface IDeltaPosition {
deltaX?: number
deltaY?: number
deltaX?: number
deltaY?: number
}
/** PointerEvent with canvasX/Y and deltaX/Y properties */
export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent {}
export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent { }
/** MouseEvent with canvasX/Y and deltaX/Y properties */
export interface CanvasMouseEvent
extends MouseEvent,
ICanvasPosition,
IDeltaPosition {
/** @deprecated Part of DragAndScale mouse API - incomplete / not maintained */
dragging?: boolean
click_time?: number
dataTransfer?: unknown
export interface CanvasMouseEvent extends MouseEvent, ICanvasPosition, IDeltaPosition {
/** @deprecated Part of DragAndScale mouse API - incomplete / not maintained */
dragging?: boolean
click_time?: number
dataTransfer?: unknown
}
/** WheelEvent with canvasX/Y properties */
export interface CanvasWheelEvent extends WheelEvent, ICanvasPosition {
dragging?: boolean
click_time?: number
dataTransfer?: unknown
dragging?: boolean
click_time?: number
dataTransfer?: unknown
}
/** DragEvent with canvasX/Y and deltaX/Y properties */
export interface CanvasDragEvent
extends DragEvent,
ICanvasPosition,
IDeltaPosition {}
export interface CanvasDragEvent extends DragEvent, ICanvasPosition, IDeltaPosition { }
/** TouchEvent with canvasX/Y and deltaX/Y properties */
export interface CanvasTouchEvent
extends TouchEvent,
ICanvasPosition,
IDeltaPosition {}
export interface CanvasTouchEvent extends TouchEvent, ICanvasPosition, IDeltaPosition { }
export type CanvasEventDetail =
| GenericEventDetail
| DragggingCanvasEventDetail
| ReadOnlyEventDetail
| GroupDoubleClickEventDetail
| EmptyDoubleClickEventDetail
| ConnectingWidgetLinkEventDetail
| EmptyReleaseEventDetail
GenericEventDetail
| DragggingCanvasEventDetail
| ReadOnlyEventDetail
| GroupDoubleClickEventDetail
| EmptyDoubleClickEventDetail
| ConnectingWidgetLinkEventDetail
| EmptyReleaseEventDetail
export interface GenericEventDetail {
subType: 'before-change' | 'after-change'
subType: "before-change" | "after-change"
}
export interface OriginalEvent {
originalEvent: CanvasPointerEvent
originalEvent: CanvasPointerEvent,
}
export interface EmptyReleaseEventDetail extends OriginalEvent {
subType: 'empty-release'
linkReleaseContext: LinkReleaseContextExtended
subType: "empty-release",
linkReleaseContext: LinkReleaseContextExtended,
}
export interface ConnectingWidgetLinkEventDetail {
subType: 'connectingWidgetLink'
link: ConnectingLink
node: LGraphNode
widget: IWidget
subType: "connectingWidgetLink"
link: ConnectingLink
node: LGraphNode
widget: IWidget
}
export interface EmptyDoubleClickEventDetail extends OriginalEvent {
subType: 'empty-double-click'
subType: "empty-double-click"
}
export interface GroupDoubleClickEventDetail extends OriginalEvent {
subType: 'group-double-click'
group: LGraphGroup
subType: "group-double-click"
group: LGraphGroup
}
export interface DragggingCanvasEventDetail {
subType: 'dragging-canvas'
draggingCanvas: boolean
subType: "dragging-canvas"
draggingCanvas: boolean
}
export interface ReadOnlyEventDetail {
subType: 'read-only'
readOnly: boolean
subType: "read-only"
readOnly: boolean
}

View File

@@ -1,53 +1,53 @@
/** Node slot type - input or output */
export enum NodeSlotType {
INPUT = 1,
OUTPUT = 2,
INPUT = 1,
OUTPUT = 2,
}
/** Shape that an object will render as - used by nodes and slots */
export enum RenderShape {
BOX = 1,
ROUND = 2,
CIRCLE = 3,
CARD = 4,
ARROW = 5,
/** intended for slot arrays */
GRID = 6,
HollowCircle = 7,
BOX = 1,
ROUND = 2,
CIRCLE = 3,
CARD = 4,
ARROW = 5,
/** intended for slot arrays */
GRID = 6,
HollowCircle = 7,
}
/** The direction that a link point will flow towards - e.g. horizontal outputs are right by default */
export enum LinkDirection {
NONE = 0,
UP = 1,
DOWN = 2,
LEFT = 3,
RIGHT = 4,
CENTER = 5,
NONE = 0,
UP = 1,
DOWN = 2,
LEFT = 3,
RIGHT = 4,
CENTER = 5,
}
/** The path calculation that links follow */
export enum LinkRenderType {
HIDDEN_LINK = -1,
/** Juts out from the input & output a little @see LinkDirection, then a straight line between them */
STRAIGHT_LINK = 0,
/** 90° angles, clean and box-like */
LINEAR_LINK = 1,
/** Smooth curved links - default */
SPLINE_LINK = 2,
HIDDEN_LINK = -1,
/** Juts out from the input & output a little @see LinkDirection, then a straight line between them */
STRAIGHT_LINK = 0,
/** 90° angles, clean and box-like */
LINEAR_LINK = 1,
/** Smooth curved links - default */
SPLINE_LINK = 2,
}
export enum TitleMode {
NORMAL_TITLE = 0,
NO_TITLE = 1,
TRANSPARENT_TITLE = 2,
AUTOHIDE_TITLE = 3,
NORMAL_TITLE = 0,
NO_TITLE = 1,
TRANSPARENT_TITLE = 2,
AUTOHIDE_TITLE = 3,
}
export enum LGraphEventMode {
ALWAYS = 0,
ON_EVENT = 1,
NEVER = 2,
ON_TRIGGER = 3,
BYPASS = 4,
ALWAYS = 0,
ON_EVENT = 1,
NEVER = 2,
ON_TRIGGER = 3,
BYPASS = 4,
}

View File

@@ -1,104 +1,89 @@
import type {
ISlotType,
Dictionary,
INodeFlags,
INodeInputSlot,
INodeOutputSlot,
Point,
Rect,
Size,
} from '@/interfaces'
import type { LGraph } from '@/LGraph'
import type { IGraphGroupFlags, LGraphGroup } from '@/LGraphGroup'
import type { LGraphNode, NodeId } from '@/LGraphNode'
import type { LiteGraph } from '@/litegraph'
import type { LinkId, LLink } from '@/LLink'
import type { TWidgetValue } from '@/types/widgets'
import { RenderShape } from './globalEnums'
import type { ISlotType, Dictionary, INodeFlags, INodeInputSlot, INodeOutputSlot, Point, Rect, Size } from "@/interfaces"
import type { LGraph } from "@/LGraph"
import type { IGraphGroupFlags, LGraphGroup } from "@/LGraphGroup"
import type { LGraphNode, NodeId } from "@/LGraphNode"
import type { LiteGraph } from "@/litegraph"
import type { LinkId, LLink } from "@/LLink"
import type { TWidgetValue } from "@/types/widgets"
import { RenderShape } from "./globalEnums"
/**
* An object that implements custom pre-serialization logic via {@link Serialisable.asSerialisable}.
*/
export interface Serialisable<SerialisableObject> {
/**
* Prepares this object for serialization.
* Creates a partial shallow copy of itself, with only the properties that should be serialised.
* @returns An object that can immediately be serialized to JSON.
*/
asSerialisable(): SerialisableObject
/**
* Prepares this object for serialization.
* Creates a partial shallow copy of itself, with only the properties that should be serialised.
* @returns An object that can immediately be serialized to JSON.
*/
asSerialisable(): SerialisableObject
}
/** Serialised LGraphNode */
export interface ISerialisedNode {
title?: string
id: NodeId
type?: string
pos?: Point
size?: Size
flags?: INodeFlags
order?: number
mode?: number
outputs?: INodeOutputSlot[]
inputs?: INodeInputSlot[]
properties?: Dictionary<unknown>
shape?: RenderShape
boxcolor?: string
color?: string
bgcolor?: string
showAdvanced?: boolean
widgets_values?: TWidgetValue[]
title?: string
id: NodeId
type?: string
pos?: Point
size?: Size
flags?: INodeFlags
order?: number
mode?: number
outputs?: INodeOutputSlot[]
inputs?: INodeInputSlot[]
properties?: Dictionary<unknown>
shape?: RenderShape
boxcolor?: string
color?: string
bgcolor?: string
showAdvanced?: boolean
widgets_values?: TWidgetValue[]
}
/** Contains serialised graph elements */
export type ISerialisedGraph<
TNode = ReturnType<LGraphNode['serialize']>,
TLink = ReturnType<LLink['serialize']>,
TGroup = ReturnType<LGraphGroup['serialize']>,
TNode = ReturnType<LGraphNode["serialize"]>,
TLink = ReturnType<LLink["serialize"]>,
TGroup = ReturnType<LGraphGroup["serialize"]>
> = {
last_node_id: LGraph['last_node_id']
last_link_id: LGraph['last_link_id']
last_reroute_id?: LGraph['last_reroute_id']
nodes: TNode[]
links: TLink[]
groups: TGroup[]
config: LGraph['config']
version: typeof LiteGraph.VERSION
extra?: unknown
last_node_id: LGraph["last_node_id"]
last_link_id: LGraph["last_link_id"]
last_reroute_id?: LGraph["last_reroute_id"]
nodes: TNode[]
links: TLink[]
groups: TGroup[]
config: LGraph["config"]
version: typeof LiteGraph.VERSION
extra?: unknown
}
/** Serialised LGraphGroup */
export interface ISerialisedGroup {
title: string
bounding: number[]
color: string
font_size: number
flags?: IGraphGroupFlags
title: string
bounding: number[]
color: string
font_size: number
flags?: IGraphGroupFlags
}
export type TClipboardLink = [
targetRelativeIndex: number,
originSlot: number,
nodeRelativeIndex: number,
targetSlot: number,
targetNodeId: NodeId,
]
export type TClipboardLink = [targetRelativeIndex: number, originSlot: number, nodeRelativeIndex: number, targetSlot: number, targetNodeId: NodeId]
/** */
export interface IClipboardContents {
nodes?: ISerialisedNode[]
links?: TClipboardLink[]
nodes?: ISerialisedNode[]
links?: TClipboardLink[]
}
export interface SerialisableLLink {
/** Link ID */
id: LinkId
/** Output node ID */
origin_id: NodeId
/** Output slot index */
origin_slot: number
/** Input node ID */
target_id: NodeId
/** Input slot index */
target_slot: number
/** Data type of the link */
type: ISlotType
/** Link ID */
id: LinkId
/** Output node ID */
origin_id: NodeId
/** Output slot index */
origin_slot: number
/** Input node ID */
target_id: NodeId
/** Input slot index */
target_slot: number
/** Data type of the link */
type: ISlotType
}

View File

@@ -1,29 +1,28 @@
import { CanvasColour, Point, Size } from '@/interfaces'
import type { LGraphCanvas, LGraphNode } from '@/litegraph'
import type { CanvasMouseEvent } from './events'
import { CanvasColour, Point, Size } from "@/interfaces"
import type { LGraphCanvas, LGraphNode } from "@/litegraph"
import type { CanvasMouseEvent } from "./events"
export interface IWidgetOptions<TValue = unknown>
extends Record<string, unknown> {
on?: string
off?: string
max?: number
min?: number
slider_color?: CanvasColour
marker_color?: CanvasColour
precision?: number
read_only?: boolean
step?: number
y?: number
multiline?: boolean
// TODO: Confirm this
property?: string
export interface IWidgetOptions<TValue = unknown> extends Record<string, unknown> {
on?: string
off?: string
max?: number
min?: number
slider_color?: CanvasColour
marker_color?: CanvasColour
precision?: number
read_only?: boolean
step?: number
y?: number
multiline?: boolean
// TODO: Confirm this
property?: string
hasOwnProperty?(arg0: string): any
// values?(widget?: IWidget, node?: LGraphNode): any
values?: TValue[]
callback?: IWidget['callback']
hasOwnProperty?(arg0: string): any
// values?(widget?: IWidget, node?: LGraphNode): any
values?: TValue[]
callback?: IWidget["callback"]
onHide?(widget: IWidget): void
onHide?(widget: IWidget): void
}
/**
@@ -35,116 +34,94 @@ export interface IWidgetOptions<TValue = unknown>
* Recommend declaration merging any properties that use IWidget (e.g. {@link LGraphNode.widgets}) with a new type alias.
* @see ICustomWidget
*/
export type IWidget =
| IBooleanWidget
| INumericWidget
| IStringWidget
| IMultilineStringWidget
| IComboWidget
| ICustomWidget
export type IWidget = IBooleanWidget | INumericWidget | IStringWidget | IMultilineStringWidget | IComboWidget | ICustomWidget
export interface IBooleanWidget extends IBaseWidget {
type?: 'toggle'
value: boolean
type?: "toggle"
value: boolean
}
/** Any widget that uses a numeric backing */
export interface INumericWidget extends IBaseWidget {
type?: 'slider' | 'number'
value: number
type?: "slider" | "number"
value: number
}
/** A combo-box widget (dropdown, select, etc) */
export interface IComboWidget extends IBaseWidget {
type?: 'combo'
value: string | number
options: IWidgetOptions<string>
type?: "combo"
value: string | number
options: IWidgetOptions<string>
}
export type IStringWidgetType =
| IStringWidget['type']
| IMultilineStringWidget['type']
export type IStringWidgetType = IStringWidget["type"] | IMultilineStringWidget["type"]
/** A widget with a string value */
export interface IStringWidget extends IBaseWidget {
type?: 'string' | 'text' | 'button'
value: string
type?: "string" | "text" | "button"
value: string
}
/** A widget with a string value and a multiline text input */
export interface IMultilineStringWidget<
TElement extends HTMLElement = HTMLTextAreaElement,
> extends IBaseWidget {
type?: 'multiline'
value: string
export interface IMultilineStringWidget<TElement extends HTMLElement = HTMLTextAreaElement> extends IBaseWidget {
type?: "multiline"
value: string
/** HTML textarea element */
element?: TElement
/** HTML textarea element */
element?: TElement
}
/** A custom widget - accepts any value and has no built-in special handling */
export interface ICustomWidget<TElement extends HTMLElement = HTMLElement>
extends IBaseWidget<TElement> {
type?: 'custom'
value: string | object
export interface ICustomWidget<TElement extends HTMLElement = HTMLElement> extends IBaseWidget<TElement> {
type?: "custom"
value: string | object
element?: TElement
element?: TElement
}
/**
* Valid widget types. TS cannot provide easily extensible type safety for this at present.
* 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.
* @see IWidget
*/
export interface IBaseWidget<TElement extends HTMLElement = HTMLElement> {
linkedWidgets?: IWidget[]
linkedWidgets?: IWidget[]
options: IWidgetOptions
marker?: number
label?: string
clicked?: boolean
name?: string
/** Widget type (see {@link TWidgetType}) */
type?: TWidgetType
value?: TWidgetValue
y?: number
last_y?: number
width?: number
disabled?: boolean
options: IWidgetOptions
marker?: number
label?: string
clicked?: boolean
name?: string
/** Widget type (see {@link TWidgetType}) */
type?: TWidgetType
value?: TWidgetValue
y?: number
last_y?: number
width?: number
disabled?: boolean
hidden?: boolean
advanced?: boolean
hidden?: boolean
advanced?: boolean
tooltip?: string
tooltip?: string
/** HTML widget element */
element?: TElement
/** HTML widget element */
element?: TElement
// TODO: Confirm this format
callback?(value: any, canvas?: LGraphCanvas, node?: LGraphNode, pos?: Point, e?: CanvasMouseEvent): void
onRemove?(): void
beforeQueued?(): void
// TODO: Confirm this format
callback?(
value: any,
canvas?: LGraphCanvas,
node?: LGraphNode,
pos?: Point,
e?: CanvasMouseEvent,
): void
onRemove?(): void
beforeQueued?(): void
mouse?(event: CanvasMouseEvent, arg1: number[], node: LGraphNode): boolean
draw?(
ctx: CanvasRenderingContext2D,
node: LGraphNode,
widget_width: number,
y: number,
H: number,
): void
computeSize?(width: number): Size
mouse?(event: CanvasMouseEvent, arg1: number[], node: LGraphNode): boolean
draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, widget_width: number, y: number, H: number): void
computeSize?(width: number): Size
}

View File

@@ -1,5 +1,5 @@
import type { Dictionary, Direction, IBoundaryNodes } from '@/interfaces'
import type { LGraphNode } from '@/LGraphNode'
import type { Dictionary, Direction, IBoundaryNodes } from "@/interfaces"
import type { LGraphNode } from "@/LGraphNode"
/**
* Finds the nodes that are farthest in all four directions, representing the boundary of the nodes.
@@ -7,31 +7,31 @@ import type { LGraphNode } from '@/LGraphNode'
* @returns An object listing the furthest node (edge) in all four directions. `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)
if (!valid) return null
const valid = nodes?.find(x => x)
if (!valid) return null
let top = valid
let right = valid
let bottom = valid
let left = valid
let top = valid
let right = valid
let bottom = valid
let left = valid
for (const node of nodes) {
if (!node) continue
const [x, y] = node.pos
const [width, height] = node.size
for (const node of nodes) {
if (!node) continue
const [x, y] = node.pos
const [width, height] = node.size
if (y < top.pos[1]) top = node
if (x + width > right.pos[0] + right.size[0]) right = node
if (y + height > bottom.pos[1] + bottom.size[1]) bottom = node
if (x < left.pos[0]) left = node
}
if (y < top.pos[1]) top = node
if (x + width > right.pos[0] + right.size[0]) right = node
if (y + height > bottom.pos[1] + bottom.size[1]) bottom = node
if (x < left.pos[0]) left = node
}
return {
top,
right,
bottom,
left,
}
return {
top,
right,
bottom,
left
}
}
/**
@@ -40,30 +40,30 @@ export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null {
* @param horizontal If true, distributes along the horizontal plane. Otherwise, the vertical plane.
*/
export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void {
const nodeCount = nodes?.length
if (!(nodeCount > 1)) return
const nodeCount = nodes?.length
if (!(nodeCount > 1)) return
const index = horizontal ? 0 : 1
const index = horizontal ? 0 : 1
let total = 0
let highest = -Infinity
let total = 0
let highest = -Infinity
for (const node of nodes) {
total += node.size[index]
for (const node of nodes) {
total += node.size[index]
const high = node.pos[index] + node.size[index]
if (high > highest) highest = high
}
const sorted = [...nodes].sort((a, b) => a.pos[index] - b.pos[index])
const lowest = sorted[0].pos[index]
const high = node.pos[index] + node.size[index]
if (high > highest) highest = high
}
const sorted = [...nodes].sort((a, b) => a.pos[index] - b.pos[index])
const lowest = sorted[0].pos[index]
const gap = (highest - lowest - total) / (nodeCount - 1)
let startAt = lowest
for (let i = 0; i < nodeCount; i++) {
const node = sorted[i]
node.pos[index] = startAt + gap * i
startAt += node.size[index]
}
const gap = ((highest - lowest) - total) / (nodeCount - 1)
let startAt = lowest
for (let i = 0; i < nodeCount; i++) {
const node = sorted[i]
node.pos[index] = startAt + (gap * i)
startAt += node.size[index]
}
}
/**
@@ -73,34 +73,33 @@ export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void
* @param align_to The node to align all other nodes to. If undefined, the farthest node will be used.
*/
export function alignNodes(nodes: LGraphNode[], direction: Direction, align_to?: LGraphNode): void {
if (!nodes) return
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
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
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
}
}
}
}

View File

@@ -1,44 +1,44 @@
import { LGraph, LGraphGroup, LGraphNode, LiteGraph } from '../src/litegraph'
import { LiteGraphGlobal } from '../src/LiteGraphGlobal'
import { LGraph, LGraphGroup, LGraphNode, LiteGraph } from "../src/litegraph"
import { LiteGraphGlobal } from "../src/LiteGraphGlobal"
function makeGraph() {
const LiteGraph = new LiteGraphGlobal()
LiteGraph.registerNodeType('TestNode', LGraphNode)
LiteGraph.registerNodeType('OtherNode', LGraphNode)
LiteGraph.registerNodeType('', LGraphNode)
LiteGraph.registerNodeType("TestNode", LGraphNode)
LiteGraph.registerNodeType("OtherNode", LGraphNode)
LiteGraph.registerNodeType("", LGraphNode)
return new LGraph()
}
describe('LGraph', () => {
it('can be instantiated', () => {
describe("LGraph", () => {
it("can be instantiated", () => {
// @ts-ignore TODO: Remove once relative imports fix goes in.
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")
})
})
describe('Legacy LGraph Compatibility Layer', () => {
it('can be extended via prototype', () => {
describe("Legacy LGraph Compatibility Layer", () => {
it("can be extended via prototype", () => {
const graph = new LGraph()
// @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(graph.newMethod()).toBe('New method added via prototype')
expect(graph.newMethod()).toBe("New method added via prototype")
})
it('is correctly assigned to LiteGraph', () => {
it("is correctly assigned to LiteGraph", () => {
expect(LiteGraph.LGraph).toBe(LGraph)
})
})
describe('LGraph Serialisation', () => {
it('should serialise', () => {
describe("LGraph Serialisation", () => {
it("should serialise", () => {
const graph = new LGraph()
graph.add(new LGraphNode('Test Node'))
graph.add(new LGraphGroup('Test Group'))
graph.add(new LGraphNode("Test Node"))
graph.add(new LGraphGroup("Test Group"))
expect(graph.nodes.length).toBe(1)
expect(graph.groups.length).toBe(1)
})

View File

@@ -1,10 +1,13 @@
import { LGraphNode } from '../src/litegraph'
import {
LGraphNode,
} from "../src/litegraph"
describe('LGraphNode', () => {
it('should serialize position correctly', () => {
const node = new LGraphNode('TestNode')
describe("LGraphNode", () => {
it("should serialize position correctly", () => {
const node = new LGraphNode("TestNode")
node.pos = [10, 10]
expect(node.pos).toEqual(new Float32Array([10, 10]))
expect(node.serialize().pos).toEqual(new Float32Array([10, 10]))
})
})
})