Remove LayoutElement, resolve root TS issues (#953)

- Converts type assertions to use inference via discriminated unions
- Removes the LayoutElement class (only used by node slots, and recently
reduced to a single function)
- Splits `boundingRect` property out from `Positionable` interface
- Slots now use the standard `boundingRect` property
- Perf improvements / Removes redundant code
This commit is contained in:
filtered
2025-04-21 22:23:09 +10:00
committed by GitHub
parent f7a0a92f3a
commit 2ad1481f02
6 changed files with 92 additions and 95 deletions

View File

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

View File

@@ -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<INodeSlot, "boundingRect">) {
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<INodeInputSlot, "boundingRect">) {
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<INodeOutputSlot, "boundingRect">) {
super(slot)
this.links = slot.links
this._data = slot._data

View File

@@ -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<T> = { [key: string]: T }
@@ -30,6 +29,22 @@ export type SharedIntersection<T1, T2> = {
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<TChild> {
/** All objects owned by the parent object. */
@@ -41,7 +56,7 @@ export interface Parent<TChild> {
*
* May contain other {@link Positionable} objects.
*/
export interface Positionable extends Parent<Positionable> {
export interface Positionable extends Parent<Positionable>, 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<Positionable> {
*/
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<TSlot extends INodeInputSlot | INodeOutputSlo
*/
export type ISlotType = number | string
export interface INodeSlot {
export interface INodeSlot extends HasBoundingRect {
/**
* The name of the slot in English.
* Will be included in the serialized data.
@@ -284,11 +288,8 @@ export interface INodeSlot {
locked?: boolean
nameLocked?: boolean
pos?: Point
/**
* A layout element that is used internally to position the slot.
* Set by {@link LGraphNode.#layoutSlots}.
*/
_layoutElement?: LayoutElement
/** @remarks Automatically calculated; not included in serialisation. */
boundingRect: Rect
/**
* A list of floating link IDs that are connected to this slot.
* This is calculated at runtime; it is **not** serialized.
@@ -322,7 +323,6 @@ export interface IWidgetLocator {
export interface INodeInputSlot extends INodeSlot {
link: LinkId | null
_layoutElement?: LayoutElement
widget?: IWidgetLocator
}
@@ -334,7 +334,6 @@ export interface INodeOutputSlot extends INodeSlot {
links: LinkId[] | null
_data?: unknown
slot_index?: number
_layoutElement?: LayoutElement
}
/** Links */

View File

@@ -1,4 +1,5 @@
import type {
HasBoundingRect,
Point,
ReadOnlyPoint,
ReadOnlyRect,
@@ -146,6 +147,18 @@ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
: true
}
/**
* Returns the centre of a rectangle.
* @param rect The rectangle, as `x, y, width, height`
* @returns The centre of the rectangle, as `x, y`
*/
export function getCentre(rect: ReadOnlyRect): Point {
return [
rect[0] + (rect[2] * 0.5),
rect[1] + (rect[3] * 0.5),
]
}
/**
* Determines if rectangle {@link a} contains the centre point of rectangle {@link b}.
* @param a Container rectangle A as `x, y, width, height`
@@ -331,7 +344,7 @@ export function findPointOnCurve(
}
export function createBounds(
objects: Iterable<{ boundingRect: ReadOnlyRect }>,
objects: Iterable<HasBoundingRect>,
padding: number = 10,
): ReadOnlyRect | null {
const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity])

View File

@@ -56,10 +56,10 @@ export interface SerialisableGraph extends BaseExportedGraph {
extra?: Dictionary<unknown>
}
export type ISerialisableNodeInput = Omit<INodeInputSlot, "_layoutElement" | "widget"> & {
export type ISerialisableNodeInput = Omit<INodeInputSlot, "boundingRect" | "widget"> & {
widget?: { name: string }
}
export type ISerialisableNodeOutput = Omit<INodeOutputSlot, "_layoutElement" | "_data"> & {
export type ISerialisableNodeOutput = Omit<INodeOutputSlot, "boundingRect" | "_data"> & {
widget?: { name: string }
}
@@ -135,7 +135,7 @@ export interface ExportedSubgraph extends ISerialisedGraph {
}
/** Properties shared by subgraph and node I/O slots. */
type SubgraphIOShared = Omit<INodeSlot, "nameLocked" | "locked" | "removable" | "_layoutElement" | "_floatingLinks">
type SubgraphIOShared = Omit<INodeSlot, "nameLocked" | "locked" | "removable" | "boundingRect" | "_floatingLinks">
/** Subgraph I/O slots */
export interface SubgraphIO extends SubgraphIOShared {

View File

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