diff --git a/src/LGraph.ts b/src/LGraph.ts index a731733a1..e3e76004a 100644 --- a/src/LGraph.ts +++ b/src/LGraph.ts @@ -94,6 +94,7 @@ export class LGraph { onSerialize?(data: ISerialisedGraph): void onConfigure?(data: ISerialisedGraph): void onGetNodeMenuOptions?(options: IContextMenuValue[], node: LGraphNode): void + onNodeConnectionChange?(nodeSlotType: ISlotType, targetNode: LGraphNode, slotIndex: number, sourceNode?: LGraphNode, sourceSlotIndex?: number): void private _input_nodes?: LGraphNode[] diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index 90f792085..e380b04af 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -1,15 +1,15 @@ -// @ts-nocheck import type { Dictionary, IContextMenuValue, IFoundSlot, INodeFlags, INodeInputSlot, INodeOutputSlot, IOptionalInputsData, ISlotType, Point, Rect, Size } from "./interfaces" import type { LGraph } from "./LGraph" -import type { IWidget } from "./types/widgets" +import type { IWidget, TWidgetValue } from "./types/widgets" import type { ISerialisedNode } from "./types/serialisation" import type { RenderShape } from "./types/globalEnums" import type { LGraphCanvas } from "./LGraphCanvas" import type { CanvasMouseEvent } from "./types/events" -import { BadgePosition, LGraphBadge } from "./LGraphBadge"; -import { LiteGraph } from "./litegraph"; -import { isInsideRectangle } from "./measure"; -import { LLink } from "./LLink"; +import type { DragAndScale } from "./DragAndScale" +import { BadgePosition, LGraphBadge } from "./LGraphBadge" +import { type LGraphNodeConstructor, LiteGraph } from "./litegraph" +import { isInsideRectangle } from "./measure" +import { LLink } from "./LLink" export type NodeId = number | string @@ -28,9 +28,6 @@ interface IMouseOverData { outputId: number } -// ************************************************************* -// Node CLASS ******* -// ************************************************************* /* title: string pos: [x,y] @@ -82,12 +79,15 @@ supported callbacks: + onAction: action slot triggered + getExtraMenuOptions: to add option to context menu */ + +export interface LGraphNode { + constructor?: LGraphNodeConstructor +} + /** * Base Class for all the node type classes - * @class LGraphNode * @param {String} name a name for the node */ - export class LGraphNode { // Static properties used by dynamic child classes static title?: string @@ -124,7 +124,7 @@ export class LGraphNode { color: string bgcolor: string boxcolor: string - shape?: Rect + shape?: RenderShape exec_version: number action_call?: string execute_triggered: number @@ -184,7 +184,7 @@ export class LGraphNode { * @returns {number | null} If a number is returned, the connection will be made to that input index. * If an invalid index or non-number (false, null, NaN etc) is returned, the connection will be cancelled. */ - onBeforeConnectInput?(this: LGraphNode, target_slot: number, requested_slot: number | string): number | false | null + onBeforeConnectInput?(this: LGraphNode, target_slot: number, requested_slot?: number | string): number | false | null onShowCustomPanelInfo?(this: LGraphNode, panel: any): void onAddPropertyToPanel?(this: LGraphNode, pName: string, panel: any): boolean onWidgetChanged?(this: LGraphNode, name: string, value: unknown, old_value: unknown, w: IWidget): void @@ -231,155 +231,140 @@ export class LGraphNode { isValidWidgetLink?(slot_index: number, node: LGraphNode, overWidget: IWidget): boolean | undefined constructor(title: string) { - this._ctor(title); + this._ctor(title) } _ctor(title: string): void { - this.title = title || "Unnamed"; - this.size = [LiteGraph.NODE_WIDTH, 60]; - this.graph = null; + this.title = title || "Unnamed" + this.size = [LiteGraph.NODE_WIDTH, 60] + this.graph = null // Initialize _pos with a Float32Array of length 2, default value [10, 10] - this._pos = new Float32Array([10, 10]); + this._pos = new Float32Array([10, 10]) Object.defineProperty(this, "pos", { set: function (v) { if (!v || v.length < 2) { - return; + return } - this._pos[0] = v[0]; - this._pos[1] = v[1]; + this._pos[0] = v[0] + this._pos[1] = v[1] }, get: function () { - return this._pos; + return this._pos }, enumerable: true - }); + }) if (LiteGraph.use_uuids) { - this.id = LiteGraph.uuidv4(); + this.id = LiteGraph.uuidv4() } else { - this.id = -1; //not know till not added + this.id = -1 //not know till not added } - this.type = null; + this.type = null //inputs available: array of inputs - this.inputs = []; - this.outputs = []; - this.connections = []; - this.badges = []; - this.badgePosition = BadgePosition.TopLeft; + this.inputs = [] + this.outputs = [] + this.connections = [] + this.badges = [] + this.badgePosition = BadgePosition.TopLeft //local data - this.properties = {}; //for the values - this.properties_info = []; //for the info + this.properties = {} //for the values + this.properties_info = [] //for the info - this.flags = {}; + this.flags = {} } /** - * configure a node from an object containing the serialized info - * @method configure - */ + * configure a node from an object containing the serialized info + */ configure(info: ISerialisedNode): void { if (this.graph) { - this.graph._version++; + this.graph._version++ } - for (var j in info) { + for (const j in info) { if (j == "properties") { //i don't want to clone properties, I want to reuse the old container - for (var k in info.properties) { - this.properties[k] = info.properties[k]; - if (this.onPropertyChanged) { - this.onPropertyChanged(k, info.properties[k]); - } + for (const k in info.properties) { + this.properties[k] = info.properties[k] + this.onPropertyChanged?.(k, info.properties[k]) } - continue; + continue } if (info[j] == null) { - continue; + continue } else if (typeof info[j] == "object") { //object - if (this[j] && this[j].configure) { - this[j].configure(info[j]); + if (this[j]?.configure) { + this[j]?.configure(info[j]) } else { - this[j] = LiteGraph.cloneObject(info[j], this[j]); + this[j] = LiteGraph.cloneObject(info[j], this[j]) } } //value else { - this[j] = info[j]; + this[j] = info[j] } } if (!info.title) { - this.title = this.constructor.title; + this.title = this.constructor.title } if (this.inputs) { - for (var i = 0; i < this.inputs.length; ++i) { - var input = this.inputs[i]; - var link_info = this.graph ? this.graph.links[input.link] : null; - if (this.onConnectionsChange) - this.onConnectionsChange(LiteGraph.INPUT, i, true, link_info, input); //link_info has been created now, so its updated - - if (this.onInputAdded) - this.onInputAdded(input); - + for (let i = 0; i < this.inputs.length; ++i) { + const input = this.inputs[i] + const link = this.graph ? this.graph.links[input.link] : null + this.onConnectionsChange?.(LiteGraph.INPUT, i, true, link, input) + this.onInputAdded?.(input) } } if (this.outputs) { - for (var i = 0; i < this.outputs.length; ++i) { - var output = this.outputs[i]; + for (let i = 0; i < this.outputs.length; ++i) { + const output = this.outputs[i] if (!output.links) { - continue; + continue } - for (var j = 0; j < output.links.length; ++j) { - var link_info = this.graph ? this.graph.links[output.links[j]] : null; - if (this.onConnectionsChange) - this.onConnectionsChange(LiteGraph.OUTPUT, i, true, link_info, output); //link_info has been created now, so its updated + for (let j = 0; j < output.links.length; ++j) { + const link = this.graph ? this.graph.links[output.links[j]] : null + this.onConnectionsChange?.(LiteGraph.OUTPUT, i, true, link, output) } - - if (this.onOutputAdded) - this.onOutputAdded(output); + this.onOutputAdded?.(output) } } if (this.widgets) { - for (var i = 0; i < this.widgets.length; ++i) { - var w = this.widgets[i]; + for (let i = 0; i < this.widgets.length; ++i) { + const w = this.widgets[i] if (!w) - continue; - if (w.options && w.options.property && (this.properties[w.options.property] != undefined)) - w.value = JSON.parse(JSON.stringify(this.properties[w.options.property])); + continue + if (w.options?.property && (this.properties[w.options.property] != undefined)) + w.value = JSON.parse(JSON.stringify(this.properties[w.options.property])) } if (info.widgets_values) { - for (var i = 0; i < info.widgets_values.length; ++i) { + for (let i = 0; i < info.widgets_values.length; ++i) { if (this.widgets[i]) { - this.widgets[i].value = info.widgets_values[i]; + this.widgets[i].value = info.widgets_values[i] } } } } // Sync the state of this.resizable. - if (this.pinned) { - this.pin(true); - } + if (this.pinned) this.pin(true) - if (this.onConfigure) { - this.onConfigure(info); - } + this.onConfigure?.(info) } /** - * serialize the content - * @method serialize - */ + * serialize the content + */ serialize(): ISerialisedNode { //create serialization object - var o = { + const o: ISerialisedNode = { id: this.id, type: this.type, pos: this.pos, @@ -387,533 +372,417 @@ export class LGraphNode { flags: LiteGraph.cloneObject(this.flags), order: this.order, mode: this.mode - }; + } //special case for when there were errors - if (this.constructor === LGraphNode && this.last_serialization) { - return this.last_serialization; - } + if (this.constructor === LGraphNode && this.last_serialization) + return this.last_serialization - if (this.inputs) { - o.inputs = this.inputs; - } + if (this.inputs) o.inputs = this.inputs if (this.outputs) { //clear outputs last data (because data in connections is never serialized but stored inside the outputs info) - for (var i = 0; i < this.outputs.length; i++) { - delete this.outputs[i]._data; + for (let i = 0; i < this.outputs.length; i++) { + delete this.outputs[i]._data } - o.outputs = this.outputs; + o.outputs = this.outputs } - if (this.title && this.title != this.constructor.title) { - o.title = this.title; - } + if (this.title && this.title != this.constructor.title) o.title = this.title - if (this.properties) { - o.properties = LiteGraph.cloneObject(this.properties); - } + if (this.properties) o.properties = LiteGraph.cloneObject(this.properties) if (this.widgets && this.serialize_widgets) { - o.widgets_values = []; - for (var i = 0; i < this.widgets.length; ++i) { + o.widgets_values = [] + for (let i = 0; i < this.widgets.length; ++i) { if (this.widgets[i]) - o.widgets_values[i] = this.widgets[i].value; - - + o.widgets_values[i] = this.widgets[i].value else - o.widgets_values[i] = null; + o.widgets_values[i] = null } } - if (!o.type) { - o.type = this.constructor.type; - } + if (!o.type) o.type = this.constructor.type - if (this.color) { - o.color = this.color; - } - if (this.bgcolor) { - o.bgcolor = this.bgcolor; - } - if (this.boxcolor) { - o.boxcolor = this.boxcolor; - } - if (this.shape) { - o.shape = this.shape; - } + if (this.color) o.color = this.color + if (this.bgcolor) o.bgcolor = this.bgcolor + if (this.boxcolor) o.boxcolor = this.boxcolor + if (this.shape) o.shape = this.shape - if (this.onSerialize) { - if (this.onSerialize(o)) { - console.warn( - "node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter" - ); - } - } + if (this.onSerialize?.(o)) console.warn("node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter") - return o; + return o } /* Creates a clone of this node */ clone(): LGraphNode { - var node = LiteGraph.createNode(this.type); - if (!node) { - return null; - } + const node = LiteGraph.createNode(this.type) + if (!node) return null //we clone it because serialize returns shared containers - var data = LiteGraph.cloneObject(this.serialize()); + const data = LiteGraph.cloneObject(this.serialize()) //remove links if (data.inputs) { - for (var i = 0; i < data.inputs.length; ++i) { - data.inputs[i].link = null; + for (let i = 0; i < data.inputs.length; ++i) { + data.inputs[i].link = null } } if (data.outputs) { - for (var i = 0; i < data.outputs.length; ++i) { + for (let i = 0; i < data.outputs.length; ++i) { if (data.outputs[i].links) { - data.outputs[i].links.length = 0; + data.outputs[i].links.length = 0 } } } - delete data["id"]; + delete data.id - if (LiteGraph.use_uuids) { - data["id"] = LiteGraph.uuidv4(); - } + if (LiteGraph.use_uuids) data.id = LiteGraph.uuidv4() //remove links - node.configure(data); + node.configure(data) - return node; + return node } /** - * serialize and stringify - * @method toString - */ + * serialize and stringify + */ toString(): string { - return JSON.stringify(this.serialize()); + return JSON.stringify(this.serialize()) } - //LGraphNode.prototype.deserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph /** - * get the title string - * @method getTitle - */ + * get the title string + */ getTitle(): string { - return this.title || this.constructor.title; + return this.title || this.constructor.title } /** - * sets the value of a property - * @method setProperty - * @param {String} name - * @param {*} value - */ + * sets the value of a property + * @param {String} name + * @param {*} value + */ setProperty(name: string, value: TWidgetValue): void { - if (!this.properties) { - this.properties = {}; - } + this.properties ||= {} if (value === this.properties[name]) - return; - var prev_value = this.properties[name]; - this.properties[name] = value; - if (this.onPropertyChanged) { - if (this.onPropertyChanged(name, value, prev_value) === false) //abort change - this.properties[name] = prev_value; - } + return + + const prev_value = this.properties[name] + this.properties[name] = value + //abort change + if (this.onPropertyChanged?.(name, value, prev_value) === false) + this.properties[name] = prev_value + if (this.widgets) //widgets could be linked to properties - for (var i = 0; i < this.widgets.length; ++i) { - var w = this.widgets[i]; + for (let i = 0; i < this.widgets.length; ++i) { + const w = this.widgets[i] if (!w) - continue; + continue if (w.options.property == name) { - w.value = value; - break; + w.value = value + break } } } - // Execution ************************* /** - * sets the output data - * @method setOutputData - * @param {number} slot - * @param {*} data - */ + * sets the output data + * @param {number} slot + * @param {*} data + */ setOutputData(slot: number, data: unknown): void { - if (!this.outputs) { - return; - } + if (!this.outputs) return //this maybe slow and a niche case //if(slot && slot.constructor === String) // slot = this.findOutputSlot(slot); - if (slot == -1 || slot >= this.outputs.length) { - return; - } + if (slot == -1 || slot >= this.outputs.length) return - var output_info = this.outputs[slot]; - if (!output_info) { - return; - } + const output_info = this.outputs[slot] + if (!output_info) return //store data in the output itself in case we want to debug - output_info._data = data; + output_info._data = data //if there are connections, pass the data to the connections if (this.outputs[slot].links) { - for (var i = 0; i < this.outputs[slot].links.length; i++) { - var link_id = this.outputs[slot].links[i]; - var link = this.graph.links[link_id]; + for (let i = 0; i < this.outputs[slot].links.length; i++) { + const link_id = this.outputs[slot].links[i] + const link = this.graph.links[link_id] if (link) - link.data = data; + link.data = data } } } /** - * sets the output data type, useful when you want to be able to overwrite the data type - * @method setOutputDataType - * @param {number} slot - * @param {String} datatype - */ + * sets the output data type, useful when you want to be able to overwrite the data type + * @param {number} slot + * @param {String} datatype + */ setOutputDataType(slot: number, type: ISlotType): void { - if (!this.outputs) { - return; - } - if (slot == -1 || slot >= this.outputs.length) { - return; - } - var output_info = this.outputs[slot]; - if (!output_info) { - return; - } + if (!this.outputs) return + if (slot == -1 || slot >= this.outputs.length) return + const output_info = this.outputs[slot] + if (!output_info) return //store data in the output itself in case we want to debug - output_info.type = type; + output_info.type = type //if there are connections, pass the data to the connections if (this.outputs[slot].links) { - for (var i = 0; i < this.outputs[slot].links.length; i++) { - var link_id = this.outputs[slot].links[i]; - this.graph.links[link_id].type = type; + for (let i = 0; i < this.outputs[slot].links.length; i++) { + const link_id = this.outputs[slot].links[i] + this.graph.links[link_id].type = type } } } /** - * Retrieves the input data (data traveling through the connection) from one slot - * @method getInputData - * @param {number} slot - * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link - * @return {*} data or if it is not connected returns undefined - */ + * Retrieves the input data (data traveling through the connection) from one slot + * @param {number} slot + * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link + * @return {*} data or if it is not connected returns undefined + */ getInputData(slot: number, force_update?: boolean): unknown { - if (!this.inputs) { - return; - } //undefined; + if (!this.inputs) return - if (slot >= this.inputs.length || this.inputs[slot].link == null) { - return; - } + if (slot >= this.inputs.length || this.inputs[slot].link == null) return - var link_id = this.inputs[slot].link; - var link = this.graph.links[link_id]; - if (!link) { - //bug: weird case but it happens sometimes - return null; - } + const link_id = this.inputs[slot].link + const link: LLink = this.graph.links[link_id] + //bug: weird case but it happens sometimes + if (!link) return null - if (!force_update) { - return link.data; - } + if (!force_update) return link.data //special case: used to extract data from the incoming connection before the graph has been executed - var node = this.graph.getNodeById(link.origin_id); - if (!node) { - return link.data; - } + const node = this.graph.getNodeById(link.origin_id) + if (!node) return link.data if (node.updateOutputData) { - node.updateOutputData(link.origin_slot); - } else if (node.onExecute) { - node.onExecute(); + node.updateOutputData(link.origin_slot) + } else { + node.onExecute?.() } - return link.data; + return link.data } /** - * Retrieves the input data type (in case this supports multiple input types) - * @method getInputDataType - * @param {number} slot - * @return {String} datatype in string format - */ + * Retrieves the input data type (in case this supports multiple input types) + * @param {number} slot + * @return {String} datatype in string format + */ getInputDataType(slot: number): ISlotType { - if (!this.inputs) { - return null; - } //undefined; + if (!this.inputs) return null - if (slot >= this.inputs.length || this.inputs[slot].link == null) { - return null; - } - var link_id = this.inputs[slot].link; - var link = this.graph.links[link_id]; - if (!link) { - //bug: weird case but it happens sometimes - return null; - } - var node = this.graph.getNodeById(link.origin_id); - if (!node) { - return link.type; - } - var output_info = node.outputs[link.origin_slot]; - if (output_info) { - return output_info.type; - } - return null; + if (slot >= this.inputs.length || this.inputs[slot].link == null) return null + const link_id = this.inputs[slot].link + const link = this.graph.links[link_id] + //bug: weird case but it happens sometimes + if (!link) return null + + const node = this.graph.getNodeById(link.origin_id) + if (!node) return link.type + + const output_info = node.outputs[link.origin_slot] + return output_info + ? output_info.type + : null } /** - * Retrieves the input data from one slot using its name instead of slot number - * @method getInputDataByName - * @param {String} slot_name - * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link - * @return {*} data or if it is not connected returns null - */ + * Retrieves the input data from one slot using its name instead of slot number + * @param {String} slot_name + * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link + * @return {*} data or if it is not connected returns null + */ getInputDataByName(slot_name: string, force_update: boolean): unknown { - var slot = this.findInputSlot(slot_name); - if (slot == -1) { - return null; - } - return this.getInputData(slot, force_update); + const slot = this.findInputSlot(slot_name) + return slot == -1 + ? null + : this.getInputData(slot, force_update) } /** - * tells you if there is a connection in one input slot - * @method isInputConnected - * @param {number} slot - * @return {boolean} - */ + * tells you if there is a connection in one input slot + * @param {number} slot + * @return {boolean} + */ isInputConnected(slot: number): boolean { - if (!this.inputs) { - return false; - } - return slot < this.inputs.length && this.inputs[slot].link != null; + if (!this.inputs) return false + return slot < this.inputs.length && this.inputs[slot].link != null } /** - * tells you info about an input connection (which node, type, etc) - * @method getInputInfo - * @param {number} slot - * @return {Object} object or null { link: id, name: string, type: string or 0 } - */ + * tells you info about an input connection (which node, type, etc) + * @param {number} slot + * @return {Object} object or null { link: id, name: string, type: string or 0 } + */ getInputInfo(slot: number): INodeInputSlot { - if (!this.inputs) { - return null; - } - if (slot < this.inputs.length) { - return this.inputs[slot]; - } - return null; + return !this.inputs || !(slot < this.inputs.length) + ? null + : this.inputs[slot] } /** - * Returns the link info in the connection of an input slot - * @method getInputLink - * @param {number} slot - * @return {LLink} object or null - */ + * Returns the link info in the connection of an input slot + * @param {number} slot + * @return {LLink} object or null + */ getInputLink(slot: number): LLink | null { - if (!this.inputs) { - return null; - } + if (!this.inputs) return null if (slot < this.inputs.length) { - var slot_info = this.inputs[slot]; - return this.graph.links[slot_info.link]; + const slot_info = this.inputs[slot] + return this.graph.links[slot_info.link] } - return null; + return null } /** - * returns the node connected in the input slot - * @method getInputNode - * @param {number} slot - * @return {LGraphNode} node or null - */ + * returns the node connected in the input slot + * @param {number} slot + * @return {LGraphNode} node or null + */ getInputNode(slot: number): LGraphNode { - if (!this.inputs) { - return null; - } - if (slot >= this.inputs.length) { - return null; - } - var input = this.inputs[slot]; - if (!input || input.link === null) { - return null; - } - var link_info = this.graph.links[input.link]; - if (!link_info) { - return null; - } - return this.graph.getNodeById(link_info.origin_id); + if (!this.inputs) return null + if (slot >= this.inputs.length) return null + + const input = this.inputs[slot] + if (!input || input.link === null) return null + + const link_info = this.graph.links[input.link] + if (!link_info) return null + + return this.graph.getNodeById(link_info.origin_id) } /** - * returns the value of an input with this name, otherwise checks if there is a property with that name - * @method getInputOrProperty - * @param {string} name - * @return {*} value - */ + * returns the value of an input with this name, otherwise checks if there is a property with that name + * @param {string} name + * @return {*} value + */ getInputOrProperty(name: string): unknown { if (!this.inputs || !this.inputs.length) { - return this.properties ? this.properties[name] : null; + return this.properties ? this.properties[name] : null } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input_info = this.inputs[i]; + for (let i = 0, l = this.inputs.length; i < l; ++i) { + const input_info = this.inputs[i] if (name == input_info.name && input_info.link != null) { - var link = this.graph.links[input_info.link]; - if (link) { - return link.data; - } + const link = this.graph.links[input_info.link] + if (link) return link.data } } - return this.properties[name]; + return this.properties[name] } /** - * tells you the last output data that went in that slot - * @method getOutputData - * @param {number} slot - * @return {Object} object or null - */ + * tells you the last output data that went in that slot + * @param {number} slot + * @return {Object} object or null + */ getOutputData(slot: number): unknown { - if (!this.outputs) { - return null; - } - if (slot >= this.outputs.length) { - return null; - } + if (!this.outputs) return null + if (slot >= this.outputs.length) return null - var info = this.outputs[slot]; - return info._data; + const info = this.outputs[slot] + return info._data } /** - * tells you info about an output connection (which node, type, etc) - * @method getOutputInfo - * @param {number} slot - * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } - */ + * tells you info about an output connection (which node, type, etc) + * @param {number} slot + * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } + */ getOutputInfo(slot: number): INodeOutputSlot { - if (!this.outputs) { - return null; - } - if (slot < this.outputs.length) { - return this.outputs[slot]; - } - return null; + return !this.outputs || !(slot < this.outputs.length) + ? null + : this.outputs[slot] } /** - * tells you if there is a connection in one output slot - * @method isOutputConnected - * @param {number} slot - * @return {boolean} - */ + * tells you if there is a connection in one output slot + * @param {number} slot + * @return {boolean} + */ isOutputConnected(slot: number): boolean { - if (!this.outputs) { - return false; - } - return ( - slot < this.outputs.length && - this.outputs[slot].links && - this.outputs[slot].links.length - ); + if (!this.outputs) return false + return slot < this.outputs.length && this.outputs[slot].links?.length > 0 } /** - * tells you if there is any connection in the output slots - * @method isAnyOutputConnected - * @return {boolean} - */ + * tells you if there is any connection in the output slots + * @return {boolean} + */ isAnyOutputConnected(): boolean { - if (!this.outputs) { - return false; - } - for (var i = 0; i < this.outputs.length; ++i) { + if (!this.outputs) return false + + for (let i = 0; i < this.outputs.length; ++i) { if (this.outputs[i].links && this.outputs[i].links.length) { - return true; + return true } } - return false; + return false } /** - * retrieves all the nodes connected to this output slot - * @method getOutputNodes - * @param {number} slot - * @return {array} - */ + * retrieves all the nodes connected to this output slot + * @param {number} slot + * @return {array} + */ getOutputNodes(slot: number): LGraphNode[] { - if (!this.outputs || this.outputs.length == 0) { - return null; - } + if (!this.outputs || this.outputs.length == 0) return null - if (slot >= this.outputs.length) { - return null; - } + if (slot >= this.outputs.length) return null - var output = this.outputs[slot]; - if (!output.links || output.links.length == 0) { - return null; - } + const output = this.outputs[slot] + if (!output.links || output.links.length == 0) return null - var r = []; - for (var i = 0; i < output.links.length; i++) { - var link_id = output.links[i]; - var link = this.graph.links[link_id]; + const r: LGraphNode[] = [] + for (let i = 0; i < output.links.length; i++) { + const link_id = output.links[i] + const link = this.graph.links[link_id] if (link) { - var target_node = this.graph.getNodeById(link.target_id); + const target_node = this.graph.getNodeById(link.target_id) if (target_node) { - r.push(target_node); + r.push(target_node) } } } - return r; + return r } addOnTriggerInput(): number { - var trigS = this.findInputSlot("onTrigger"); + const trigS = this.findInputSlot("onTrigger") if (trigS == -1) { //!trigS || - var input = this.addInput("onTrigger", LiteGraph.EVENT, { optional: true, nameLocked: true }); - return this.findInputSlot("onTrigger"); + const input = this.addInput("onTrigger", LiteGraph.EVENT, { optional: true, nameLocked: true }) + return this.findInputSlot("onTrigger") } - return trigS; + return trigS } addOnExecutedOutput(): number { - var trigS = this.findOutputSlot("onExecuted"); + const trigS = this.findOutputSlot("onExecuted") if (trigS == -1) { //!trigS || - var output = this.addOutput("onExecuted", LiteGraph.ACTION, { optional: true, nameLocked: true }); - return this.findOutputSlot("onExecuted"); + const output = this.addOutput("onExecuted", LiteGraph.ACTION, { optional: true, nameLocked: true }) + return this.findOutputSlot("onExecuted") } - return trigS; + return trigS } onAfterExecuteNode(param: unknown, options?: { action_call?: any }) { - var trigS = this.findOutputSlot("onExecuted"); + const trigS = this.findOutputSlot("onExecuted") if (trigS != -1) { //console.debug(this.id+":"+this.order+" triggering slot onAfterExecute"); //console.debug(param); //console.debug(options); - this.triggerSlot(trigS, param, null, options); + this.triggerSlot(trigS, param, null, options) } } @@ -922,742 +791,646 @@ export class LGraphNode { switch (modeTo) { case LiteGraph.ON_EVENT: // this.addOnExecutedOutput(); - break; + break case LiteGraph.ON_TRIGGER: - this.addOnTriggerInput(); - this.addOnExecutedOutput(); - break; + this.addOnTriggerInput() + this.addOnExecutedOutput() + break case LiteGraph.NEVER: - break; + break case LiteGraph.ALWAYS: - break; + break + // @ts-expect-error Not impl. case LiteGraph.ON_REQUEST: - break; + break default: - return false; - break; + return false + break } - this.mode = modeTo; - return true; + this.mode = modeTo + return true } /** - * Triggers the node code execution, place a boolean/counter to mark the node as being executed - * @method execute - * @param {*} param - * @param {*} options - */ + * Triggers the node code execution, place a boolean/counter to mark the node as being executed + * @param {*} param + * @param {*} options + */ doExecute(param?: unknown, options?: { action_call?: any }): void { - options = options || {}; + options = options || {} if (this.onExecute) { // enable this to give the event an ID - if (!options.action_call) options.action_call = this.id + "_exec_" + Math.floor(Math.random() * 9999); - - this.graph.nodes_executing[this.id] = true; //.push(this.id); - - this.onExecute(param, options); - - this.graph.nodes_executing[this.id] = false; //.pop(); - + options.action_call ||= this.id + "_exec_" + Math.floor(Math.random() * 9999) + this.graph.nodes_executing[this.id] = true //.push(this.id); + this.onExecute(param, options) + this.graph.nodes_executing[this.id] = false //.pop(); // save execution/action ref - this.exec_version = this.graph.iteration; - if (options && options.action_call) { - this.action_call = options.action_call; // if (param) - this.graph.nodes_executedAction[this.id] = options.action_call; + this.exec_version = this.graph.iteration + if (options?.action_call) { + this.action_call = options.action_call // if (param) + this.graph.nodes_executedAction[this.id] = options.action_call } } - this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event - if (this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback + this.execute_triggered = 2 // the nFrames it will be used (-- each step), means "how old" is the event + this.onAfterExecuteNode?.(param, options) // callback } /** - * Triggers an action, wrapped by logics to control execution flow - * @method actionDo - * @param {String} action name - * @param {*} param - */ + * Triggers an action, wrapped by logics to control execution flow + * @param {String} action name + * @param {*} param + */ actionDo(action: string, param: unknown, options: { action_call?: string }): void { - options = options || {}; + options = options || {} if (this.onAction) { // enable this to give the event an ID - if (!options.action_call) options.action_call = this.id + "_" + (action ? action : "action") + "_" + Math.floor(Math.random() * 9999); - - this.graph.nodes_actioning[this.id] = (action ? action : "actioning"); //.push(this.id); - - this.onAction(action, param, options); - - this.graph.nodes_actioning[this.id] = false; //.pop(); - + options.action_call ||= this.id + "_" + (action ? action : "action") + "_" + Math.floor(Math.random() * 9999) + this.graph.nodes_actioning[this.id] = (action ? action : "actioning") //.push(this.id); + this.onAction(action, param, options) + this.graph.nodes_actioning[this.id] = false //.pop(); // save execution/action ref - if (options && options.action_call) { - this.action_call = options.action_call; // if (param) - this.graph.nodes_executedAction[this.id] = options.action_call; + if (options?.action_call) { + this.action_call = options.action_call // if (param) + this.graph.nodes_executedAction[this.id] = options.action_call } } - this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event - if (this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); + this.action_triggered = 2 // the nFrames it will be used (-- each step), means "how old" is the event + this.onAfterExecuteNode?.(param, options) } /** - * Triggers an event in this node, this will trigger any output with the same name - * @method trigger - * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all - * @param {*} param - */ + * Triggers an event in this node, this will trigger any output with the same name + * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all + * @param {*} param + */ trigger(action: string, param: unknown, options: { action_call?: any }): void { if (!this.outputs || !this.outputs.length) { - return; + return } if (this.graph) - this.graph._last_trigger_time = LiteGraph.getTime(); + this.graph._last_trigger_time = LiteGraph.getTime() - for (var i = 0; i < this.outputs.length; ++i) { - var output = this.outputs[i]; + for (let i = 0; i < this.outputs.length; ++i) { + const output = this.outputs[i] if (!output || output.type !== LiteGraph.EVENT || (action && output.name != action)) - continue; - this.triggerSlot(i, param, null, options); + continue + this.triggerSlot(i, param, null, options) } } /** - * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes - * @method triggerSlot - * @param {Number} slot the index of the output slot - * @param {*} param - * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot - */ + * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes + * @param {Number} slot the index of the output slot + * @param {*} param + * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot + */ triggerSlot(slot: number, param: unknown, link_id: number, options: { action_call?: any }): void { - options = options || {}; - if (!this.outputs) { - return; - } + options = options || {} + if (!this.outputs) return if (slot == null) { - console.error("slot must be a number"); - return; + console.error("slot must be a number") + return } - if (slot.constructor !== Number) - console.warn("slot must be a number, use node.trigger('name') if you want to use a string"); + if (typeof slot !== "number") + console.warn("slot must be a number, use node.trigger('name') if you want to use a string") - var output = this.outputs[slot]; - if (!output) { - return; - } + const output = this.outputs[slot] + if (!output) return - var links = output.links; - if (!links || !links.length) { - return; - } + const links = output.links + if (!links || !links.length) return - if (this.graph) { - this.graph._last_trigger_time = LiteGraph.getTime(); - } + if (this.graph) + this.graph._last_trigger_time = LiteGraph.getTime() //for every link attached here - for (var k = 0; k < links.length; ++k) { - var id = links[k]; - if (link_id != null && link_id != id) { - //to skip links - continue; - } - var link_info = this.graph.links[links[k]]; - if (!link_info) { - //not connected - continue; - } - link_info._last_time = LiteGraph.getTime(); - var node = this.graph.getNodeById(link_info.target_id); - if (!node) { - //node not found? - continue; - } + for (let k = 0; k < links.length; ++k) { + const id = links[k] + //to skip links + if (link_id != null && link_id != id) continue - //used to mark events in graph - var target_connection = node.inputs[link_info.target_slot]; + const link_info = this.graph.links[links[k]] + //not connected + if (!link_info) continue + + link_info._last_time = LiteGraph.getTime() + const node = this.graph.getNodeById(link_info.target_id) + //node not found? + if (!node) continue if (node.mode === LiteGraph.ON_TRIGGER) { // generate unique trigger ID if not present - if (!options.action_call) options.action_call = this.id + "_trigg_" + Math.floor(Math.random() * 9999); - if (node.onExecute) { - // -- wrapping node.onExecute(param); -- - node.doExecute(param, options); - } + if (!options.action_call) options.action_call = this.id + "_trigg_" + Math.floor(Math.random() * 9999) + // -- wrapping node.onExecute(param); -- + node.doExecute?.(param, options) } else if (node.onAction) { // generate unique action ID if not present - if (!options.action_call) options.action_call = this.id + "_act_" + Math.floor(Math.random() * 9999); + if (!options.action_call) options.action_call = this.id + "_act_" + Math.floor(Math.random() * 9999) //pass the action name - var target_connection = node.inputs[link_info.target_slot]; + const target_connection = node.inputs[link_info.target_slot] // wrap node.onAction(target_connection.name, param); - node.actionDo(target_connection.name, param, options); + node.actionDo(target_connection.name, param, options) } } } /** - * clears the trigger slot animation - * @method clearTriggeredSlot - * @param {Number} slot the index of the output slot - * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot - */ + * clears the trigger slot animation + * @param {Number} slot the index of the output slot + * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot + */ clearTriggeredSlot(slot: number, link_id: number): void { - if (!this.outputs) { - return; - } + if (!this.outputs) return - var output = this.outputs[slot]; - if (!output) { - return; - } + const output = this.outputs[slot] + if (!output) return - var links = output.links; - if (!links || !links.length) { - return; - } + const links = output.links + if (!links || !links.length) return //for every link attached here - for (var k = 0; k < links.length; ++k) { - var id = links[k]; - if (link_id != null && link_id != id) { - //to skip links - continue; - } - var link_info = this.graph.links[links[k]]; - if (!link_info) { - //not connected - continue; - } - link_info._last_time = 0; + for (let k = 0; k < links.length; ++k) { + const id = links[k] + //to skip links + if (link_id != null && link_id != id) continue + + const link_info = this.graph.links[links[k]] + //not connected + if (!link_info) continue + + link_info._last_time = 0 } } /** - * changes node size and triggers callback - * @method setSize - * @param {vec2} size - */ + * changes node size and triggers callback + * @param {vec2} size + */ setSize(size: Size): void { - this.size = size; - if (this.onResize) - this.onResize(this.size); + this.size = size + this.onResize?.(this.size) } /** - * add a new property to this node - * @method addProperty - * @param {string} name - * @param {*} default_value - * @param {string} type string defining the output type ("vec3","number",...) - * @param {Object} extra_info this can be used to have special properties of the property (like values, etc) - */ + * add a new property to this node + * @param {string} name + * @param {*} default_value + * @param {string} type string defining the output type ("vec3","number",...) + * @param {Object} extra_info this can be used to have special properties of the property (like values, etc) + */ addProperty(name: string, default_value: unknown, type?: string, extra_info?: Dictionary): INodePropertyInfo { - var o = { name: name, type: type, default_value: default_value }; + const o: INodePropertyInfo = { name: name, type: type, default_value: default_value } if (extra_info) { - for (var i in extra_info) { - o[i] = extra_info[i]; + for (const i in extra_info) { + o[i] = extra_info[i] } } - if (!this.properties_info) { - this.properties_info = []; - } - this.properties_info.push(o); - if (!this.properties) { - this.properties = {}; - } - this.properties[name] = default_value; - return o; + this.properties_info ||= [] + this.properties_info.push(o) + this.properties ||= {} + this.properties[name] = default_value + return o } - //connections /** - * add a new output slot to use in this node - * @method addOutput - * @param {string} name - * @param {string} type string defining the output type ("vec3","number",...) - * @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc) - */ + * add a new output slot to use in this node + * @param {string} name + * @param {string} type string defining the output type ("vec3","number",...) + * @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc) + */ addOutput(name?: string, type?: ISlotType, extra_info?: object): INodeOutputSlot { - var output = { name: name, type: type, links: null }; + const output = { name: name, type: type, links: null } if (extra_info) { - for (var i in extra_info) { - output[i] = extra_info[i]; + for (const i in extra_info) { + output[i] = extra_info[i] } } - if (!this.outputs) { - this.outputs = []; - } - this.outputs.push(output); - if (this.onOutputAdded) { - this.onOutputAdded(output); - } + this.outputs ||= [] + this.outputs.push(output) + this.onOutputAdded?.(output) - if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this, type, true); + if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this, type, true) - this.setSize(this.computeSize()); - this.setDirtyCanvas(true, true); - return output; + this.setSize(this.computeSize()) + this.setDirtyCanvas(true, true) + return output } /** - * add a new output slot to use in this node - * @method addOutputs - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ + * add a new output slot to use in this node + * @param {Array} array of triplets like [[name,type,extra_info],[...]] + */ addOutputs(array: [string, ISlotType, Record][]): void { - for (var i = 0; i < array.length; ++i) { - var info = array[i]; - var o = { name: info[0], type: info[1], link: null }; + for (let i = 0; i < array.length; ++i) { + const info = array[i] + const o = { name: info[0], type: info[1], link: null } if (array[2]) { - for (var j in info[2]) { - o[j] = info[2][j]; + for (const j in info[2]) { + o[j] = info[2][j] } } - if (!this.outputs) { - this.outputs = []; - } - this.outputs.push(o); - if (this.onOutputAdded) { - this.onOutputAdded(o); - } + this.outputs ||= [] + this.outputs.push(o) + this.onOutputAdded?.(o) - if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this, info[1], true); + if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this, info[1], true) } - this.setSize(this.computeSize()); - this.setDirtyCanvas(true, true); + this.setSize(this.computeSize()) + this.setDirtyCanvas(true, true) } /** - * remove an existing output slot - * @method removeOutput - * @param {number} slot - */ + * remove an existing output slot + * @param {number} slot + */ removeOutput(slot: number): void { - this.disconnectOutput(slot); - this.outputs.splice(slot, 1); - for (var i = slot; i < this.outputs.length; ++i) { - if (!this.outputs[i] || !this.outputs[i].links) { - continue; - } - var links = this.outputs[i].links; - for (var j = 0; j < links.length; ++j) { - var link = this.graph.links[links[j]]; - if (!link) { - continue; - } - link.origin_slot -= 1; + this.disconnectOutput(slot) + this.outputs.splice(slot, 1) + for (let i = slot; i < this.outputs.length; ++i) { + if (!this.outputs[i] || !this.outputs[i].links) + continue + const links = this.outputs[i].links + for (let j = 0; j < links.length; ++j) { + const link = this.graph.links[links[j]] + if (!link) continue + + link.origin_slot -= 1 } } - this.setSize(this.computeSize()); - if (this.onOutputRemoved) { - this.onOutputRemoved(slot); - } - this.setDirtyCanvas(true, true); + this.setSize(this.computeSize()) + this.onOutputRemoved?.(slot) + this.setDirtyCanvas(true, true) } /** - * add a new input slot to use in this node - * @method addInput - * @param {string} name - * @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0 - * @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc) - */ + * add a new input slot to use in this node + * @param {string} name + * @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0 + * @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc) + */ addInput(name: string, type: ISlotType, extra_info?: object): INodeInputSlot { - type = type || 0; - var input = { name: name, type: type, link: null }; + type = type || 0 + const input: INodeInputSlot = { name: name, type: type, link: null } if (extra_info) { - for (var i in extra_info) { - input[i] = extra_info[i]; + for (const i in extra_info) { + input[i] = extra_info[i] } } - if (!this.inputs) { - this.inputs = []; - } + this.inputs ||= [] + this.inputs.push(input) + this.setSize(this.computeSize()) - this.inputs.push(input); - this.setSize(this.computeSize()); + this.onInputAdded?.(input) + LiteGraph.registerNodeAndSlotType(this, type) - if (this.onInputAdded) { - this.onInputAdded(input); - } - - LiteGraph.registerNodeAndSlotType(this, type); - - this.setDirtyCanvas(true, true); - return input; + this.setDirtyCanvas(true, true) + return input } /** - * add several new input slots in this node - * @method addInputs - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ + * add several new input slots in this node + * @param {Array} array of triplets like [[name,type,extra_info],[...]] + */ addInputs(array: [string, ISlotType, Record][]): void { - for (var i = 0; i < array.length; ++i) { - var info = array[i]; - var o = { name: info[0], type: info[1], link: null }; + for (let i = 0; i < array.length; ++i) { + const info = array[i] + const o: INodeInputSlot = { name: info[0], type: info[1], link: null } + // TODO: Checking the wrong variable here - confirm no downstream consumers, then remove. if (array[2]) { - for (var j in info[2]) { - o[j] = info[2][j]; + for (const j in info[2]) { + o[j] = info[2][j] } } - if (!this.inputs) { - this.inputs = []; - } - this.inputs.push(o); - if (this.onInputAdded) { - this.onInputAdded(o); - } + this.inputs ||= [] + this.inputs.push(o) + this.onInputAdded?.(o) - LiteGraph.registerNodeAndSlotType(this, info[1]); + LiteGraph.registerNodeAndSlotType(this, info[1]) } - this.setSize(this.computeSize()); - this.setDirtyCanvas(true, true); + this.setSize(this.computeSize()) + this.setDirtyCanvas(true, true) } /** - * remove an existing input slot - * @method removeInput - * @param {number} slot - */ + * remove an existing input slot + * @param {number} slot + */ removeInput(slot: number): void { - this.disconnectInput(slot); - var slot_info = this.inputs.splice(slot, 1); - for (var i = slot; i < this.inputs.length; ++i) { - if (!this.inputs[i]) { - continue; - } - var link = this.graph.links[this.inputs[i].link]; - if (!link) { - continue; - } - link.target_slot -= 1; + this.disconnectInput(slot) + const slot_info = this.inputs.splice(slot, 1) + for (let i = slot; i < this.inputs.length; ++i) { + if (!this.inputs[i]) continue + + const link = this.graph.links[this.inputs[i].link] + if (!link) continue + + link.target_slot -= 1 } - this.setSize(this.computeSize()); - if (this.onInputRemoved) { - this.onInputRemoved(slot, slot_info[0]); - } - this.setDirtyCanvas(true, true); + this.setSize(this.computeSize()) + this.onInputRemoved?.(slot, slot_info[0]) + this.setDirtyCanvas(true, true) } /** - * add an special connection to this node (used for special kinds of graphs) - * @method addConnection - * @param {string} name - * @param {string} type string defining the input type ("vec3","number",...) - * @param {[x,y]} pos position of the connection inside the node - * @param {string} direction if is input or output - */ + * add an special connection to this node (used for special kinds of graphs) + * @param {string} name + * @param {string} type string defining the input type ("vec3","number",...) + * @param {[x,y]} pos position of the connection inside the node + * @param {string} direction if is input or output + */ addConnection(name: string, type: string, pos: Point, direction: string) { - var o = { + const o = { name: name, type: type, pos: pos, direction: direction, links: null - }; - this.connections.push(o); - return o; + } + this.connections.push(o) + return o } /** - * computes the minimum size of a node according to its inputs and output slots - * @method computeSize - * @param {vec2} minHeight - * @return {vec2} the total size - */ + * computes the minimum size of a node according to its inputs and output slots + * @param out + * @return the total size + */ computeSize(out?: Size): Size { - if (this.constructor.size) { - return this.constructor.size.concat(); - } + const ctorSize = this.constructor.size + if (ctorSize) return [ctorSize[0], ctorSize[1]] - var rows = Math.max( + let rows = Math.max( this.inputs ? this.inputs.length : 1, this.outputs ? this.outputs.length : 1 - ); - var size = out || new Float32Array([0, 0]); - rows = Math.max(rows, 1); - var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size + ) + const size = out || new Float32Array([0, 0]) + rows = Math.max(rows, 1) + const font_size = LiteGraph.NODE_TEXT_SIZE //although it should be graphcanvas.inner_text_font size - var title_width = compute_text_size(this.title); - var input_width = 0; - var output_width = 0; + const title_width = compute_text_size(this.title) + let input_width = 0 + let output_width = 0 if (this.inputs) { - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input = this.inputs[i]; - var text = input.label || input.name || ""; - var text_width = compute_text_size(text); - if (input_width < text_width) { - input_width = text_width; - } + for (let i = 0, l = this.inputs.length; i < l; ++i) { + const input = this.inputs[i] + const text = input.label || input.name || "" + const text_width = compute_text_size(text) + if (input_width < text_width) + input_width = text_width } } if (this.outputs) { - for (var i = 0, l = this.outputs.length; i < l; ++i) { - var output = this.outputs[i]; - var text = output.label || output.name || ""; - var text_width = compute_text_size(text); - if (output_width < text_width) { - output_width = text_width; - } + for (let i = 0, l = this.outputs.length; i < l; ++i) { + const output = this.outputs[i] + const text = output.label || output.name || "" + const text_width = compute_text_size(text) + if (output_width < text_width) + output_width = text_width } } - size[0] = Math.max(input_width + output_width + 10, title_width); - size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH); - if (this.widgets && this.widgets.length) { - size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5); - } + size[0] = Math.max(input_width + output_width + 10, title_width) + size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH) + if (this.widgets?.length) + size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5) - size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; + size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT - var widgets_height = 0; - if (this.widgets && this.widgets.length) { - for (var i = 0, l = this.widgets.length; i < l; ++i) { - if (this.widgets[i].computeSize) - widgets_height += this.widgets[i].computeSize(size[0])[1] + 4; + let widgets_height = 0 + if (this.widgets?.length) { + for (let i = 0, l = this.widgets.length; i < l; ++i) { + const widget = this.widgets[i] - - else - widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4; + widgets_height += widget.computeSize + ? widget.computeSize(size[0])[1] + 4 + : LiteGraph.NODE_WIDGET_HEIGHT + 4 } - widgets_height += 8; + widgets_height += 8 } //compute height using widgets height if (this.widgets_up) - size[1] = Math.max(size[1], widgets_height); + size[1] = Math.max(size[1], widgets_height) else if (this.widgets_start_y != null) - size[1] = Math.max(size[1], widgets_height + this.widgets_start_y); - - + size[1] = Math.max(size[1], widgets_height + this.widgets_start_y) else - size[1] += widgets_height; + size[1] += widgets_height - function compute_text_size(text) { - if (!text) { - return 0; - } - return font_size * text.length * 0.6; + function compute_text_size(text: string) { + return text + ? font_size * text.length * 0.6 + : 0 } - if (this.constructor.min_height && - size[1] < this.constructor.min_height) { - size[1] = this.constructor.min_height; + if (this.constructor.min_height && size[1] < this.constructor.min_height) { + size[1] = this.constructor.min_height } - size[1] += 6; //margin + //margin + size[1] += 6 - return size; + return size } inResizeCorner(canvasX: number, canvasY: number): boolean { - var rows = this.outputs ? this.outputs.length : 1; - var outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; + const rows = this.outputs ? this.outputs.length : 1 + const outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT return isInsideRectangle(canvasX, canvasY, this.pos[0] + this.size[0] - 15, this.pos[1] + Math.max(this.size[1] - 15, outputs_offset), 20, 20 - ); + ) } /** - * returns all the info available about a property of this node. - * - * @method getPropertyInfo - * @param {String} property name of the property - * @return {Object} the object with all the available info - */ + * returns all the info available about a property of this node. + * + * @param {String} property name of the property + * @return {Object} the object with all the available info + */ getPropertyInfo(property: string) { - var info = null; + let info = null //there are several ways to define info about a property //legacy mode if (this.properties_info) { - for (var i = 0; i < this.properties_info.length; ++i) { + for (let i = 0; i < this.properties_info.length; ++i) { if (this.properties_info[i].name == property) { - info = this.properties_info[i]; - break; + info = this.properties_info[i] + break } } } //litescene mode using the constructor if (this.constructor["@" + property]) - info = this.constructor["@" + property]; + info = this.constructor["@" + property] - if (this.constructor.widgets_info && this.constructor.widgets_info[property]) - info = this.constructor.widgets_info[property]; + if (this.constructor.widgets_info?.[property]) + info = this.constructor.widgets_info[property] //litescene mode using the constructor if (!info && this.onGetPropertyInfo) { - info = this.onGetPropertyInfo(property); + info = this.onGetPropertyInfo(property) } - if (!info) - info = {}; - if (!info.type) - info.type = typeof this.properties[property]; + info ||= {} + info.type ||= typeof this.properties[property] if (info.widget == "combo") - info.type = "enum"; + info.type = "enum" - return info; + return info } /** - * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties - * - * @method addWidget - * @param {String} type the widget type (could be "number","string","combo" - * @param {String} name the text to show on the widget - * @param {String} value the default value - * @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify) - * @param {Object} options the object that contains special properties of this widget - * @return {Object} the created widget object - */ + * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties + * + * @param {String} type the widget type (could be "number","string","combo" + * @param {String} name the text to show on the widget + * @param {String} value the default value + * @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify) + * @param {Object} options the object that contains special properties of this widget + * @return {Object} the created widget object + */ addWidget(type: string, name: string, value: any, callback: IWidget["callback"], options?: any): IWidget { - if (!this.widgets) { - this.widgets = []; + this.widgets ||= [] + + if (!options && callback && typeof callback === "object") { + options = callback + callback = null } - if (!options && callback && callback.constructor === Object) { - options = callback; - callback = null; + //options can be the property name + if (options && typeof options === "string") + options = { property: options } + + //callback can be the property name + if (callback && typeof callback === "string") { + options ||= {} + options.property = callback + callback = null } - if (options && options.constructor === String) //options can be the property name - options = { property: options }; - - if (callback && callback.constructor === String) //callback can be the property name - { - if (!options) - options = {}; - options.property = callback; - callback = null; + if (callback && typeof callback !== "function") { + console.warn("addWidget: callback must be a function") + callback = null } - if (callback && callback.constructor !== Function) { - console.warn("addWidget: callback must be a function"); - callback = null; - } - - var w = { + const w: IWidget = { + // @ts-expect-error Type check or just assert? type: type.toLowerCase(), name: name, value: value, callback: callback, options: options || {} - }; + } if (w.options.y !== undefined) { - w.y = w.options.y; + w.y = w.options.y } if (!callback && !w.options.callback && !w.options.property) { - console.warn("LiteGraph addWidget(...) without a callback or property assigned"); + console.warn("LiteGraph addWidget(...) without a callback or property assigned") } if (type == "combo" && !w.options.values) { - throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }"; + throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }" } - this.widgets.push(w); - this.setSize(this.computeSize()); - return w; + this.widgets.push(w) + this.setSize(this.computeSize()) + return w } addCustomWidget(custom_widget: IWidget): IWidget { - if (!this.widgets) { - this.widgets = []; - } - this.widgets.push(custom_widget); - return custom_widget; + this.widgets ||= [] + this.widgets.push(custom_widget) + return custom_widget } /** - * returns the bounding of the object, used for rendering purposes - * @method getBounding - * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage - * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation - * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] - */ + * returns the bounding of the object, used for rendering purposes + * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage + * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation + * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] + */ getBounding(out?: Float32Array, compute_outer?: boolean): Float32Array { - out = out || new Float32Array(4); - const nodePos = this.pos; - const isCollapsed = this.flags.collapsed; - const nodeSize = this.size; + out = out || new Float32Array(4) + const nodePos = this.pos + const isCollapsed = this.flags.collapsed + const nodeSize = this.size - let left_offset = 0; + let left_offset = 0 // 1 offset due to how nodes are rendered - let right_offset = 1; - let top_offset = 0; - let bottom_offset = 0; + let right_offset = 1 + let top_offset = 0 + let bottom_offset = 0 if (compute_outer) { // 4 offset for collapsed node connection points - left_offset = 4; + left_offset = 4 // 6 offset for right shadow and collapsed node connection points - right_offset = 6 + left_offset; + right_offset = 6 + left_offset // 4 offset for collapsed nodes top connection points - top_offset = 4; + top_offset = 4 // 5 offset for bottom shadow and collapsed node connection points - bottom_offset = 5 + top_offset; + bottom_offset = 5 + top_offset } - out[0] = nodePos[0] - left_offset; - out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; - out[2] = isCollapsed ? - (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset : - nodeSize[0] + right_offset; - out[3] = isCollapsed ? - LiteGraph.NODE_TITLE_HEIGHT + bottom_offset : - nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; + out[0] = nodePos[0] - left_offset + out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset + out[2] = isCollapsed + ? (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset + : nodeSize[0] + right_offset + out[3] = isCollapsed + ? LiteGraph.NODE_TITLE_HEIGHT + bottom_offset + : nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset - if (this.onBounding) { - this.onBounding(out); - } - return out; + this.onBounding?.(out) + return out } /** - * checks if a point is inside the shape of a node - * @method isPointInside - * @param {number} x - * @param {number} y - * @return {boolean} - */ + * checks if a point is inside the shape of a node + * @param {number} x + * @param {number} y + * @return {boolean} + */ isPointInside(x: number, y: number, margin?: number, skip_title?: boolean): boolean { - margin = margin || 0; + margin ||= 0 - var margin_top = this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT; - if (skip_title) { - margin_top = 0; - } - if (this.flags && this.flags.collapsed) { + const margin_top = skip_title || this.graph?.isLive() + ? 0 + : LiteGraph.NODE_TITLE_HEIGHT + + if (this.flags?.collapsed) { //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS) if (isInsideRectangle( x, @@ -1668,415 +1441,375 @@ export class LGraphNode { 2 * margin, LiteGraph.NODE_TITLE_HEIGHT + 2 * margin )) { - return true; + return true } } else if (this.pos[0] - 4 - margin < x && this.pos[0] + this.size[0] + 4 + margin > x && this.pos[1] - margin_top - margin < y && this.pos[1] + this.size[1] + margin > y) { - return true; + return true } - return false; + return false } /** - * checks if a point is inside a node slot, and returns info about which slot - * @method getSlotInPosition - * @param {number} x - * @param {number} y - * @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } - */ + * checks if a point is inside a node slot, and returns info about which slot + * @param x + * @param y + * @returns if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } + */ getSlotInPosition(x: number, y: number): IFoundSlot | null { //search for inputs - var link_pos = new Float32Array(2); + const link_pos = new Float32Array(2) if (this.inputs) { - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input = this.inputs[i]; - this.getConnectionPos(true, i, link_pos); - if (isInsideRectangle( - x, - y, - link_pos[0] - 10, - link_pos[1] - 5, - 20, - 10 - )) { - return { input: input, slot: i, link_pos: link_pos }; + for (let i = 0, l = this.inputs.length; i < l; ++i) { + const input = this.inputs[i] + this.getConnectionPos(true, i, link_pos) + if (isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20, 10)) { + return { input, slot: i, link_pos } } } } if (this.outputs) { - for (var i = 0, l = this.outputs.length; i < l; ++i) { - var output = this.outputs[i]; - this.getConnectionPos(false, i, link_pos); - if (isInsideRectangle( - x, - y, - link_pos[0] - 10, - link_pos[1] - 5, - 20, - 10 - )) { - return { output: output, slot: i, link_pos: link_pos }; + for (let i = 0, l = this.outputs.length; i < l; ++i) { + const output = this.outputs[i] + this.getConnectionPos(false, i, link_pos) + if (isInsideRectangle(x, y, link_pos[0] - 10, link_pos[1] - 5, 20, 10)) { + return { output, slot: i, link_pos } } } } - return null; + return null } /** - * returns the input slot with a given name (used for dynamic slots), -1 if not found - * @method findInputSlot - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number_or_object} the slot (-1 if not found) - */ + * Returns the input slot with a given name (used for dynamic slots), -1 if not found + * @param name the name of the slot + * @param returnObj if the obj itself wanted + * @returns the slot (-1 if not found) + */ findInputSlot(name: string, returnObj?: TReturn): number findInputSlot(name: string, returnObj?: TReturn): INodeInputSlot findInputSlot(name: string, returnObj: boolean = false) { - if (!this.inputs) { - return -1 - } + if (!this.inputs) return -1 + for (let i = 0, l = this.inputs.length; i < l; ++i) { if (name == this.inputs[i].name) { return !returnObj ? i : this.inputs[i] } } return -1 - if (!this.inputs) { - return -1; - } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - if (name == this.inputs[i].name) { - return !returnObj ? i : this.inputs[i]; - } - } - return -1; } /** - * returns the output slot with a given name (used for dynamic slots), -1 if not found - * @method findOutputSlot - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number_or_object} the slot (-1 if not found) - */ + * returns the output slot with a given name (used for dynamic slots), -1 if not found + * @param {string} name the name of the slot + * @param {boolean} returnObj if the obj itself wanted + * @return {number | INodeOutputSlot} the slot (-1 if not found) + */ findOutputSlot(name: string, returnObj?: TReturn): number findOutputSlot(name: string, returnObj?: TReturn): INodeOutputSlot findOutputSlot(name: string, returnObj: boolean = false) { - returnObj = returnObj || false; - if (!this.outputs) { - return -1; - } - for (var i = 0, l = this.outputs.length; i < l; ++i) { + if (!this.outputs) return -1 + + for (let i = 0, l = this.outputs.length; i < l; ++i) { if (name == this.outputs[i].name) { - return !returnObj ? i : this.outputs[i]; + return !returnObj ? i : this.outputs[i] } } - return -1; + return -1 } - // TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options /** - * returns the first free input slot - * @method findInputSlotFree - * @param {object} options - * @return {number_or_object} the slot (-1 if not found) - */ - findInputSlotFree(optsIn: { typesNotAccepted: number[], returnObj?: TReturn }): number - findInputSlotFree(optsIn: { typesNotAccepted: number[], returnObj?: TReturn }): INodeInputSlot - findInputSlotFree(optsIn: { typesNotAccepted: number[], returnObj?: boolean }) { - var optsIn = optsIn || {}; - var optsDef = { + * returns the first free input slot + * @param {object} optsIn + * @return {number | INodeInputSlot} the slot (-1 if not found) + */ + findInputSlotFree(optsIn?: { typesNotAccepted?: number[], returnObj?: TReturn }): number + findInputSlotFree(optsIn?: { typesNotAccepted?: number[], returnObj?: TReturn }): INodeInputSlot + findInputSlotFree(optsIn?: { typesNotAccepted?: number[], returnObj?: boolean }) { + const optsDef = { returnObj: false, typesNotAccepted: [] - }; - var opts = Object.assign(optsDef, optsIn); - if (!this.inputs) { - return -1; } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - if (this.inputs[i].link && this.inputs[i].link != null) { - continue; - } - if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.inputs[i].type)) { - continue; - } - return !opts.returnObj ? i : this.inputs[i]; + const opts = Object.assign(optsDef, optsIn || {}) + if (!this.inputs) return -1 + + for (let i = 0, l = this.inputs.length; i < l; ++i) { + if (this.inputs[i].link) continue + if (opts.typesNotAccepted?.includes?.(this.inputs[i].type)) continue + return !opts.returnObj ? i : this.inputs[i] } - return -1; + return -1 } /** - * returns the first output slot free - * @method findOutputSlotFree - * @param {object} options - * @return {number_or_object} the slot (-1 if not found) - */ - findOutputSlotFree(optsIn: { typesNotAccepted: number[], returnObj?: TReturn }): number - findOutputSlotFree(optsIn: { typesNotAccepted: number[], returnObj?: TReturn }): INodeOutputSlot - findOutputSlotFree(optsIn: { typesNotAccepted: number[], returnObj?: boolean }) { - var optsIn = optsIn || {}; - var optsDef = { + * returns the first output slot free + * @param {object} optsIn + * @return {number | INodeOutputSlot} the slot (-1 if not found) + */ + findOutputSlotFree(optsIn?: { typesNotAccepted?: number[], returnObj?: TReturn }): number + findOutputSlotFree(optsIn?: { typesNotAccepted?: number[], returnObj?: TReturn }): INodeOutputSlot + findOutputSlotFree(optsIn?: { typesNotAccepted?: number[], returnObj?: boolean }) { + const optsDef = { returnObj: false, typesNotAccepted: [] - }; - var opts = Object.assign(optsDef, optsIn); - if (!this.outputs) { - return -1; } - for (var i = 0, l = this.outputs.length; i < l; ++i) { - if (this.outputs[i].links && this.outputs[i].links != null) { - continue; - } - if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.outputs[i].type)) { - continue; - } - return !opts.returnObj ? i : this.outputs[i]; + const opts = Object.assign(optsDef, optsIn || {}) + if (!this.outputs) return -1 + + for (let i = 0, l = this.outputs.length; i < l; ++i) { + if (this.outputs[i].links) continue + if (opts.typesNotAccepted?.includes?.(this.outputs[i].type)) continue + return !opts.returnObj ? i : this.outputs[i] } - return -1; + return -1 } /** - * findSlotByType for INPUTS - */ - findInputSlotByType(type, returnObj, preferFreeSlot, doNotUseOccupied) { - return this.findSlotByType(true, type, returnObj, preferFreeSlot, doNotUseOccupied); + * findSlotByType for INPUTS + */ + findInputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number + findInputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): INodeInputSlot + findInputSlotByType(type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { + // Requires refactor + return returnObj + ? this.findSlotByType(true, type, true, preferFreeSlot, doNotUseOccupied) + : this.findSlotByType(true, type, false, preferFreeSlot, doNotUseOccupied) } /** - * findSlotByType for OUTPUTS - */ - findOutputSlotByType(type, returnObj, preferFreeSlot, doNotUseOccupied) { - return this.findSlotByType(false, type, returnObj, preferFreeSlot, doNotUseOccupied); + * findSlotByType for OUTPUTS + */ + findOutputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number + findOutputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): INodeOutputSlot + findOutputSlotByType(type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { + // Requires refactor + return returnObj + ? this.findSlotByType(false, type, true, preferFreeSlot, doNotUseOccupied) + : this.findSlotByType(false, type, false, preferFreeSlot, doNotUseOccupied) } /** - * returns the output (or input) slot with a given type, -1 if not found - * @method findSlotByType - * @param {boolean} input uise inputs instead of outputs - * @param {string} type the type of the slot - * @param {boolean} returnObj if the obj itself wanted - * @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway) - * @return {number_or_object} the slot (-1 if not found) - */ - findSlotByType(input, type, returnObj, preferFreeSlot, doNotUseOccupied) { - input = input || false; - returnObj = returnObj || false; - preferFreeSlot = preferFreeSlot || false; - doNotUseOccupied = doNotUseOccupied || false; - var aSlots = input ? this.inputs : this.outputs; - if (!aSlots) { - return -1; - } + * returns the output (or input) slot with a given type, -1 if not found + * @param {boolean} input uise inputs instead of outputs + * @param {string} type the type of the slot + * @param {boolean} returnObj if the obj itself wanted + * @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway) + * @return {number_or_object} the slot (-1 if not found) + */ + findSlotByType(input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number + findSlotByType(input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): INodeInputSlot + findSlotByType(input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): INodeOutputSlot + findSlotByType(input: TSlot, type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { + // TODO: Write wrappers to sanitise the "returnObj" situation + // @ts-expect-error + input ||= false + returnObj ||= false + preferFreeSlot ||= false + doNotUseOccupied ||= false + const slots = input ? this.inputs : this.outputs + if (!slots) return -1 + // !! empty string type is considered 0, * !! - if (type == "" || type == "*") type = 0; - for (var i = 0, l = aSlots.length; i < l; ++i) { - var tFound = false; - var aSource = (type + "").toLowerCase().split(","); - var aDest = aSlots[i].type == "0" || aSlots[i].type == "*" ? "0" : aSlots[i].type; - aDest = (aDest + "").toLowerCase().split(","); - for (var sI = 0; sI < aSource.length; sI++) { - for (var dI = 0; dI < aDest.length; dI++) { - if (aSource[sI] == "_event_") aSource[sI] = LiteGraph.EVENT; - if (aDest[sI] == "_event_") aDest[sI] = LiteGraph.EVENT; - if (aSource[sI] == "*") aSource[sI] = 0; - if (aDest[sI] == "*") aDest[sI] = 0; - if (aSource[sI] == aDest[dI]) { - if (preferFreeSlot && (aSlots[i].links && aSlots[i].links !== null) || (aSlots[i].link && aSlots[i].link !== null)) continue; - return !returnObj ? i : aSlots[i]; - } - } - } - } - // if didnt find some, stop checking for free slots - if (preferFreeSlot && !doNotUseOccupied) { - for (var i = 0, l = aSlots.length; i < l; ++i) { - var tFound = false; - var aSource = (type + "").toLowerCase().split(","); - var aDest = aSlots[i].type == "0" || aSlots[i].type == "*" ? "0" : aSlots[i].type; - aDest = (aDest + "").toLowerCase().split(","); - for (var sI = 0; sI < aSource.length; sI++) { - for (var dI = 0; dI < aDest.length; dI++) { - if (aSource[sI] == "*") aSource[sI] = 0; - if (aDest[sI] == "*") aDest[sI] = 0; - if (aSource[sI] == aDest[dI]) { - return !returnObj ? i : aSlots[i]; + if (type == "" || type == "*") type = 0 + const sourceTypes = String(type).toLowerCase().split(",") + + // Run the search + let occupiedSlot: number | INodeInputSlot | INodeOutputSlot = null + for (let i = 0, l = slots.length; i < l; ++i) { + const slot = slots[i] + const destTypes = slot.type == "0" || slot.type == "*" + ? ["0"] + : String(slot.type).toLowerCase().split(",") + + for (const sourceType of sourceTypes) { + // TODO: Remove _event_ entirely. + const source = sourceType == "_event_" ? LiteGraph.EVENT : sourceType + + for (const destType of destTypes) { + const dest = destType == "_event_" ? LiteGraph.EVENT : destType + + if (source == dest || source === "*" || dest === "*") { + // @ts-expect-error I/O link vs links issue + if (preferFreeSlot && (slot.links?.length > 0 || slot.link != null)) { + // In case we can't find a free slot. + occupiedSlot ??= returnObj ? slot : i + continue } + return returnObj ? slot : i } } } } - return -1; + + return doNotUseOccupied ? -1 : occupiedSlot } /** - * connect this node output to the input of another node BY TYPE - * @method connectByType - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {string} target_type the input slot type of the target node - * @return {Object} the link_info is created, otherwise null - */ - connectByType(slot: number, target_node: LGraphNode, target_slotType: ISlotType, optsIn?: { reroutes?: RerouteId[] }): LLink | null { - var optsIn = optsIn || {}; - var optsDef = { + * connect this node output to the input of another node BY TYPE + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} node the target node + * @param {string} target_type the input slot type of the target node + * @return {Object} the link_info is created, otherwise null + */ + connectByType(slot: number, target_node: LGraphNode, target_slotType: ISlotType, optsIn?: unknown): LLink | null { + optsIn = optsIn || {} + const optsDef = { createEventInCase: true, firstFreeIfOutputGeneralInCase: true, generalTypeInCase: true - }; - var opts = Object.assign(optsDef, optsIn); - if (target_node && target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); } - var target_slot = target_node.findInputSlotByType(target_slotType, false, true); + const opts = Object.assign(optsDef, optsIn) + if (target_node && typeof target_node === "number") { + target_node = this.graph.getNodeById(target_node) + } + let target_slot = target_node.findInputSlotByType(target_slotType, false, true) if (target_slot >= 0 && target_slot !== null) { //console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot) - return this.connect(slot, target_node, target_slot); + return this.connect(slot, target_node, target_slot) } else { //console.log("type "+target_slotType+" not found or not free?") if (opts.createEventInCase && target_slotType == LiteGraph.EVENT) { // WILL CREATE THE onTrigger IN SLOT //console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node); - return this.connect(slot, target_node, -1); + return this.connect(slot, target_node, -1) } // connect to the first general output slot if not found a specific type and if (opts.generalTypeInCase) { - var target_slot = target_node.findInputSlotByType(0, false, true, true); + target_slot = target_node.findInputSlotByType(0, false, true, true) //console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); if (target_slot >= 0) { - return this.connect(slot, target_node, target_slot); + return this.connect(slot, target_node, target_slot) } } // connect to the first free input slot if not found a specific type and this output is general if (opts.firstFreeIfOutputGeneralInCase && (target_slotType == 0 || target_slotType == "*" || target_slotType == "")) { - var target_slot = target_node.findInputSlotFree({ typesNotAccepted: [LiteGraph.EVENT] }); + target_slot = target_node.findInputSlotFree({ typesNotAccepted: [LiteGraph.EVENT] }) //console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); if (target_slot >= 0) { - return this.connect(slot, target_node, target_slot); + return this.connect(slot, target_node, target_slot) } } - console.debug("no way to connect type: ", target_slotType, " to targetNODE ", target_node); + console.debug("no way to connect type: ", target_slotType, " to targetNODE ", target_node) //TODO filter - return null; + return null } } /** - * connect this node input to the output of another node BY TYPE - * @method connectByType - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {string} target_type the output slot type of the target node - * @return {Object} the link_info is created, otherwise null - */ + * connect this node input to the output of another node BY TYPE + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} node the target node + * @param {string} target_type the output slot type of the target node + * @return {Object} the link_info is created, otherwise null + */ connectByTypeOutput(slot: number, source_node: LGraphNode, source_slotType: ISlotType, optsIn?: unknown): any { - var optsIn = optsIn || {}; - var optsDef = { + optsIn = optsIn || {} + const optsDef = { createEventInCase: true, firstFreeIfInputGeneralInCase: true, generalTypeInCase: true - }; - var opts = Object.assign(optsDef, optsIn); - if (source_node && source_node.constructor === Number) { - source_node = this.graph.getNodeById(source_node); } - var source_slot = source_node.findOutputSlotByType(source_slotType, false, true); + const opts = Object.assign(optsDef, optsIn) + if (source_node && typeof source_node === "number") { + source_node = this.graph.getNodeById(source_node) + } + let source_slot = source_node.findOutputSlotByType(source_slotType, false, true) if (source_slot >= 0 && source_slot !== null) { //console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot) - return source_node.connect(source_slot, this, slot); + return source_node.connect(source_slot, this, slot) } else { // connect to the first general output slot if not found a specific type and if (opts.generalTypeInCase) { - var source_slot = source_node.findOutputSlotByType(0, false, true, true); + source_slot = source_node.findOutputSlotByType(0, false, true, true) if (source_slot >= 0) { - return source_node.connect(source_slot, this, slot); + return source_node.connect(source_slot, this, slot) } } if (opts.createEventInCase && source_slotType == LiteGraph.EVENT) { // WILL CREATE THE onExecuted OUT SLOT if (LiteGraph.do_add_triggers_slots) { - var source_slot = source_node.addOnExecutedOutput(); - return source_node.connect(source_slot, this, slot); + source_slot = source_node.addOnExecutedOutput() + return source_node.connect(source_slot, this, slot) } } // connect to the first free output slot if not found a specific type and this input is general if (opts.firstFreeIfInputGeneralInCase && (source_slotType == 0 || source_slotType == "*" || source_slotType == "")) { - var source_slot = source_node.findOutputSlotFree({ typesNotAccepted: [LiteGraph.EVENT] }); + source_slot = source_node.findOutputSlotFree({ typesNotAccepted: [LiteGraph.EVENT] }) if (source_slot >= 0) { - return source_node.connect(source_slot, this, slot); + return source_node.connect(source_slot, this, slot) } } - console.debug("no way to connect byOUT type: ", source_slotType, " to sourceNODE ", source_node); + console.debug("no way to connect byOUT type: ", source_slotType, " to sourceNODE ", source_node) //TODO filter //console.log("type OUT! "+source_slotType+" not found or not free?") - return null; + return null } } /** - * connect this node output to the input of another node - * @method connect - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) - * @return {Object} the link_info is created, otherwise null - */ - connect(slot: number, target_node: LGraphNode, target_slot: ISlotType, reroutes?: RerouteId[]): LLink | null { - target_slot = target_slot || 0; + * connect this node output to the input of another node + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} node the target node + * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) + * @return {Object} the link_info is created, otherwise null + */ + connect(slot: number, target_node: LGraphNode, target_slot: ISlotType | false): LLink | null { + target_slot = target_slot || 0 if (!this.graph) { //could be connected before adding it to a graph console.log( "Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them." - ); //due to link ids being associated with graphs - return null; + ) //due to link ids being associated with graphs + return null } //seek for the output slot - if (slot.constructor === String) { - slot = this.findOutputSlot(slot); + if (typeof slot === "string") { + slot = this.findOutputSlot(slot) if (slot == -1) { if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); + console.log("Connect: Error, no slot of name " + slot) } - return null; + return null } } else if (!this.outputs || slot >= this.outputs.length) { if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); + console.log("Connect: Error, slot number not found") } - return null; + return null } - if (target_node && target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); + if (target_node && typeof target_node === "number") { + target_node = this.graph.getNodeById(target_node) } if (!target_node) { - throw "target node is null"; + throw "target node is null" } //avoid loopback if (target_node == this) { - return null; + return null } //you can specify the slot by name - if (target_slot.constructor === String) { - target_slot = target_node.findInputSlot(target_slot); + if (typeof target_slot === "string") { + target_slot = target_node.findInputSlot(target_slot) if (target_slot == -1) { if (LiteGraph.debug) { console.log( "Connect: Error, no slot of name " + target_slot - ); + ) } - return null; + return null } } else if (target_slot === LiteGraph.EVENT) { @@ -2084,43 +1817,43 @@ export class LGraphNode { //search for first slot with event? :: NO this is done outside //console.log("Connect: Creating triggerEvent"); // force mode - target_node.changeMode(LiteGraph.ON_TRIGGER); - target_slot = target_node.findInputSlot("onTrigger"); + target_node.changeMode(LiteGraph.ON_TRIGGER) + target_slot = target_node.findInputSlot("onTrigger") } else { - return null; // -- break -- + return null // -- break -- } } else if (!target_node.inputs || target_slot >= target_node.inputs.length) { if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); + console.log("Connect: Error, slot number not found") } - return null; + return null } - var changed = false; + let changed = false - var input = target_node.inputs[target_slot]; - var link_info = null; - var output = this.outputs[slot]; + const input = target_node.inputs[target_slot] + let link_info = null + const output = this.outputs[slot] if (!this.outputs[slot]) { /*console.debug("Invalid slot passed: "+slot); console.debug(this.outputs);*/ - return null; + return null } // allow target node to change slot if (target_node.onBeforeConnectInput) { // This way node can choose another slot (or make a new one?) - target_slot = target_node.onBeforeConnectInput(target_slot); //callback + target_slot = target_node.onBeforeConnectInput(target_slot) //callback } //check target_slot and check connection types if (target_slot === false || target_slot === null || !LiteGraph.isValidConnection(output.type, input.type)) { - this.setDirtyCanvas(false, true); + this.setDirtyCanvas(false, true) if (changed) - this.graph.connectionChange(this, link_info); - return null; + this.graph.connectionChange(this) + return null } else { //console.debug("valid connection",output.type, input.type); } @@ -2128,42 +1861,42 @@ export class LGraphNode { //allows nodes to block connection, callback if (target_node.onConnectInput) { if (target_node.onConnectInput(target_slot, output.type, output, this, slot) === false) { - return null; + return null } } if (this.onConnectOutput) { // callback if (this.onConnectOutput(slot, input.type, input, target_node, target_slot) === false) { - return null; + return null } } //if there is something already plugged there, disconnect if (target_node.inputs[target_slot] && target_node.inputs[target_slot].link != null) { - this.graph.beforeChange(); - target_node.disconnectInput(target_slot, { doProcessChange: false }); - changed = true; + this.graph.beforeChange() + target_node.disconnectInput(target_slot) + changed = true } if (output.links !== null && output.links.length) { switch (output.type) { case LiteGraph.EVENT: if (!LiteGraph.allow_multi_output_for_events) { - this.graph.beforeChange(); - this.disconnectOutput(slot, false, { doProcessChange: false }); // Input(target_slot, {doProcessChange: false}); - changed = true; + this.graph.beforeChange() + this.disconnectOutput(slot) + changed = true } - break; + break default: - break; + break } } - var nextId; + let nextId if (LiteGraph.use_uuids) - nextId = LiteGraph.uuidv4(); + nextId = LiteGraph.uuidv4() else - nextId = ++this.graph.last_link_id; + nextId = ++this.graph.last_link_id //create link class link_info = new LLink( @@ -2173,20 +1906,20 @@ export class LGraphNode { slot, target_node.id, target_slot - ); + ) //add to graph links list - this.graph.links[link_info.id] = link_info; + this.graph.links[link_info.id] = link_info //connect in output if (output.links == null) { - output.links = []; + output.links = [] } - output.links.push(link_info.id); + output.links.push(link_info.id) //connect in input - target_node.inputs[target_slot].link = link_info.id; + target_node.inputs[target_slot].link = link_info.id if (this.graph) { - this.graph._version++; + this.graph._version++ } if (this.onConnectionsChange) { this.onConnectionsChange( @@ -2195,7 +1928,7 @@ export class LGraphNode { true, link_info, output - ); + ) } //link_info has been created now, so its updated if (target_node.onConnectionsChange) { target_node.onConnectionsChange( @@ -2204,7 +1937,7 @@ export class LGraphNode { true, link_info, input - ); + ) } if (this.graph && this.graph.onNodeConnectionChange) { this.graph.onNodeConnectionChange( @@ -2213,539 +1946,412 @@ export class LGraphNode { target_slot, this, slot - ); + ) this.graph.onNodeConnectionChange( LiteGraph.OUTPUT, this, slot, target_node, target_slot - ); + ) } - this.setDirtyCanvas(false, true); - this.graph.afterChange(); - this.graph.connectionChange(this, link_info); + this.setDirtyCanvas(false, true) + this.graph.afterChange() + this.graph.connectionChange(this) - return link_info; + return link_info } /** - * disconnect one output to an specific node - * @method disconnectOutput - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] - * @return {boolean} if it was disconnected successfully - */ + * disconnect one output to an specific node + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] + * @return {boolean} if it was disconnected successfully + */ disconnectOutput(slot: string | number, target_node?: LGraphNode): boolean { - if (slot.constructor === String) { - slot = this.findOutputSlot(slot); + if (typeof slot === "string") { + slot = this.findOutputSlot(slot) if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return false; + if (LiteGraph.debug) console.log("Connect: Error, no slot of name " + slot) + return false } } else if (!this.outputs || slot >= this.outputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return false; + if (LiteGraph.debug) console.log("Connect: Error, slot number not found") + return false } //get output slot - var output = this.outputs[slot]; - if (!output || !output.links || output.links.length == 0) { - return false; - } + const output = this.outputs[slot] + if (!output || !output.links || output.links.length == 0) + return false //one of the output links in this slot + const graph = this.graph if (target_node) { - if (target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); - } - if (!target_node) { - throw "Target Node not found"; - } + if (typeof target_node === "number") + target_node = graph.getNodeById(target_node) + if (!target_node) + throw "Target Node not found" - for (var i = 0, l = output.links.length; i < l; i++) { - var link_id = output.links[i]; - var link_info = this.graph.links[link_id]; + for (let i = 0, l = output.links.length; i < l; i++) { + const link_id = output.links[i] + const link_info = graph.links[link_id] //is the link we are searching for... if (link_info.target_id == target_node.id) { - output.links.splice(i, 1); //remove here - var input = target_node.inputs[link_info.target_slot]; - input.link = null; //remove there - delete this.graph.links[link_id]; //remove the link from the links pool - if (this.graph) { - this.graph._version++; - } - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - link_info.target_slot, - false, - link_info, - input - ); - } //link_info hasn't been modified so its ok - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - break; + output.links.splice(i, 1) //remove here + const input = target_node.inputs[link_info.target_slot] + input.link = null //remove there + + delete graph.links[link_id] //remove the link from the links pool //remove the link from the links pool + if (graph) graph._version++ + + //link_info hasn't been modified so its ok + target_node.onConnectionsChange?.( + LiteGraph.INPUT, + link_info.target_slot, + false, + link_info, + input + ) + this.onConnectionsChange?.( + LiteGraph.OUTPUT, + slot, + false, + link_info, + output + ) + + // FIXME: Called twice. + graph?.onNodeConnectionChange?.(LiteGraph.OUTPUT, this, slot) + graph?.onNodeConnectionChange?.(LiteGraph.OUTPUT, this, slot) + graph?.onNodeConnectionChange?.(LiteGraph.INPUT, target_node, link_info.target_slot) + break } } } //all the links in this output slot else { - for (var i = 0, l = output.links.length; i < l; i++) { - var link_id = output.links[i]; - var link_info = this.graph.links[link_id]; - if (!link_info) { - //bug: it happens sometimes - continue; - } + for (let i = 0, l = output.links.length; i < l; i++) { + const link_id = output.links[i] + const link_info = graph.links[link_id] + //bug: it happens sometimes + if (!link_info) continue + + target_node = graph.getNodeById(link_info.target_id) + if (graph) graph._version++ - var target_node = this.graph.getNodeById(link_info.target_id); - var input = null; - if (this.graph) { - this.graph._version++; - } if (target_node) { - input = target_node.inputs[link_info.target_slot]; - input.link = null; //remove other side link - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - link_info.target_slot, - false, - link_info, - input - ); - } //link_info hasn't been modified so its ok - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - } - delete this.graph.links[link_id]; //remove the link from the links pool - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - this.graph.onNodeConnectionChange( + const input = target_node.inputs[link_info.target_slot] + //remove other side link + input.link = null + + //link_info hasn't been modified so its ok + target_node.onConnectionsChange?.( LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - } - output.links = null; - } - - this.setDirtyCanvas(false, true); - this.graph.connectionChange(this); - return true; - } - - /** - * disconnect one input - * @method disconnectInput - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @return {boolean} if it was disconnected successfully - */ - disconnectInput(slot: number | string): boolean { - //seek for the output slot - if (slot.constructor === String) { - slot = this.findInputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return false; - } - } else if (!this.inputs || slot >= this.inputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return false; - } - - var input = this.inputs[slot]; - if (!input) { - return false; - } - - var link_id = this.inputs[slot].link; - if (link_id != null) { - this.inputs[slot].link = null; - - //remove other side - var link_info = this.graph.links[link_id]; - if (link_info) { - var target_node = this.graph.getNodeById(link_info.origin_id); - if (!target_node) { - return false; - } - - var output = target_node.outputs[link_info.origin_slot]; - if (!output || !output.links || output.links.length == 0) { - return false; - } - - //search in the inputs list for this link - for (var i = 0, l = output.links.length; i < l; i++) { - if (output.links[i] == link_id) { - output.links.splice(i, 1); - break; - } - } - - delete this.graph.links[link_id]; //remove from the pool - if (this.graph) { - this.graph._version++; - } - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.INPUT, - slot, + link_info.target_slot, false, link_info, input - ); + ) + // FIXME: Called twice. + graph?.onNodeConnectionChange?.(LiteGraph.INPUT, target_node, link_info.target_slot) } - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.OUTPUT, - i, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - target_node, - i - ); - this.graph.onNodeConnectionChange(LiteGraph.INPUT, this, slot); - } - } - } //link != null + //remove the link from the links pool + delete graph.links[link_id] - this.setDirtyCanvas(false, true); - if (this.graph) - this.graph.connectionChange(this); - return true; + this.onConnectionsChange?.( + LiteGraph.OUTPUT, + slot, + false, + link_info, + output + ) + graph?.onNodeConnectionChange?.(LiteGraph.OUTPUT, this, slot) + graph?.onNodeConnectionChange?.(LiteGraph.INPUT, target_node, link_info.target_slot) + } + output.links = null + } + + this.setDirtyCanvas(false, true) + graph.connectionChange(this) + return true } /** - * returns the center of a connection point in canvas coords - * @method getConnectionPos - * @param {boolean} is_input true if if a input slot, false if it is an output - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {vec2} out [optional] a place to store the output, to free garbage - * @return {[x,y]} the position - **/ - getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point { - out = out || new Float32Array(2); - var num_slots = 0; - if (is_input && this.inputs) { - num_slots = this.inputs.length; - } - if (!is_input && this.outputs) { - num_slots = this.outputs.length; + * Disconnect one input + * @param slot Input slot index, or the name of the slot + * @return true if disconnected successfully or already disconnected, otherwise false + */ + disconnectInput(slot: number | string): boolean { + // Allow search by string + if (typeof slot === "string") { + slot = this.findInputSlot(slot) + if (slot == -1) { + if (LiteGraph.debug) console.log("Connect: Error, no slot of name " + slot) + return false + } + } else if (!this.inputs || slot >= this.inputs.length) { + if (LiteGraph.debug) { + console.log("Connect: Error, slot number not found") + } + return false } - var offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5; + const input = this.inputs[slot] + if (!input) { + return false + } + + const link_id = this.inputs[slot].link + if (link_id != null) { + this.inputs[slot].link = null + + //remove other side + const link_info = this.graph.links[link_id] + if (link_info) { + const target_node = this.graph.getNodeById(link_info.origin_id) + if (!target_node) { + return false + } + + const output = target_node.outputs[link_info.origin_slot] + if (!(output?.links?.length > 0)) { + return false + } + + //search in the inputs list for this link + let i = 0 + for (const l = output.links.length; i < l; i++) { + if (output.links[i] == link_id) { + output.links.splice(i, 1) + break + } + } + + delete this.graph.links[link_id] //remove from the pool + if (this.graph) this.graph._version++ + + this.onConnectionsChange?.( + LiteGraph.INPUT, + slot, + false, + link_info, + input + ) + target_node.onConnectionsChange?.( + LiteGraph.OUTPUT, + i, + false, + link_info, + output + ) + this.graph?.onNodeConnectionChange?.(LiteGraph.OUTPUT, target_node, i) + this.graph?.onNodeConnectionChange?.(LiteGraph.INPUT, this, slot) + } + } + + this.setDirtyCanvas(false, true) + this.graph?.connectionChange(this) + return true + } + + /** + * returns the center of a connection point in canvas coords + * @param {boolean} is_input true if if a input slot, false if it is an output + * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) + * @param {vec2} out [optional] a place to store the output, to free garbage + * @return {[x,y]} the position + **/ + getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point { + out ||= new Float32Array(2) + + const num_slots = is_input + ? this.inputs?.length ?? 0 + : this.outputs?.length ?? 0 + + const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5 if (this.flags.collapsed) { - var w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; + const w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH if (this.horizontal) { - out[0] = this.pos[0] + w * 0.5; - if (is_input) { - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - } else { - out[1] = this.pos[1]; - } + out[0] = this.pos[0] + w * 0.5 + out[1] = is_input + ? this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + : this.pos[1] } else { - if (is_input) { - out[0] = this.pos[0]; - } else { - out[0] = this.pos[0] + w; - } - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5; + out[0] = is_input + ? this.pos[0] + : this.pos[0] + w + out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5 } - return out; + return out } //weird feature that never got finished if (is_input && slot_number == -1) { - out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; - out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; - return out; + out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5 + out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5 + return out } //hard-coded pos if (is_input && num_slots > slot_number && this.inputs[slot_number].pos) { - out[0] = this.pos[0] + this.inputs[slot_number].pos[0]; - out[1] = this.pos[1] + this.inputs[slot_number].pos[1]; - return out; + + out[0] = this.pos[0] + this.inputs[slot_number].pos[0] + out[1] = this.pos[1] + this.inputs[slot_number].pos[1] + return out } else if (!is_input && num_slots > slot_number && this.outputs[slot_number].pos) { - out[0] = this.pos[0] + this.outputs[slot_number].pos[0]; - out[1] = this.pos[1] + this.outputs[slot_number].pos[1]; - return out; + + out[0] = this.pos[0] + this.outputs[slot_number].pos[0] + out[1] = this.pos[1] + this.outputs[slot_number].pos[1] + return out } //horizontal distributed slots if (this.horizontal) { - out[0] = - this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots); - if (is_input) { - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - } else { - out[1] = this.pos[1] + this.size[1]; - } - return out; + out[0] = this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots) + out[1] = is_input + ? this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + : this.pos[1] + this.size[1] + return out } //default vertical slots - if (is_input) { - out[0] = this.pos[0] + offset; - } else { - out[0] = this.pos[0] + this.size[0] + 1 - offset; - } + out[0] = is_input + ? this.pos[0] + offset + : this.pos[0] + this.size[0] + 1 - offset out[1] = this.pos[1] + (slot_number + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + - (this.constructor.slot_start_y || 0); - return out; + (this.constructor.slot_start_y || 0) + return out } /* Force align to grid */ alignToGrid(): void { - this.pos[0] = - LiteGraph.CANVAS_GRID_SIZE * - Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE); - this.pos[1] = - LiteGraph.CANVAS_GRID_SIZE * - Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE); + this.pos[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE) + this.pos[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE) } /* Console output */ - trace(msg?: string) { - if (!this.console) { - this.console = []; - } + trace(msg?: string): void { + this.console ||= [] + this.console.push(msg) + if (this.console.length > LGraphNode.MAX_CONSOLE) + this.console.shift() - this.console.push(msg); - if (this.console.length > LGraphNode.MAX_CONSOLE) { - this.console.shift(); - } - - if (this.graph.onNodeTrace) - this.graph.onNodeTrace(this, msg); + this.graph.onNodeTrace?.(this, msg) } /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ - setDirtyCanvas(dirty_foreground: boolean, dirty_background?: boolean) { - if (!this.graph) { - return; - } - this.graph.sendActionToCanvas("setDirty", [ + setDirtyCanvas(dirty_foreground: boolean, dirty_background?: boolean): void { + this.graph?.sendActionToCanvas("setDirty", [ dirty_foreground, dirty_background - ]); + ]) } - loadImage(url: string): any { - var img = new Image(); - img.src = LiteGraph.node_images_path + url; - img.ready = false; + loadImage(url: string): HTMLImageElement { + interface AsyncImageElement extends HTMLImageElement { ready?: boolean } - var that = this; - img.onload = function () { - this.ready = true; - that.setDirtyCanvas(true); - }; - return img; + const img: AsyncImageElement = new Image() + img.src = LiteGraph.node_images_path + url + img.ready = false + + const that = this + img.onload = function (this: AsyncImageElement) { + this.ready = true + that.setDirtyCanvas(true) + } + return img } - //safe LGraphNode action execution (not sure if safe) - /* - LGraphNode.prototype.executeAction = function(action) - { - if(action == "") return false; - - if( action.indexOf(";") != -1 || action.indexOf("}") != -1) - { - this.trace("Error: Action contains unsafe characters"); - return false; - } - - var tokens = action.split("("); - var func_name = tokens[0]; - if( typeof(this[func_name]) != "function") - { - this.trace("Error: Action not found on node: " + func_name); - return false; - } - - var code = action; - - try - { - var _foo = eval; - eval = null; - (new Function("with(this) { " + code + "}")).call(this); - eval = _foo; - } - catch (err) - { - this.trace("Error executing action {" + action + "} :" + err); - return false; - } - - return true; - } - */ /* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ captureInput(v: boolean): void { - if (!this.graph || !this.graph.list_of_graphcanvas) { - return; - } + if (!this.graph || !this.graph.list_of_graphcanvas) + return - var list = this.graph.list_of_graphcanvas; + const list = this.graph.list_of_graphcanvas - for (var i = 0; i < list.length; ++i) { - var c = list[i]; + for (let i = 0; i < list.length; ++i) { + const c = list[i] //releasing somebody elses capture?! - if (!v && c.node_capturing_input != this) { - continue; - } + if (!v && c.node_capturing_input != this) + continue //change - c.node_capturing_input = v ? this : null; + c.node_capturing_input = v ? this : null } } get collapsed() { - return !!this.flags.collapsed; + return !!this.flags.collapsed } get collapsible() { - return !this.pinned && (this.constructor.collapsable !== false); + return !this.pinned && (this.constructor.collapsable !== false) } /** - * Collapse the node to make it smaller on the canvas - * @method collapse - **/ + * Collapse the node to make it smaller on the canvas + **/ collapse(force?: boolean): void { - this.graph._version++; - if (!this.collapsible && !force) { - return; - } - if (!this.flags.collapsed) { - this.flags.collapsed = true; - } else { - this.flags.collapsed = false; - } - this.setDirtyCanvas(true, true); + this.graph._version++ + if (!this.collapsible && !force) return + this.flags.collapsed = !this.flags.collapsed + this.setDirtyCanvas(true, true) } get pinned() { - return !!this.flags.pinned; + return !!this.flags.pinned } /** - * Forces the node to do not move or realign on Z or resize - * @method pin - **/ - pin(v?) { - this.graph._version++; - if (v === undefined) { - this.flags.pinned = !this.flags.pinned; - } else { - this.flags.pinned = v; - } - this.resizable = !this.pinned; + * Prevents the node being accidentally moved or resized by mouse interaction. + **/ + pin(v?: boolean): void { + this.graph._version++ + this.flags.pinned = v === undefined + ? !this.flags.pinned + : v + this.resizable = !this.pinned // Delete the flag if unpinned, so that we don't get unnecessary // flags.pinned = false in serialized object. - if (!this.pinned) { - delete this.flags.pinned; - } + if (!this.pinned) + delete this.flags.pinned } localToScreen(x: number, y: number, dragAndScale: DragAndScale): Point { return [ (x + this.pos[0]) * dragAndScale.scale + dragAndScale.offset[0], (y + this.pos[1]) * dragAndScale.scale + dragAndScale.offset[1] - ]; + ] } get width() { - return this.collapsed ? this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH : this.size[0]; + return this.collapsed ? this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH : this.size[0] } get height() { - return this.collapsed ? LiteGraph.NODE_COLLAPSED_HEIGHT : this.size[1]; + // @ts-expect-error Not impl. + return this.collapsed ? LiteGraph.NODE_COLLAPSED_HEIGHT : this.size[1] } - drawBadges(ctx, { gap = 2 } = {}) { - const badgeInstances = this.badges.map(badge => badge instanceof LGraphBadge ? badge : badge()); - const isLeftAligned = this.badgePosition === BadgePosition.TopLeft; + drawBadges(ctx: CanvasRenderingContext2D, { gap = 2 } = {}): void { + const badgeInstances = this.badges.map(badge => badge instanceof LGraphBadge ? badge : badge()) + const isLeftAligned = this.badgePosition === BadgePosition.TopLeft - let currentX = isLeftAligned ? 0 : this.width - badgeInstances.reduce((acc, badge) => acc + badge.getWidth(ctx) + gap, 0); - const y = -(LiteGraph.NODE_TITLE_HEIGHT + gap); + let currentX = isLeftAligned ? 0 : this.width - badgeInstances.reduce((acc, badge) => acc + badge.getWidth(ctx) + gap, 0) + const y = -(LiteGraph.NODE_TITLE_HEIGHT + gap) for (const badge of badgeInstances) { - badge.draw(ctx, currentX, y - badge.height); - currentX += badge.getWidth(ctx) + gap; + badge.draw(ctx, currentX, y - badge.height) + currentX += badge.getWidth(ctx) + gap } } } diff --git a/src/litegraph.ts b/src/litegraph.ts index ad105332d..156f6859f 100644 --- a/src/litegraph.ts +++ b/src/litegraph.ts @@ -99,6 +99,13 @@ export interface LiteGraphCanvasGroupEvent extends CustomEvent<{ /** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */ export interface LGraphNodeConstructor { + title?: string + type?: string + size?: Size + min_height?: number + slot_start_y?: number + widgets_info?: any + collapsable?: boolean nodeData: any new(): T } diff --git a/src/types/serialisation.ts b/src/types/serialisation.ts index 4faf1fd72..4ecf3324d 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -5,6 +5,7 @@ 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" /** Serialised LGraphNode */ export interface ISerialisedNode { @@ -19,7 +20,7 @@ export interface ISerialisedNode { outputs?: INodeOutputSlot[] inputs?: INodeInputSlot[] properties?: Dictionary - shape?: Rect + shape?: RenderShape boxcolor?: CanvasColour color?: CanvasColour bgcolor?: string