From 3e44f6a0c1603ae7234dccb02cadccdd15ad2ebe Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:18:06 +1100 Subject: [PATCH] [API] Add improved slot locator functions (#716) - Returns object with slot, index, and pos - Locate-by-type returns object with slot & index - Uses standard `undefined` return for concise chaining & validation - Free 10x perf increase over getConnectionPos (used basic random data to test, out of curiosity) --- src/LGraphCanvas.ts | 10 ---- src/LGraphNode.ts | 98 ++++++++++++++++++++++++++++--- src/canvas/measureSlots.ts | 114 +++++++++++++++++++++---------------- src/interfaces.ts | 5 ++ src/strings.ts | 6 ++ src/utils/collections.ts | 57 ++++++++++++++++++- 6 files changed, 221 insertions(+), 69 deletions(-) diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 10e7de720..3b9560906 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -3109,16 +3109,6 @@ export class LGraphCanvas implements ConnectionColorContext { return } - /** @deprecated - use {@link isOverNodeInput} from '@/canvas/measureSlots.ts' */ - isOverNodeInput(node: LGraphNode, canvasx: number, canvasy: number, slot_pos?: Point): number { - return isOverNodeInput(node, canvasx, canvasy, slot_pos) - } - - /** @deprecated - use {@link isOverNodeOutput} from '@/canvas/measureSlots.ts' */ - isOverNodeOutput(node: LGraphNode, canvasx: number, canvasy: number, slot_pos?: Point): number { - return isOverNodeOutput(node, canvasx, canvasy, slot_pos) - } - /** * process a key event */ diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index 18e7364ff..c672a5739 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -39,6 +39,7 @@ import { RenderShape, TitleMode, } from "./types/globalEnums" +import { findFreeSlotOfType } from "./utils/collections" import { LayoutElement } from "./utils/layout" import { distributeSpace } from "./utils/spaceDistribution" import { toClass } from "./utils/type" @@ -1941,7 +1942,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { ): number findInputSlotFree( optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }, - ): INodeInputSlot + ): INodeInputSlot | -1 findInputSlotFree(optsIn?: FindFreeSlotOptions) { return this.#findFreeSlot(this.inputs, optsIn) } @@ -1956,7 +1957,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { ): number findOutputSlotFree( optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }, - ): INodeOutputSlot + ): INodeOutputSlot | -1 findOutputSlotFree(optsIn?: FindFreeSlotOptions) { return this.#findFreeSlot(this.outputs, optsIn) } @@ -2067,21 +2068,21 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean, - ): INodeInputSlot + ): INodeInputSlot | -1 findSlotByType( input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean, - ): INodeOutputSlot + ): INodeOutputSlot | -1 findSlotByType( input: boolean, type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean, - ) { + ): number | INodeOutputSlot | INodeInputSlot { return input ? this.#findSlotByType( this.inputs, @@ -2218,6 +2219,34 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { } } + /** + * Finds the first free output slot with any of the comma-delimited types in {@link type}. + * + * If no slots are free, falls back in order to: + * - The first free wildcard slot + * - The first occupied slot + * - The first occupied wildcard slot + * @param type The {@link ISlotType type} of slot to find + * @returns The index and slot if found, otherwise `undefined`. + */ + findOutputByType(type: ISlotType): { index: number, slot: INodeOutputSlot } | undefined { + return findFreeSlotOfType(this.outputs, type) + } + + /** + * Finds the first free input slot with any of the comma-delimited types in {@link type}. + * + * If no slots are free, falls back in order to: + * - The first free wildcard slot + * - The first occupied slot + * - The first occupied wildcard slot + * @param type The {@link ISlotType type} of slot to find + * @returns The index and slot if found, otherwise `undefined`. + */ + findInputByType(type: ISlotType): { index: number, slot: INodeInputSlot } | undefined { + return findFreeSlotOfType(this.inputs, type) + } + /** * connect this node output to the input of another node BY TYPE * @param slot (could be the number of the slot or the string with the name of the slot) @@ -2388,8 +2417,8 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { output: INodeOutputSlot, inputNode: LGraphNode, input: INodeInputSlot, - afterRerouteId?: RerouteId, - ) { + afterRerouteId: RerouteId | undefined, + ): LLink | null | undefined { const { graph } = this if (!graph) throw new NullGraphError() @@ -2650,6 +2679,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { } /** + * @deprecated Use {@link getInputPos} or {@link getOutputPos} instead. * returns the center of a connection point in canvas coords * @param is_input true if if a input slot, false if it is an output * @param slot_number (could be the number of the slot or the string with the name of the slot) @@ -2701,6 +2731,60 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { return out } + /** + * Gets the position of an input slot, in graph co-ordinates. + * + * This method is preferred over the legacy {@link getConnectionPos} method. + * @param slot Input slot index + * @returns Position of the input slot + */ + getInputPos(slot: number): Point { + const { pos: [nodeX, nodeY], inputs } = this + + if (this.flags.collapsed) { + const halfTitle = LiteGraph.NODE_TITLE_HEIGHT * 0.5 + return [nodeX, nodeY - halfTitle] + } + + const inputPos = inputs?.[slot]?.pos + if (inputPos) return [nodeX + inputPos[0], nodeY + inputPos[1]] + + // default vertical slots + const offsetX = LiteGraph.NODE_SLOT_HEIGHT * 0.5 + const nodeOffsetY = this.constructor.slot_start_y || 0 + const slotY = (slot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + + return [nodeX + offsetX, nodeY + slotY + nodeOffsetY] + } + + /** + * Gets the position of an output slot, in graph co-ordinates. + * + * This method is preferred over the legacy {@link getConnectionPos} method. + * @param slot Output slot index + * @returns Position of the output slot + */ + getOutputPos(slot: number): Point { + const { pos: [nodeX, nodeY], outputs, size: [width] } = this + + if (this.flags.collapsed) { + const width = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH + const halfTitle = LiteGraph.NODE_TITLE_HEIGHT * 0.5 + return [nodeX + width, nodeY - halfTitle] + } + + const outputPos = outputs?.[slot]?.pos + if (outputPos) return outputPos + + // default vertical slots + const offsetX = LiteGraph.NODE_SLOT_HEIGHT * 0.5 + const nodeOffsetY = this.constructor.slot_start_y || 0 + const slotY = (slot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + + // TODO: Why +1? + return [nodeX + width + 1 - offsetX, nodeY + slotY + nodeOffsetY] + } + /** @inheritdoc */ snapToGrid(snapTo: number): boolean { return this.pinned ? false : snapPoint(this.pos, snapTo) diff --git a/src/canvas/measureSlots.ts b/src/canvas/measureSlots.ts index 4fed1b319..89b6c9329 100644 --- a/src/canvas/measureSlots.ts +++ b/src/canvas/measureSlots.ts @@ -1,10 +1,56 @@ -import type { Point } from "@/interfaces" +import type { INodeInputSlot, INodeOutputSlot, Point } from "@/interfaces" import type { LGraphNode } from "@/LGraphNode" import { isInRectangle } from "@/measure" +export function getInputOnPos(node: LGraphNode, x: number, y: number): { index: number, input: INodeInputSlot, pos: Point } | undefined { + const { inputs } = node + if (!inputs) return + + for (const [index, input] of inputs.entries()) { + const pos = node.getInputPos(index) + + // TODO: Find a cheap way to measure text, and do it on node label change instead of here + // Input icon width + text approximation + const nameLength = input.label?.length ?? input.localized_name?.length ?? input.name?.length + const width = 20 + (nameLength || 3) * 7 + + if (isInRectangle( + x, + y, + pos[0] - 10, + pos[1] - 10, + width, + 20, + )) { + return { index, input, pos } + } + } +} + +export function getOutputOnPos(node: LGraphNode, x: number, y: number): { index: number, output: INodeOutputSlot, pos: Point } | undefined { + const { outputs } = node + if (!outputs) return + + for (const [index, output] of outputs.entries()) { + const pos = node.getOutputPos(index) + + if (isInRectangle( + x, + y, + pos[0] - 10, + pos[1] - 10, + 40, + 20, + )) { + return { index, output, pos } + } + } +} + /** - * returns the INDEX if a position (in graph space) is on top of a node input slot + * Returns the input slot index if the given position (in graph space) is on top of a node input slot. + * A helper function - originally on the prototype of LGraphCanvas. */ export function isOverNodeInput( node: LGraphNode, @@ -12,37 +58,19 @@ export function isOverNodeInput( canvasy: number, slot_pos?: Point, ): number { - const { inputs } = node - if (inputs) { - for (const [i, input] of inputs.entries()) { - const link_pos = node.getConnectionPos(true, i) - let is_inside = false - // TODO: Find a cheap way to measure text, and do it on node label change instead of here - // Input icon width + text approximation - const width = - 20 + ((input.label?.length ?? input.localized_name?.length ?? input.name?.length) || 3) * 7 - is_inside = isInRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 10, - width, - 20, - ) - if (is_inside) { - if (slot_pos) { - slot_pos[0] = link_pos[0] - slot_pos[1] = link_pos[1] - } - return i - } - } + const result = getInputOnPos(node, canvasx, canvasy) + if (!result) return -1 + + if (slot_pos) { + slot_pos[0] = result.pos[0] + slot_pos[1] = result.pos[1] } - return -1 + return result.index } /** - * returns the INDEX if a position (in graph space) is on top of a node output slot + * Returns the output slot index if the given position (in graph space) is on top of a node output slot. + * A helper function - originally on the prototype of LGraphCanvas. */ export function isOverNodeOutput( node: LGraphNode, @@ -50,26 +78,12 @@ export function isOverNodeOutput( canvasy: number, slot_pos?: Point, ): number { - const { outputs } = node - if (outputs) { - for (let i = 0; i < outputs.length; ++i) { - const link_pos = node.getConnectionPos(false, i) - const is_inside = isInRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 10, - 40, - 20, - ) - if (is_inside) { - if (slot_pos) { - slot_pos[0] = link_pos[0] - slot_pos[1] = link_pos[1] - } - return i - } - } + const result = getOutputOnPos(node, canvasx, canvasy) + if (!result) return -1 + + if (slot_pos) { + slot_pos[0] = result.pos[0] + slot_pos[1] = result.pos[1] } - return -1 + return result.index } diff --git a/src/interfaces.ts b/src/interfaces.ts index 84cfaa1cb..c0051fc3b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -217,6 +217,11 @@ export interface IOptionalSlotData = { index: number, slot: T } | undefined + +/** + * Finds the first free in/out slot with any of the comma-delimited types in {@link type}. + * + * If no slots are free, falls back in order to: + * - The first free wildcard slot + * - The first occupied slot + * - The first occupied wildcard slot + * @param slots The iterable of node slots slots to search through + * @param type The {@link ISlotType type} of slot to find + * @returns The index and slot if found, otherwise `undefined`. + */ +export function findFreeSlotOfType( + slots: T[], + type: ISlotType, +): FreeSlotResult { + if (!slots?.length) return + + let occupiedSlot: FreeSlotResult + let wildSlot: FreeSlotResult + let occupiedWildSlot: FreeSlotResult + + const validTypes = parseSlotTypes(type) + + for (const [index, slot] of slots.entries()) { + const slotTypes = parseSlotTypes(slot.type) + + for (const validType of validTypes) { + for (const slotType of slotTypes) { + if (slotType === validType) { + if (slot.link == null && !slot.links?.length) { + // Exact match - short circuit + return { index, slot } + } + // In case we can't find a free slot. + occupiedSlot ??= { index, slot } + } else if (!wildSlot && (validType === "*" || slotType === "*")) { + // Save the first free wildcard slot as a fallback + if (slot.link == null && !slot.links?.length) { + wildSlot = { index, slot } + } else { + occupiedWildSlot ??= { index, slot } + } + } + } + } + } + return wildSlot ?? occupiedSlot ?? occupiedWildSlot +}