import type { Dictionary, IContextMenuValue, IFoundSlot, INodeFlags, INodeInputSlot, INodeOutputSlot, IOptionalSlotData, ISlotType, Point, Rect, Size } from "./interfaces" import type { LGraph } from "./LGraph" import type { IWidget, TWidgetValue } from "./types/widgets" import type { ISerialisedNode } from "./types/serialisation" import { RenderShape } from "./types/globalEnums" import type { LGraphCanvas } from "./LGraphCanvas" import type { CanvasMouseEvent } from "./types/events" 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 export interface INodePropertyInfo { name: string type: string default_value: unknown } export type INodeProperties = Dictionary & { horizontal?: boolean } interface IMouseOverData { inputId: number | null outputId: number | null overWidget: IWidget | null } interface ConnectByTypeOptions { /** @deprecated Events */ createEventInCase?: boolean /** Allow our wildcard slot to connect to typed slots on remote node. Default: true */ wildcardToTyped?: boolean /** Allow our typed slot to connect to wildcard slots on remote node. Default: true */ typedToWildcard?: boolean } /** Internal type used for type safety when implementing generic checks for inputs & outputs */ interface IGenericLinkOrLinks { links?: INodeOutputSlot["links"] link?: INodeInputSlot["link"] } interface FindFreeSlotOptions { /** Slots matching these types will be ignored. Default: [] */ typesNotAccepted?: ISlotType[] /** If true, the slot itself is returned instead of the index. Default: false */ returnObj?: boolean } /* title: string pos: [x,y] size: [x,y] input|output: every connection + { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array }); general properties: + clip_area: if you render outside the node, it will be clipped + unsafe_execution: not allowed for safe execution + skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected + resizable: if set to false it wont be resizable with the mouse + horizontal: slots are distributed horizontally + widgets_start_y: widgets start at y distance from the top of the node flags object: + collapsed: if it is collapsed supported callbacks: + onAdded: when added to graph (warning: this is called BEFORE the node is configured when loading) + onRemoved: when removed from graph + onStart: when the graph starts playing + onStop: when the graph stops playing + onDrawForeground: render the inside widgets inside the node + onDrawBackground: render the background area inside the node (only in edit mode) + onMouseDown + onMouseMove + onMouseUp + onMouseEnter + onMouseLeave + onExecute: execute the node + onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour) + onGetInputs: returns an array of possible inputs + onGetOutputs: returns an array of possible outputs + onBounding: in case this node has a bigger bounding than the node itself (the callback receives the bounding as [x,y,w,h]) + onDblClick: double clicked in the node + onNodeTitleDblClick: double clicked in the node title + onInputDblClick: input slot double clicked (can be used to automatically create a node connected) + onOutputDblClick: output slot double clicked (can be used to automatically create a node connected) + onConfigure: called after the node has been configured + onSerialize: to add extra info when serializing (the callback receives the object that should be filled with the data) + onSelected + onDeselected + onDropItem : DOM item dropped over the node + onDropFile : file dropped over the node + onConnectInput : if returns false the incoming connection will be canceled + onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info ) + onAction: action slot triggered + getExtraMenuOptions: to add option to context menu */ export interface LGraphNode { constructor: LGraphNodeConstructor } /** * Base Class for all the node type classes * @param {String} name a name for the node */ export class LGraphNode { // Static properties used by dynamic child classes static title?: string static MAX_CONSOLE?: number static type?: string static category?: string static supported_extensions?: string[] static filter?: string static skip_list?: boolean title?: string graph: LGraph id?: NodeId type?: string inputs: INodeInputSlot[] outputs: INodeOutputSlot[] // Not used connections: unknown[] properties: INodeProperties properties_info: INodePropertyInfo[] flags?: INodeFlags widgets?: IWidget[] size: Point pos: Point _pos: Point locked?: boolean // Execution order, automatically computed during run order?: number mode: number last_serialization?: ISerialisedNode serialize_widgets?: boolean color: string bgcolor: string boxcolor: string exec_version: number action_call?: string execute_triggered: number action_triggered: number widgets_up?: boolean widgets_start_y?: number lostFocusAt?: number gotFocusAt?: number badges: (LGraphBadge | (() => LGraphBadge))[] badgePosition: BadgePosition onOutputRemoved?(this: LGraphNode, slot: number): void onInputRemoved?(this: LGraphNode, slot: number, input: INodeInputSlot): void _collapsed_width: number onBounding?(this: LGraphNode, out: Rect): void horizontal?: boolean console?: string[] _level: number _shape?: RenderShape subgraph?: LGraph skip_subgraph_button?: boolean mouseOver?: IMouseOverData is_selected?: boolean redraw_on_mouse?: boolean // Appears unused optional_inputs? // Appears unused optional_outputs? resizable?: boolean clonable?: boolean _relative_id?: number clip_area?: boolean ignore_remove?: boolean has_errors?: boolean removable?: boolean block_delete?: boolean get shape(): RenderShape { return this._shape } set shape(v: RenderShape | "default" | "box" | "round" | "circle" | "card") { switch (v) { case "default": delete this._shape break case "box": this._shape = RenderShape.BOX break case "round": this._shape = RenderShape.ROUND break case "circle": this._shape = RenderShape.CIRCLE break case "card": this._shape = RenderShape.CARD break default: this._shape = v } } // Used in group node setInnerNodes?(this: LGraphNode, nodes: LGraphNode[]): void onConnectInput?(this: LGraphNode, target_slot: number, type: unknown, output: INodeOutputSlot, node: LGraphNode, slot: number): boolean onConnectOutput?(this: LGraphNode, slot: number, type: unknown, input: INodeInputSlot, target_node: number | LGraphNode, target_slot: number): boolean onResize?(this: LGraphNode, size: Size): void onPropertyChanged?(this: LGraphNode, name: string, value: unknown, prev_value?: unknown): boolean onConnectionsChange?(this: LGraphNode, type: ISlotType, index: number, isConnected: boolean, link_info: LLink, inputOrOutput: INodeInputSlot | INodeOutputSlot): void onInputAdded?(this: LGraphNode, input: INodeInputSlot): void onOutputAdded?(this: LGraphNode, output: INodeOutputSlot): void onConfigure?(this: LGraphNode, serialisedNode: ISerialisedNode): void onSerialize?(this: LGraphNode, serialised: ISerialisedNode): any onExecute?(this: LGraphNode, param?: unknown, options?: { action_call?: any }): void onAction?(this: LGraphNode, action: string, param: unknown, options: { action_call?: string }): void onDrawBackground?(this: LGraphNode, ctx: CanvasRenderingContext2D, canvas: LGraphCanvas, canvasElement: HTMLCanvasElement, mousePosition: Point): void onNodeCreated?(this: LGraphNode): void /** * Callback invoked by {@link connect} to override the target slot index. Its return value overrides the target index selection. * @param target_slot The current input slot index * @param requested_slot The originally requested slot index - could be negative, or if using (deprecated) name search, a string * @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 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 onDeselected?(this: LGraphNode): void onKeyUp?(this: LGraphNode, e: KeyboardEvent): void onKeyDown?(this: LGraphNode, e: KeyboardEvent): void onSelected?(this: LGraphNode): void getExtraMenuOptions?(this: LGraphNode, canvas: LGraphCanvas, options: IContextMenuValue[]): IContextMenuValue[] getMenuOptions?(this: LGraphNode, canvas: LGraphCanvas): IContextMenuValue[] onAdded?(this: LGraphNode, graph: LGraph): void onDrawCollapsed?(this: LGraphNode, ctx: CanvasRenderingContext2D, cavnas: LGraphCanvas): boolean onDrawForeground?(this: LGraphNode, ctx: CanvasRenderingContext2D, canvas: LGraphCanvas, canvasElement: HTMLCanvasElement): void onMouseLeave?(this: LGraphNode, e: CanvasMouseEvent): void getSlotMenuOptions?(this: LGraphNode, slot: IFoundSlot): IContextMenuValue[] // FIXME: Re-typing onDropItem?(this: LGraphNode, event: Event): boolean onDropData?(this: LGraphNode, data: string | ArrayBuffer, filename: any, file: any): void onDropFile?(this: LGraphNode, file: any): void onInputClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void onInputDblClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void onOutputClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void onOutputDblClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void // TODO: Return type onGetPropertyInfo?(this: LGraphNode, property: string): any onNodeOutputAdd?(this: LGraphNode, value): void onNodeInputAdd?(this: LGraphNode, value): void onMenuNodeInputs?(this: LGraphNode, entries: IOptionalSlotData[]): IOptionalSlotData[] onMenuNodeOutputs?(this: LGraphNode, entries: IOptionalSlotData[]): IOptionalSlotData[] onGetInputs?(this: LGraphNode): INodeInputSlot[] onGetOutputs?(this: LGraphNode): INodeOutputSlot[] onMouseUp?(this: LGraphNode, e: CanvasMouseEvent, pos: Point): void onMouseEnter?(this: LGraphNode, e: CanvasMouseEvent): void onMouseDown?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): boolean onDblClick?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): void onNodeTitleDblClick?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): void onDrawTitle?(this: LGraphNode, ctx: CanvasRenderingContext2D): void onDrawTitleText?(this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, scale: number, title_text_font: string, selected: boolean): void onDrawTitleBox?(this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, scale: number): void onDrawTitleBar?(this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, scale: number, fgcolor: any): void onRemoved?(this: LGraphNode): void onMouseMove?(this: LGraphNode, e: MouseEvent, pos: Point, arg2: LGraphCanvas): void onPropertyChange?(this: LGraphNode): void updateOutputData?(this: LGraphNode, origin_slot: number): void isValidWidgetLink?(slot_index: number, node: LGraphNode, overWidget: IWidget): boolean | undefined constructor(title: string) { this._ctor(title) } _ctor(title: string): void { 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]) Object.defineProperty(this, "pos", { set: function (v) { if (!v || v.length < 2) { return } this._pos[0] = v[0] this._pos[1] = v[1] }, get: function () { return this._pos }, enumerable: true }) if (LiteGraph.use_uuids) { this.id = LiteGraph.uuidv4() } else { this.id = -1 //not know till not added } this.type = null //inputs available: array of inputs 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.flags = {} } /** * configure a node from an object containing the serialized info */ configure(info: ISerialisedNode): void { if (this.graph) { this.graph._version++ } for (const j in info) { if (j == "properties") { //i don't want to clone properties, I want to reuse the old container for (const k in info.properties) { this.properties[k] = info.properties[k] this.onPropertyChanged?.(k, info.properties[k]) } continue } if (info[j] == null) { continue } else if (typeof info[j] == "object") { //object if (this[j]?.configure) { this[j]?.configure(info[j]) } else { this[j] = LiteGraph.cloneObject(info[j], this[j]) } } //value else { this[j] = info[j] } } if (!info.title) { this.title = this.constructor.title } if (this.inputs) { 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 (let i = 0; i < this.outputs.length; ++i) { const output = this.outputs[i] if (!output.links) { continue } 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) } this.onOutputAdded?.(output) } } if (this.widgets) { for (let i = 0; i < this.widgets.length; ++i) { const w = this.widgets[i] if (!w) continue if (w.options?.property && (this.properties[w.options.property] != undefined)) w.value = JSON.parse(JSON.stringify(this.properties[w.options.property])) } if (info.widgets_values) { for (let i = 0; i < info.widgets_values.length; ++i) { if (this.widgets[i]) { this.widgets[i].value = info.widgets_values[i] } } } } // Sync the state of this.resizable. if (this.pinned) this.pin(true) this.onConfigure?.(info) } /** * serialize the content */ serialize(): ISerialisedNode { //create serialization object const o: ISerialisedNode = { id: this.id, type: this.type, pos: this.pos, size: this.size, 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.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 (let i = 0; i < this.outputs.length; i++) { delete this.outputs[i]._data } o.outputs = this.outputs } if (this.title && this.title != this.constructor.title) o.title = this.title if (this.properties) o.properties = LiteGraph.cloneObject(this.properties) if (this.widgets && this.serialize_widgets) { o.widgets_values = [] for (let i = 0; i < this.widgets.length; ++i) { if (this.widgets[i]) o.widgets_values[i] = this.widgets[i].value else o.widgets_values[i] = null } } 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.onSerialize?.(o)) console.warn("node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter") return o } /* Creates a clone of this node */ clone(): LGraphNode { const node = LiteGraph.createNode(this.type) if (!node) return null //we clone it because serialize returns shared containers const data = LiteGraph.cloneObject(this.serialize()) //remove links if (data.inputs) { for (let i = 0; i < data.inputs.length; ++i) { data.inputs[i].link = null } } if (data.outputs) { for (let i = 0; i < data.outputs.length; ++i) { if (data.outputs[i].links) { data.outputs[i].links.length = 0 } } } delete data.id if (LiteGraph.use_uuids) data.id = LiteGraph.uuidv4() //remove links node.configure(data) return node } /** * serialize and stringify */ toString(): string { return JSON.stringify(this.serialize()) } /** * get the title string */ getTitle(): string { return this.title || this.constructor.title } /** * sets the value of a property * @param {String} name * @param {*} value */ setProperty(name: string, value: TWidgetValue): void { this.properties ||= {} if (value === this.properties[name]) 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 (let i = 0; i < this.widgets.length; ++i) { const w = this.widgets[i] if (!w) continue if (w.options.property == name) { w.value = value break } } } /** * sets the output data * @param {number} slot * @param {*} data */ setOutputData(slot: number, data: unknown): void { 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 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 //if there are connections, pass the data to the connections if (this.outputs[slot].links) { 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 } } } /** * 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 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 //if there are connections, pass the data to the connections if (this.outputs[slot].links) { 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 * @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 if (slot >= this.inputs.length || this.inputs[slot].link == null) return 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 //special case: used to extract data from the incoming connection before the graph has been executed const node = this.graph.getNodeById(link.origin_id) if (!node) return link.data if (node.updateOutputData) { node.updateOutputData(link.origin_slot) } else { node.onExecute?.() } return link.data } /** * 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 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 * @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 { 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 * @param {number} slot * @return {boolean} */ isInputConnected(slot: number): boolean { 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) * @param {number} slot * @return {Object} object or null { link: id, name: string, type: string or 0 } */ getInputInfo(slot: number): INodeInputSlot { return !this.inputs || !(slot < this.inputs.length) ? null : this.inputs[slot] } /** * 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 (slot < this.inputs.length) { const slot_info = this.inputs[slot] return this.graph.links[slot_info.link] } return 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 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 * @param {string} name * @return {*} value */ getInputOrProperty(name: string): unknown { if (!this.inputs || !this.inputs.length) { return this.properties ? this.properties[name] : null } 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) { const link = this.graph.links[input_info.link] if (link) return link.data } } return this.properties[name] } /** * 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 const info = this.outputs[slot] return info._data } /** * 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 { return !this.outputs || !(slot < this.outputs.length) ? null : this.outputs[slot] } /** * 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?.length > 0 } /** * tells you if there is any connection in the output slots * @return {boolean} */ isAnyOutputConnected(): boolean { 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 false } /** * 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 (slot >= this.outputs.length) return null const output = this.outputs[slot] if (!output.links || output.links.length == 0) return null 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) { const target_node = this.graph.getNodeById(link.target_id) if (target_node) { r.push(target_node) } } } return r } addOnTriggerInput(): number { const trigS = this.findInputSlot("onTrigger") if (trigS == -1) { //!trigS || const input = this.addInput("onTrigger", LiteGraph.EVENT, { optional: true, nameLocked: true }) return this.findInputSlot("onTrigger") } return trigS } addOnExecutedOutput(): number { const trigS = this.findOutputSlot("onExecuted") if (trigS == -1) { //!trigS || const output = this.addOutput("onExecuted", LiteGraph.ACTION, { optional: true, nameLocked: true }) return this.findOutputSlot("onExecuted") } return trigS } onAfterExecuteNode(param: unknown, options?: { action_call?: any }) { 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) } } changeMode(modeTo: number): boolean { switch (modeTo) { case LiteGraph.ON_EVENT: // this.addOnExecutedOutput(); break case LiteGraph.ON_TRIGGER: this.addOnTriggerInput() this.addOnExecutedOutput() break case LiteGraph.NEVER: break case LiteGraph.ALWAYS: break // @ts-expect-error Not impl. case LiteGraph.ON_REQUEST: break default: return false break } this.mode = modeTo return true } /** * 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 || {} if (this.onExecute) { // enable this to give the event an ID 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?.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 this.onAfterExecuteNode?.(param, options) // callback } /** * 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 || {} if (this.onAction) { // enable this to give the event an ID 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?.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 this.onAfterExecuteNode?.(param, options) } /** * 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 } if (this.graph) this.graph._last_trigger_time = LiteGraph.getTime() 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) } } /** * 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 if (slot == null) { console.error("slot must be a number") return } if (typeof slot !== "number") console.warn("slot must be a number, use node.trigger('name') if you want to use a string") const output = this.outputs[slot] if (!output) return const links = output.links if (!links || !links.length) return if (this.graph) this.graph._last_trigger_time = LiteGraph.getTime() //for every link attached here 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 = 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) // -- 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) //pass the action name const target_connection = node.inputs[link_info.target_slot] // wrap node.onAction(target_connection.name, param); node.actionDo(target_connection.name, param, options) } } } /** * 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 const output = this.outputs[slot] if (!output) return const links = output.links if (!links || !links.length) return //for every link attached here 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 * @param {vec2} size */ setSize(size: Size): void { this.size = size this.onResize?.(this.size) } /** * 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 { const o: INodePropertyInfo = { name: name, type: type, default_value: default_value } if (extra_info) { for (const i in extra_info) { o[i] = extra_info[i] } } this.properties_info ||= [] this.properties_info.push(o) this.properties ||= {} this.properties[name] = default_value return o } /** * 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 { const output = { name: name, type: type, links: null } if (extra_info) { for (const i in extra_info) { output[i] = extra_info[i] } } this.outputs ||= [] this.outputs.push(output) this.onOutputAdded?.(output) if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this, type, true) this.setSize(this.computeSize()) this.setDirtyCanvas(true, true) return output } /** * 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 (let i = 0; i < array.length; ++i) { const info = array[i] const o = { name: info[0], type: info[1], links: null } if (array[2]) { for (const j in info[2]) { o[j] = info[2][j] } } this.outputs ||= [] this.outputs.push(o) this.onOutputAdded?.(o) if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this, info[1], true) } this.setSize(this.computeSize()) this.setDirtyCanvas(true, true) } /** * remove an existing output slot * @param {number} slot */ removeOutput(slot: number): void { 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()) this.onOutputRemoved?.(slot) this.setDirtyCanvas(true, true) } /** * 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 const input: INodeInputSlot = { name: name, type: type, link: null } if (extra_info) { for (const i in extra_info) { input[i] = extra_info[i] } } this.inputs ||= [] this.inputs.push(input) this.setSize(this.computeSize()) this.onInputAdded?.(input) LiteGraph.registerNodeAndSlotType(this, type) this.setDirtyCanvas(true, true) return input } /** * 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 (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 (const j in info[2]) { o[j] = info[2][j] } } this.inputs ||= [] this.inputs.push(o) this.onInputAdded?.(o) LiteGraph.registerNodeAndSlotType(this, info[1]) } this.setSize(this.computeSize()) this.setDirtyCanvas(true, true) } /** * remove an existing input slot * @param {number} slot */ removeInput(slot: number): void { 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()) this.onInputRemoved?.(slot, slot_info[0]) this.setDirtyCanvas(true, true) } /** * 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) { const o = { name: name, type: type, pos: pos, direction: direction, links: null } this.connections.push(o) return o } /** * computes the minimum size of a node according to its inputs and output slots * @param out * @return the total size */ computeSize(out?: Size): Size { const ctorSize = this.constructor.size if (ctorSize) return [ctorSize[0], ctorSize[1]] let rows = Math.max( this.inputs ? this.inputs.length : 1, this.outputs ? this.outputs.length : 1 ) 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 const title_width = compute_text_size(this.title) let input_width = 0 let output_width = 0 if (this.inputs) { 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 (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?.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 let widgets_height = 0 if (this.widgets?.length) { for (let i = 0, l = this.widgets.length; i < l; ++i) { const widget = this.widgets[i] widgets_height += widget.computeSize ? widget.computeSize(size[0])[1] + 4 : LiteGraph.NODE_WIDGET_HEIGHT + 4 } widgets_height += 8 } //compute height using widgets height if (this.widgets_up) size[1] = Math.max(size[1], widgets_height) else if (this.widgets_start_y != null) size[1] = Math.max(size[1], widgets_height + this.widgets_start_y) else size[1] += widgets_height 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 } //margin size[1] += 6 return size } inResizeCorner(canvasX: number, canvasY: number): boolean { const rows = this.outputs ? this.outputs.length : 1 const outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT 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. * * @param {String} property name of the property * @return {Object} the object with all the available info */ getPropertyInfo(property: string) { let info = null //there are several ways to define info about a property //legacy mode if (this.properties_info) { for (let i = 0; i < this.properties_info.length; ++i) { if (this.properties_info[i].name == property) { info = this.properties_info[i] break } } } //litescene mode using the constructor if (this.constructor["@" + property]) info = this.constructor["@" + 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 ||= {} info.type ||= typeof this.properties[property] if (info.widget == "combo") info.type = "enum" return info } /** * 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 { this.widgets ||= [] if (!options && callback && typeof callback === "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 (callback && typeof callback !== "function") { console.warn("addWidget: callback must be a function") callback = null } 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 } if (!callback && !w.options.callback && !w.options.property) { 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'] }" } this.widgets.push(w) this.setSize(this.computeSize()) return w } addCustomWidget(custom_widget: IWidget): IWidget { this.widgets ||= [] this.widgets.push(custom_widget) return custom_widget } /** * Measures the node for rendering, populating {@link out} with the results in graph space. * @param out Results (x, y, width, height) are inserted into this array. * @param pad Expands the area by this amount on each side. Default: 0 */ measure(out: Rect, pad = 0): void { const titleMode = this.constructor.title_mode const renderTitle = titleMode != LiteGraph.TRANSPARENT_TITLE && titleMode != LiteGraph.NO_TITLE const titleHeight = renderTitle ? LiteGraph.NODE_TITLE_HEIGHT : 0 out[0] = this.pos[0] - pad out[1] = this.pos[1] + -titleHeight - pad if (!this.flags?.collapsed) { out[2] = this.size[0] + (2 * pad) out[3] = this.size[1] + titleHeight + (2 * pad) } else { out[2] = (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + (2 * pad) out[3] = LiteGraph.NODE_TITLE_HEIGHT + (2 * pad) } } /** * 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) this.measure(out) if (compute_outer) { // 4 offset for collapsed node connection points out[0] -= 4 out[1] -= 4 // Add shadow & left offset out[2] += 6 + 4 // Add shadow & top offsets out[3] += 5 + 4 } this.onBounding?.(out) return out } /** * 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 ||= 0 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, y, this.pos[0] - margin, this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + 2 * margin, LiteGraph.NODE_TITLE_HEIGHT + 2 * margin )) { 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 false } /** * 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 const link_pos = new Float32Array(2) if (this.inputs) { 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 (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 } /** * 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 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 } /** * 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) { 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 -1 } /** * Finds the first free input slot. * @param {object} optsIn * @return The index of the first matching slot, the slot itself if returnObj is true, or -1 if not found. */ findInputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): number findInputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): INodeInputSlot findInputSlotFree(optsIn?: FindFreeSlotOptions) { return this.#findFreeSlot(this.inputs, optsIn) } /** * Finds the first free output slot. * @param {object} optsIn * @return The index of the first matching slot, the slot itself if returnObj is true, or -1 if not found. */ findOutputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): number findOutputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): INodeOutputSlot findOutputSlotFree(optsIn?: FindFreeSlotOptions) { return this.#findFreeSlot(this.outputs, optsIn) } /** * Finds the next free slot * @param slots The slots to search, i.e. this.inputs or this.outputs * @param options Options */ #findFreeSlot(slots: TSlot[], options?: FindFreeSlotOptions): TSlot | number { const defaults = { returnObj: false, typesNotAccepted: [] } const opts = Object.assign(defaults, options || {}) const length = slots?.length if (!(length > 0)) return -1 for (let i = 0; i < length; ++i) { const slot: TSlot & IGenericLinkOrLinks = slots[i] if (!slot || slot.link || slot.links?.length) continue if (opts.typesNotAccepted?.includes?.(slot.type)) continue return !opts.returnObj ? i : slot } return -1 } /** * 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) { return this.#findSlotByType(this.inputs, 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) { return this.#findSlotByType(this.outputs, type, returnObj, preferFreeSlot, doNotUseOccupied) } /** * 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: boolean, type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { return input ? this.#findSlotByType(this.inputs, type, returnObj, preferFreeSlot, doNotUseOccupied) : this.#findSlotByType(this.outputs, type, returnObj, preferFreeSlot, doNotUseOccupied) } /** * Finds a matching slot from those provided, returning the slot itself or its index in {@link slots}. * @param slots Slots to search (this.inputs or this.outputs) * @param type Type of slot to look for * @param returnObj If true, returns the slot itself. Otherwise, the index. * @param preferFreeSlot Prefer a free slot, but if none are found, fall back to an occupied slot. * @param doNotUseOccupied Do not fall back to occupied slots. * @see {findSlotByType} * @see {findOutputSlotByType} * @see {findInputSlotByType} * @returns If a match is found, the slot if returnObj is true, otherwise the index. If no matches are found, -1 */ #findSlotByType(slots: TSlot[], type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): TSlot | number { const length = slots?.length if (!length) return -1 // !! empty string type is considered 0, * !! if (type == "" || type == "*") type = 0 const sourceTypes = String(type).toLowerCase().split(",") // Run the search let occupiedSlot: number | TSlot | null = null for (let i = 0; i < length; ++i) { const slot: TSlot & IGenericLinkOrLinks = slots[i] const destTypes = slot.type == "0" || slot.type == "*" ? ["0"] : String(slot.type).toLowerCase().split(",") 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 === "*") { if (preferFreeSlot && (slot.links?.length || slot.link != null)) { // In case we can't find a free slot. occupiedSlot ??= returnObj ? slot : i continue } return returnObj ? slot : i } } } } return doNotUseOccupied ? -1 : occupiedSlot ?? -1 } /** * Determines the slot index to connect to when attempting to connect by type. * * @param findInputs If true, searches for an input. Otherwise, an output. * @param node The node at the other end of the connection. * @param slotType The type of slot at the other end of the connection. * @param options Search restrictions to adhere to. * @see {connectByType} * @see {connectByTypeOutput} */ findConnectByTypeSlot( findInputs: boolean, node: LGraphNode, slotType: ISlotType, options?: ConnectByTypeOptions ): number | null { // LEGACY: Old options names if (options && typeof options === "object") { if ("firstFreeIfInputGeneralInCase" in options) options.wildcardToTyped = !!options.firstFreeIfInputGeneralInCase if ("firstFreeIfOutputGeneralInCase" in options) options.wildcardToTyped = !!options.firstFreeIfOutputGeneralInCase if ("generalTypeInCase" in options) options.typedToWildcard = !!options.generalTypeInCase } const optsDef: ConnectByTypeOptions = { createEventInCase: true, wildcardToTyped: true, typedToWildcard: true } const opts = Object.assign(optsDef, options) if (node && typeof node === "number") { node = this.graph.getNodeById(node) } const slot = node.findSlotByType(findInputs, slotType, false, true) if (slot >= 0 && slot !== null) return slot // TODO: Remove or reimpl. events. WILL CREATE THE onTrigger IN SLOT if (opts.createEventInCase && slotType == LiteGraph.EVENT) { if (findInputs) return -1 if (LiteGraph.do_add_triggers_slots) return node.addOnExecutedOutput() } // connect to the first general output slot if not found a specific type and if (opts.typedToWildcard) { const generalSlot = node.findSlotByType(findInputs, 0, false, true, true) if (generalSlot >= 0) return generalSlot } // connect to the first free input slot if not found a specific type and this output is general if (opts.wildcardToTyped && (slotType == 0 || slotType == "*" || slotType == "")) { const opt = { typesNotAccepted: [LiteGraph.EVENT] } const nonEventSlot = findInputs ? node.findInputSlotFree(opt) : node.findOutputSlotFree(opt) if (nonEventSlot >= 0) return nonEventSlot } return null } /** * connect this node output to the input of another node BY TYPE * @param {number} slot (could be the number of the slot or the string with the name of the slot) * @param {LGraphNode} target_node the target node * @param {string} target_slotType the input slot type of the target node * @return {Object} the link_info is created, otherwise null */ connectByType(slot: number | string, target_node: LGraphNode, target_slotType: ISlotType, optsIn?: ConnectByTypeOptions): LLink | null { const slotIndex = this.findConnectByTypeSlot(true, target_node, target_slotType, optsIn) if (slotIndex !== null) return this.connect(slot, target_node, slotIndex) console.debug("[connectByType]: no way to connect type: ", target_slotType, " to node: ", target_node) return null } /** * connect this node input to the output of another node BY TYPE * @method connectByType * @param {number | string} slot (could be the number of the slot or the string with the name of the slot) * @param {LGraphNode} source_node the target node * @param {string} source_slotType the output slot type of the target node * @return {Object} the link_info is created, otherwise null */ connectByTypeOutput(slot: number | string, source_node: LGraphNode, source_slotType: ISlotType, optsIn?: ConnectByTypeOptions): LLink | null { // LEGACY: Old options names if (typeof optsIn === "object") { if ("firstFreeIfInputGeneralInCase" in optsIn) optsIn.wildcardToTyped = !!optsIn.firstFreeIfInputGeneralInCase if ("generalTypeInCase" in optsIn) optsIn.typedToWildcard = !!optsIn.generalTypeInCase } const slotIndex = this.findConnectByTypeSlot(false, source_node, source_slotType, optsIn) if (slotIndex !== null) return source_node.connect(slotIndex, this, slot) console.debug("[connectByType]: no way to connect type: ", source_slotType, " to node: ", source_node) return null } /** * Connect an output of this node to an input of another node * @param {number | 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 * @param {number | 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 | string, target_node: LGraphNode, target_slot: ISlotType): LLink | null { // Allow legacy API support for searching target_slot by string, without mutating the input variables let targetIndex: number if (!this.graph) { //could be connected before adding it to a graph //due to link ids being associated with graphs console.log("Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them.") return null } //seek for the output 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 null } } else if (!this.outputs || slot >= this.outputs.length) { if (LiteGraph.debug) console.log("Connect: Error, slot number not found") return null } if (target_node && typeof target_node === "number") { target_node = this.graph.getNodeById(target_node) } if (!target_node) throw "target node is null" //avoid loopback if (target_node == this) return null //you can specify the slot by name if (typeof target_slot === "string") { targetIndex = target_node.findInputSlot(target_slot) if (targetIndex == -1) { if (LiteGraph.debug) console.log("Connect: Error, no slot of name " + targetIndex) return null } } else if (target_slot === LiteGraph.EVENT) { // TODO: Events if (LiteGraph.do_add_triggers_slots) { target_node.changeMode(LiteGraph.ON_TRIGGER) targetIndex = target_node.findInputSlot("onTrigger") } else { return null } } else if (typeof target_slot === "number") { targetIndex = target_slot } else { targetIndex = 0 } // Allow target node to change slot if (target_node.onBeforeConnectInput) { // This way node can choose another slot (or make a new one?) const requestedIndex: false | number | null = target_node.onBeforeConnectInput(targetIndex, target_slot) targetIndex = typeof requestedIndex === "number" ? requestedIndex : null } if (targetIndex === null || !target_node.inputs || targetIndex >= target_node.inputs.length) { if (LiteGraph.debug) console.log("Connect: Error, slot number not found") return null } let changed = false const input = target_node.inputs[targetIndex] let link_info: LLink = null const output = this.outputs[slot] if (!this.outputs[slot]) return null //check targetSlot and check connection types if (!LiteGraph.isValidConnection(output.type, input.type)) { this.setDirtyCanvas(false, true) // @ts-expect-error Unused param if (changed) this.graph.connectionChange(this, link_info) return null } // Allow nodes to block connection if (target_node.onConnectInput?.(targetIndex, output.type, output, this, slot) === false) return null if (this.onConnectOutput?.(slot, input.type, input, target_node, targetIndex) === false) return null //if there is something already plugged there, disconnect if (target_node.inputs[targetIndex]?.link != null) { this.graph.beforeChange() target_node.disconnectInput(targetIndex) changed = true } if (output.links?.length) { if (output.type === LiteGraph.EVENT && !LiteGraph.allow_multi_output_for_events) { this.graph.beforeChange() // @ts-expect-error Unused param this.disconnectOutput(slot, false, { doProcessChange: false }) changed = true } } const nextId = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++this.graph.last_link_id //create link class link_info = new LLink( nextId, input.type || output.type, this.id, slot, target_node.id, targetIndex ) //add to graph links list this.graph.links[link_info.id] = link_info //connect in output output.links ??= [] output.links.push(link_info.id) //connect in input target_node.inputs[targetIndex].link = link_info.id if (this.graph) this.graph._version++ //link_info has been created now, so its updated this.onConnectionsChange?.( LiteGraph.OUTPUT, slot, true, link_info, output ) target_node.onConnectionsChange?.( LiteGraph.INPUT, targetIndex, true, link_info, input ) this.graph?.onNodeConnectionChange?.( LiteGraph.INPUT, target_node, targetIndex, this, slot ) this.graph?.onNodeConnectionChange?.( LiteGraph.OUTPUT, this, slot, target_node, targetIndex ) this.setDirtyCanvas(false, true) this.graph.afterChange() this.graph.connectionChange(this) return link_info } /** * 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 (typeof slot === "string") { slot = this.findOutputSlot(slot) if (slot == -1) { 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 } //get output slot 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 (typeof target_node === "number") target_node = graph.getNodeById(target_node) if (!target_node) throw "Target Node not found" 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 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 (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++ if (target_node) { 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, link_info.target_slot, false, link_info, input ) // FIXME: Called twice. graph?.onNodeConnectionChange?.(LiteGraph.INPUT, target_node, link_info.target_slot) } //remove the link from the links pool delete graph.links[link_id] 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 } /** * 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 } 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) { const w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH if (this.horizontal) { out[0] = this.pos[0] + w * 0.5 out[1] = is_input ? this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT : this.pos[1] } else { out[0] = is_input ? this.pos[0] : this.pos[0] + w out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5 } 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 } //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 } 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 } //horizontal distributed slots if (this.horizontal) { 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 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 } /* 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) } /* Console output */ trace(msg?: string): void { this.console ||= [] this.console.push(msg) if (this.console.length > LGraphNode.MAX_CONSOLE) this.console.shift() 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): void { this.graph?.sendActionToCanvas("setDirty", [ dirty_foreground, dirty_background ]) } loadImage(url: string): HTMLImageElement { interface AsyncImageElement extends HTMLImageElement { ready?: boolean } 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 } /* 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 const list = this.graph.list_of_graphcanvas for (let i = 0; i < list.length; ++i) { const c = list[i] //releasing somebody elses capture?! if (!v && c.node_capturing_input != this) continue //change c.node_capturing_input = v ? this : null } } get collapsed() { return !!this.flags.collapsed } get collapsible() { return !this.pinned && (this.constructor.collapsable !== false) } /** * Collapse the node to make it smaller on the canvas **/ collapse(force?: boolean): void { this.graph._version++ if (!this.collapsible && !force) return this.flags.collapsed = !this.flags.collapsed this.setDirtyCanvas(true, true) } get pinned() { return !!this.flags.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 } 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] } get height() { // @ts-expect-error Not impl. return this.collapsed ? LiteGraph.NODE_COLLAPSED_HEIGHT : this.size[1] } 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) for (const badge of badgeInstances) { badge.draw(ctx, currentX, y - badge.height) currentX += badge.getWidth(ctx) + gap } } }