[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)
This commit is contained in:
filtered
2025-03-08 00:18:06 +11:00
committed by GitHub
parent 84fad5b8a5
commit 3e44f6a0c1
6 changed files with 221 additions and 69 deletions

View File

@@ -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
*/

View File

@@ -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<TReturn extends true>(
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<TReturn extends true>(
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<TSlot extends false, TReturn extends true>(
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)

View File

@@ -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
}

View File

@@ -217,6 +217,11 @@ export interface IOptionalSlotData<TSlot extends INodeInputSlot | INodeOutputSlo
className?: string
}
/**
* A string that represents a specific data / slot type, e.g. `STRING`.
*
* Can be comma-delimited to specify multiple allowed types, e.g. `STRING,INT`.
*/
export type ISlotType = number | string
export interface INodeSlot {

View File

@@ -1,3 +1,5 @@
import type { ISlotType } from "./litegraph"
/**
* Uses the standard String() function to coerce to string, unless the value is null or undefined - then null.
* @param value The value to convert
@@ -15,3 +17,7 @@ export function stringOrNull(value: unknown): string | null {
export function stringOrEmpty(value: unknown): string {
return value == null ? "" : String(value)
}
export function parseSlotTypes(type: ISlotType): string[] {
return type == "" || type == "0" ? ["*"] : String(type).toLowerCase().split(",")
}

View File

@@ -1,7 +1,8 @@
import type { ConnectingLink, Positionable } from "../interfaces"
import type { ConnectingLink, INodeInputSlot, INodeOutputSlot, ISlotType, Positionable } from "../interfaces"
import type { LinkId } from "@/LLink"
import { LGraphNode } from "@/LGraphNode"
import { type IGenericLinkOrLinks, LGraphNode } from "@/LGraphNode"
import { parseSlotTypes } from "@/strings"
/**
* Creates a flat set of all positionable items by recursively iterating through all child items.
@@ -46,3 +47,55 @@ export function isDraggingLink(linkId: LinkId, connectingLinks: ConnectingLink[]
if (linkId === connectingLink.link.id) return connectingLink
}
}
type InputOrOutput = (INodeInputSlot | INodeOutputSlot) & IGenericLinkOrLinks
type FreeSlotResult<T extends InputOrOutput> = { 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<T extends InputOrOutput>(
slots: T[],
type: ISlotType,
): FreeSlotResult<T> {
if (!slots?.length) return
let occupiedSlot: FreeSlotResult<T>
let wildSlot: FreeSlotResult<T>
let occupiedWildSlot: FreeSlotResult<T>
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
}