mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
[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:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(",")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user