diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index 9c10e1307..16f017ef5 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -1,7 +1,6 @@ import type { DragAndScale } from "./DragAndScale" import type { IDrawBoundingOptions } from "./draw" import type { - CanvasColour, ColorOption, Dictionary, IColorable, @@ -41,7 +40,6 @@ import { TitleMode, } from "./types/globalEnums" import { findFreeSlotOfType } from "./utils/collections" -import { LayoutElement } from "./utils/layout" import { distributeSpace } from "./utils/spaceDistribution" import { toClass } from "./utils/type" import { WIDGET_TYPE_MAP } from "./widgets/widgetMap" @@ -3448,31 +3446,22 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { } } - get highlightColor(): CanvasColour { - return LiteGraph.NODE_TEXT_HIGHLIGHT_COLOR ?? LiteGraph.NODE_SELECTED_TITLE_COLOR ?? LiteGraph.NODE_TEXT_COLOR - } - - get slots(): INodeSlot[] { + get slots(): (INodeInputSlot | INodeOutputSlot)[] { return [...this.inputs, ...this.outputs] } - #measureSlot(slot: INodeSlot, slotIndex: number): LayoutElement { + #measureSlot(slot: INodeSlot, slotIndex: number): void { const isInput = isINodeInputSlot(slot) const pos = isInput ? this.getInputPos(slotIndex) : this.getOutputPos(slotIndex) - slot._layoutElement = new LayoutElement({ - boundingRect: [ - pos[0] - this.pos[0] - LiteGraph.NODE_SLOT_HEIGHT * 0.5, - pos[1] - this.pos[1] - LiteGraph.NODE_SLOT_HEIGHT * 0.5, - LiteGraph.NODE_SLOT_HEIGHT, - LiteGraph.NODE_SLOT_HEIGHT, - ], - }) - return slot._layoutElement + slot.boundingRect[0] = pos[0] - this.pos[0] - LiteGraph.NODE_SLOT_HEIGHT * 0.5 + slot.boundingRect[1] = pos[1] - this.pos[1] - LiteGraph.NODE_SLOT_HEIGHT * 0.5 + slot.boundingRect[2] = LiteGraph.NODE_SLOT_HEIGHT + slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT } #measureSlots(): ReadOnlyRect | null { - const slots: LayoutElement[] = [] + const slots: INodeSlot[] = [] for (const [slotIndex, slot] of this.inputs.entries()) { // Unrecognized nodes (Nodes with error) has inputs but no widgets. Treat @@ -3480,12 +3469,12 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { /** Widget input slots are handled in {@link layoutWidgetInputSlots} */ if (this.widgets?.length && isWidgetInputSlot(slot)) continue - const layoutElement = this.#measureSlot(slot, slotIndex) - slots.push(layoutElement) + this.#measureSlot(slot, slotIndex) + slots.push(slot) } for (const [slotIndex, slot] of this.outputs.entries()) { - const layoutElement = this.#measureSlot(slot, slotIndex) - slots.push(layoutElement) + this.#measureSlot(slot, slotIndex) + slots.push(slot) } return slots.length ? createBounds(slots, 0) : null @@ -3533,32 +3522,29 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { lowQuality, }: DrawSlotsOptions) { for (const slot of this.slots) { - // change opacity of incompatible slots when dragging a connection - const layoutElement = slot._layoutElement const slotInstance = toNodeSlotClass(slot) - const isValid = !fromSlot || slotInstance.isValidTarget(fromSlot) - const highlight = isValid && this.#isMouseOverSlot(slot) - const labelColor = highlight - ? this.highlightColor - : LiteGraph.NODE_TEXT_COLOR + const isValidTarget = fromSlot && slotInstance.isValidTarget(fromSlot) + const isMouseOverSlot = this.#isMouseOverSlot(slot) + + // change opacity of incompatible slots when dragging a connection + const isValid = !fromSlot || isValidTarget + const highlight = isValid && isMouseOverSlot // Show slot if it's not a widget input slot // or if it's a widget input slot and satisfies one of the following: // - the mouse is over the widget // - the slot is valid during link drop // - the slot is connected - const showSlot = !isWidgetInputSlot(slot) || - this.#isMouseOverSlot(slot) || - this.#isMouseOverWidget(this.getWidgetFromSlot(slot)!) || - (fromSlot && slotInstance.isValidTarget(fromSlot)) || + const showSlot = isMouseOverSlot || + isValidTarget || + !slotInstance.isWidgetInputSlot || + this.#isMouseOverWidget(this.getWidgetFromSlot(slotInstance)!) || slotInstance.isConnected() ctx.globalAlpha = showSlot ? (isValid ? editorAlpha : 0.4 * editorAlpha) : 0 slotInstance.draw(ctx, { - pos: layoutElement?.center ?? [0, 0], colorContext, - labelColor, lowQuality, highlight, }) diff --git a/src/NodeSlot.ts b/src/NodeSlot.ts index ac3eda8e8..c766b7f74 100644 --- a/src/NodeSlot.ts +++ b/src/NodeSlot.ts @@ -1,9 +1,10 @@ -import type { CanvasColour, Dictionary, INodeInputSlot, INodeOutputSlot, INodeSlot, ISlotType, IWidgetInputSlot, IWidgetLocator, Point, SharedIntersection } from "./interfaces" +import type { CanvasColour, Dictionary, INodeInputSlot, INodeOutputSlot, INodeSlot, ISlotType, IWidgetInputSlot, IWidgetLocator, OptionalProps, Point, Rect, SharedIntersection } from "./interfaces" import type { LinkId } from "./LLink" import type { IWidget } from "./types/widgets" import { LabelPosition, SlotShape, SlotType } from "./draw" import { LiteGraph } from "./litegraph" +import { getCentre } from "./measure" import { LinkDirection, RenderShape } from "./types/globalEnums" import { ISerialisableNodeInput, ISerialisableNodeOutput } from "./types/serialisation" @@ -19,9 +20,7 @@ export interface ConnectionColorContext { } interface IDrawOptions { - pos: Point colorContext: ConnectionColorContext - labelColor?: CanvasColour labelPosition?: LabelPosition lowQuality?: boolean doStroke?: boolean @@ -64,20 +63,19 @@ export function outputAsSerialisable(slot: INodeOutputSlot & { widget?: IWidget } } -export function toNodeSlotClass(slot: INodeSlot): NodeSlot { - if (isINodeInputSlot(slot)) { - return new NodeInputSlot(slot) - } else if (isINodeOutputSlot(slot)) { - return new NodeOutputSlot(slot) - } - throw new Error("Invalid slot type") +export function toNodeSlotClass(slot: INodeInputSlot | INodeOutputSlot): NodeInputSlot | NodeOutputSlot { + if (slot instanceof NodeInputSlot || slot instanceof NodeOutputSlot) return slot + + return "link" in slot + ? new NodeInputSlot(slot) + : new NodeOutputSlot(slot) } /** * Whether this slot is an input slot and attached to a widget. * @param slot The slot to check. */ -export function isWidgetInputSlot(slot: INodeSlot): slot is IWidgetInputSlot { +export function isWidgetInputSlot(slot: INodeInputSlot): slot is IWidgetInputSlot { return isINodeInputSlot(slot) && !!slot.widget } @@ -96,11 +94,19 @@ export abstract class NodeSlot implements INodeSlot { pos?: Point widget?: IWidgetLocator hasErrors?: boolean + boundingRect: Rect - constructor(slot: INodeSlot) { + get highlightColor(): CanvasColour { + return LiteGraph.NODE_TEXT_HIGHLIGHT_COLOR ?? LiteGraph.NODE_SELECTED_TITLE_COLOR ?? LiteGraph.NODE_TEXT_COLOR + } + + abstract get isWidgetInputSlot(): boolean + + constructor(slot: OptionalProps) { Object.assign(this, slot) this.name = slot.name this.type = slot.type + this.boundingRect = slot.boundingRect ?? [0, 0, 0, 0] } /** @@ -140,9 +146,7 @@ export abstract class NodeSlot implements INodeSlot { draw( ctx: CanvasRenderingContext2D, { - pos, colorContext, - labelColor = "#AAA", labelPosition = LabelPosition.Right, lowQuality = false, highlight = false, @@ -154,6 +158,11 @@ export abstract class NodeSlot implements INodeSlot { const originalStrokeStyle = ctx.strokeStyle const originalLineWidth = ctx.lineWidth + const labelColor = highlight + ? this.highlightColor + : LiteGraph.NODE_TEXT_COLOR + + const pos = getCentre(this.boundingRect) const slot_type = this.type const slot_shape = ( slot_type === SlotType.Array ? SlotShape.Grid : this.shape @@ -211,7 +220,7 @@ export abstract class NodeSlot implements INodeSlot { if (!lowQuality && doStroke) ctx.stroke() // render slot label - const hideLabel = lowQuality || isWidgetInputSlot(this) + const hideLabel = lowQuality || this.isWidgetInputSlot if (!hideLabel) { const text = this.renderingLabel if (text) { @@ -290,7 +299,11 @@ export function isINodeInputSlot(slot: INodeSlot): slot is INodeInputSlot { export class NodeInputSlot extends NodeSlot implements INodeInputSlot { link: LinkId | null - constructor(slot: INodeInputSlot) { + get isWidgetInputSlot(): boolean { + return !!this.widget + } + + constructor(slot: OptionalProps) { super(slot) this.link = slot.link } @@ -326,7 +339,11 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot { _data?: unknown slot_index?: number - constructor(slot: INodeOutputSlot) { + get isWidgetInputSlot(): false { + return false + } + + constructor(slot: OptionalProps) { super(slot) this.links = slot.links this._data = slot._data diff --git a/src/interfaces.ts b/src/interfaces.ts index 58d9b9817..ca1f9643e 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -3,7 +3,6 @@ import type { LGraphNode, NodeId } from "./LGraphNode" import type { LinkId, LLink } from "./LLink" import type { Reroute, RerouteId } from "./Reroute" import type { LinkDirection, RenderShape } from "./types/globalEnums" -import type { LayoutElement } from "./utils/layout" export type Dictionary = { [key: string]: T } @@ -30,6 +29,22 @@ export type SharedIntersection = { export type CanvasColour = string | CanvasGradient | CanvasPattern +/** + * Any object that has a {@link boundingRect}. + */ +export interface HasBoundingRect { + /** + * A rectangle that represents the outer edges of the item. + * + * Used for various calculations, such as overlap, selective rendering, and click checks. + * For most items, this is cached position & size as `x, y, width, height`. + * Some items (such as nodes) may extend above and/or to the left of their {@link pos}. + * @readonly + * @see {@link move} + */ + readonly boundingRect: ReadOnlyRect +} + /** An object containing a set of child objects */ export interface Parent { /** All objects owned by the parent object. */ @@ -41,7 +56,7 @@ export interface Parent { * * May contain other {@link Positionable} objects. */ -export interface Positionable extends Parent { +export interface Positionable extends Parent, HasBoundingRect { readonly id: NodeId | RerouteId | number /** Position in graph coordinates. Default: 0,0 */ readonly pos: Point @@ -68,17 +83,6 @@ export interface Positionable extends Parent { */ snapToGrid(snapTo: number): boolean - /** - * A rectangle that represents the outer edges of the item. - * - * Used for various calculations, such as overlap, selective rendering, and click checks. - * For most items, this is cached position & size as `x, y, width, height`. - * Some items (such as nodes) may extend above and/or to the left of their {@link pos}. - * @readonly - * @see {@link move} - */ - readonly boundingRect: ReadOnlyRect - /** Called whenever the item is selected */ onSelected?(): void /** Called whenever the item is deselected */ @@ -256,7 +260,7 @@ export interface IOptionalSlotData, + objects: Iterable, padding: number = 10, ): ReadOnlyRect | null { const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity]) diff --git a/src/types/serialisation.ts b/src/types/serialisation.ts index 809b2c429..ba00bb8f7 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -56,10 +56,10 @@ export interface SerialisableGraph extends BaseExportedGraph { extra?: Dictionary } -export type ISerialisableNodeInput = Omit & { +export type ISerialisableNodeInput = Omit & { widget?: { name: string } } -export type ISerialisableNodeOutput = Omit & { +export type ISerialisableNodeOutput = Omit & { widget?: { name: string } } @@ -135,7 +135,7 @@ export interface ExportedSubgraph extends ISerialisedGraph { } /** Properties shared by subgraph and node I/O slots. */ -type SubgraphIOShared = Omit +type SubgraphIOShared = Omit /** Subgraph I/O slots */ export interface SubgraphIO extends SubgraphIOShared { diff --git a/src/utils/layout.ts b/src/utils/layout.ts deleted file mode 100644 index 59b7a5779..000000000 --- a/src/utils/layout.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Point, ReadOnlyRect } from "@/interfaces" - -export class LayoutElement { - public readonly boundingRect: ReadOnlyRect - - constructor(o: { - boundingRect: ReadOnlyRect - }) { - this.boundingRect = o.boundingRect - } - - get center(): Point { - return [ - this.boundingRect[0] + this.boundingRect[2] / 2, - this.boundingRect[1] + this.boundingRect[3] / 2, - ] - } -}