Fix Rectangle/Rect type compatibility and unify type system

This commit addresses PR review comments by fixing the fundamental
type incompatibility between Rectangle (Array<number>) and strict
tuple types (Rect = [x, y, width, height]).

Key changes:
- Updated Rectangle class methods to accept both Rect and Rectangle unions
- Fixed all measure functions (containsRect, overlapBounding, etc.) to accept Rectangle
- Updated boundingRect interfaces to consistently use Rectangle instead of Rect
- Fixed all tests and Vue components to use Rectangle instead of array literals
- Resolved Point type conflicts between litegraph and pathRenderer modules
- Removed ReadOnlyPoint types and unified with Point arrays

The type system is now consistent: boundingRect properties return Rectangle
objects with full functionality, while Rect remains as a simple tuple type
for data interchange.
This commit is contained in:
bymyself
2025-09-19 11:45:16 -07:00
parent c82c3c24f7
commit c8e1f494fa
27 changed files with 228 additions and 239 deletions

View File

@@ -4,7 +4,7 @@ import type { Ref } from 'vue'
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync' import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems' import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags' import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces' import type { Rect } from '@/lib/litegraph/src/interfaces'
import { LGraphNode } from '@/lib/litegraph/src/litegraph' import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
@@ -71,7 +71,7 @@ export function useSelectionToolboxPosition(
visible.value = true visible.value = true
// Get bounds for all selected items // Get bounds for all selected items
const allBounds: ReadOnlyRect[] = [] const allBounds: Rect[] = []
for (const item of selectableItems) { for (const item of selectableItems) {
// Skip items without valid IDs // Skip items without valid IDs
if (item.id == null) continue if (item.id == null) continue

View File

@@ -1,4 +1,4 @@
import type { Point, ReadOnlyRect, Rect } from './interfaces' import type { Point, Rect } from './interfaces'
import { EaseFunction, Rectangle } from './litegraph' import { EaseFunction, Rectangle } from './litegraph'
export interface DragAndScaleState { export interface DragAndScaleState {
@@ -188,10 +188,7 @@ export class DragAndScale {
* Fits the view to the specified bounds. * Fits the view to the specified bounds.
* @param bounds The bounds to fit the view to, defined by a rectangle. * @param bounds The bounds to fit the view to, defined by a rectangle.
*/ */
fitToBounds( fitToBounds(bounds: Rect, { zoom = 0.75 }: { zoom?: number } = {}): void {
bounds: ReadOnlyRect,
{ zoom = 0.75 }: { zoom?: number } = {}
): void {
const cw = this.element.width / window.devicePixelRatio const cw = this.element.width / window.devicePixelRatio
const ch = this.element.height / window.devicePixelRatio const ch = this.element.height / window.devicePixelRatio
let targetScale = this.scale let targetScale = this.scale
@@ -223,7 +220,7 @@ export class DragAndScale {
* @param bounds The bounds to animate the view to, defined by a rectangle. * @param bounds The bounds to animate the view to, defined by a rectangle.
*/ */
animateToBounds( animateToBounds(
bounds: ReadOnlyRect, bounds: Rect | Rectangle,
setDirty: () => void, setDirty: () => void,
{ {
duration = 350, duration = 350,

View File

@@ -4,6 +4,7 @@ import {
SUBGRAPH_INPUT_ID, SUBGRAPH_INPUT_ID,
SUBGRAPH_OUTPUT_ID SUBGRAPH_OUTPUT_ID
} from '@/lib/litegraph/src/constants' } from '@/lib/litegraph/src/constants'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { UUID } from '@/lib/litegraph/src/utils/uuid' import type { UUID } from '@/lib/litegraph/src/utils/uuid'
import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid' import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
@@ -1707,7 +1708,12 @@ export class LGraph
...subgraphNode.subgraph.groups ...subgraphNode.subgraph.groups
].map((p: { pos: Point; size?: Size }): HasBoundingRect => { ].map((p: { pos: Point; size?: Size }): HasBoundingRect => {
return { return {
boundingRect: [p.pos[0], p.pos[1], p.size?.[0] ?? 0, p.size?.[1] ?? 0] boundingRect: new Rectangle(
p.pos[0],
p.pos[1],
p.size?.[0] ?? 0,
p.size?.[1] ?? 0
)
} }
}) })
const bounds = createBounds(positionables) ?? [0, 0, 0, 0] const bounds = createBounds(positionables) ?? [0, 0, 0, 0]

View File

@@ -47,8 +47,6 @@ import type {
NullableProperties, NullableProperties,
Point, Point,
Positionable, Positionable,
ReadOnlyPoint,
ReadOnlyRect,
Rect, Rect,
Size Size
} from './interfaces' } from './interfaces'
@@ -628,7 +626,7 @@ export class LGraphCanvas
dirty_area?: Rect | null dirty_area?: Rect | null
/** @deprecated Unused */ /** @deprecated Unused */
node_in_panel?: LGraphNode | null node_in_panel?: LGraphNode | null
last_mouse: ReadOnlyPoint = [0, 0] last_mouse: Point = [0, 0]
last_mouseclick: number = 0 last_mouseclick: number = 0
graph: LGraph | Subgraph | null graph: LGraph | Subgraph | null
get _graph(): LGraph | Subgraph { get _graph(): LGraph | Subgraph {
@@ -3166,7 +3164,7 @@ export class LGraphCanvas
LGraphCanvas.active_canvas = this LGraphCanvas.active_canvas = this
this.adjustMouseEvent(e) this.adjustMouseEvent(e)
const mouse: ReadOnlyPoint = [e.clientX, e.clientY] const mouse: Point = [e.clientX, e.clientY]
this.mouse[0] = mouse[0] this.mouse[0] = mouse[0]
this.mouse[1] = mouse[1] this.mouse[1] = mouse[1]
const delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]] const delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]]
@@ -4829,7 +4827,7 @@ export class LGraphCanvas
} }
/** Get the target snap / highlight point in graph space */ /** Get the target snap / highlight point in graph space */
#getHighlightPosition(): ReadOnlyPoint { #getHighlightPosition(): Point {
return LiteGraph.snaps_for_comfy return LiteGraph.snaps_for_comfy
? this.linkConnector.state.snapLinksPos ?? ? this.linkConnector.state.snapLinksPos ??
this._highlight_pos ?? this._highlight_pos ??
@@ -4844,7 +4842,7 @@ export class LGraphCanvas
*/ */
#renderSnapHighlight( #renderSnapHighlight(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
highlightPos: ReadOnlyPoint highlightPos: Point
): void { ): void {
const linkConnectorSnap = !!this.linkConnector.state.snapLinksPos const linkConnectorSnap = !!this.linkConnector.state.snapLinksPos
if (!this._highlight_pos && !linkConnectorSnap) return if (!this._highlight_pos && !linkConnectorSnap) return
@@ -5930,8 +5928,8 @@ export class LGraphCanvas
*/ */
renderLink( renderLink(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
a: ReadOnlyPoint, a: Point,
b: ReadOnlyPoint, b: Point,
link: LLink | null, link: LLink | null,
skip_border: boolean, skip_border: boolean,
flow: number | null, flow: number | null,
@@ -5948,9 +5946,9 @@ export class LGraphCanvas
/** When defined, render data will be saved to this reroute instead of the {@link link}. */ /** When defined, render data will be saved to this reroute instead of the {@link link}. */
reroute?: Reroute reroute?: Reroute
/** Offset of the bezier curve control point from {@link a point a} (output side) */ /** Offset of the bezier curve control point from {@link a point a} (output side) */
startControl?: ReadOnlyPoint startControl?: Point
/** Offset of the bezier curve control point from {@link b point b} (input side) */ /** Offset of the bezier curve control point from {@link b point b} (input side) */
endControl?: ReadOnlyPoint endControl?: Point
/** Number of sublines (useful to represent vec3 or rgb) @todo If implemented, refactor calculations out of the loop */ /** Number of sublines (useful to represent vec3 or rgb) @todo If implemented, refactor calculations out of the loop */
num_sublines?: number num_sublines?: number
/** Whether this is a floating link segment */ /** Whether this is a floating link segment */
@@ -8421,7 +8419,7 @@ export class LGraphCanvas
* Starts an animation to fit the view around the specified selection of nodes. * Starts an animation to fit the view around the specified selection of nodes.
* @param bounds The bounds to animate the view to, defined by a rectangle. * @param bounds The bounds to animate the view to, defined by a rectangle.
*/ */
animateToBounds(bounds: ReadOnlyRect, options: AnimationOptions = {}) { animateToBounds(bounds: Rect | Rectangle, options: AnimationOptions = {}) {
const setDirty = () => this.setDirty(true, true) const setDirty = () => this.setDirty(true, true)
this.ds.animateToBounds(bounds, setDirty, options) this.ds.animateToBounds(bounds, setDirty, options)
} }

View File

@@ -1,4 +1,5 @@
import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError' import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { LGraph } from './LGraph' import type { LGraph } from './LGraph'
import { LGraphCanvas } from './LGraphCanvas' import { LGraphCanvas } from './LGraphCanvas'
@@ -11,7 +12,6 @@ import type {
IPinnable, IPinnable,
Point, Point,
Positionable, Positionable,
ReadOnlyRect,
Size Size
} from './interfaces' } from './interfaces'
import { LiteGraph } from './litegraph' import { LiteGraph } from './litegraph'
@@ -108,8 +108,13 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
this._size[1] = Math.max(LGraphGroup.minHeight, v[1]) this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
} }
get boundingRect(): ReadOnlyRect { get boundingRect(): Rectangle {
return [this._pos[0], this._pos[1], this._size[0], this._size[1]] as const return Rectangle.from([
this._pos[0],
this._pos[1],
this._size[0],
this._size[1]
])
} }
get nodes() { get nodes() {
@@ -214,7 +219,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
) )
if (LiteGraph.highlight_selected_group && this.selected) { if (LiteGraph.highlight_selected_group && this.selected) {
strokeShape(ctx, [...this.boundingRect], { strokeShape(ctx, this.boundingRect, {
title_height: this.titleHeight, title_height: this.titleHeight,
padding padding
}) })

View File

@@ -18,7 +18,6 @@ import type { Reroute, RerouteId } from './Reroute'
import { getNodeInputOnPos, getNodeOutputOnPos } from './canvas/measureSlots' import { getNodeInputOnPos, getNodeOutputOnPos } from './canvas/measureSlots'
import type { IDrawBoundingOptions } from './draw' import type { IDrawBoundingOptions } from './draw'
import { NullGraphError } from './infrastructure/NullGraphError' import { NullGraphError } from './infrastructure/NullGraphError'
import type { ReadOnlyRectangle } from './infrastructure/Rectangle'
import { Rectangle } from './infrastructure/Rectangle' import { Rectangle } from './infrastructure/Rectangle'
import type { import type {
ColorOption, ColorOption,
@@ -37,8 +36,6 @@ import type {
ISlotType, ISlotType,
Point, Point,
Positionable, Positionable,
ReadOnlyPoint,
ReadOnlyRect,
Rect, Rect,
Size Size
} from './interfaces' } from './interfaces'
@@ -387,7 +384,7 @@ export class LGraphNode
* Called once at the start of every frame. Caller may change the values in {@link out}, which will be reflected in {@link boundingRect}. * Called once at the start of every frame. Caller may change the values in {@link out}, which will be reflected in {@link boundingRect}.
* WARNING: Making changes to boundingRect via onBounding is poorly supported, and will likely result in strange behaviour. * WARNING: Making changes to boundingRect via onBounding is poorly supported, and will likely result in strange behaviour.
*/ */
onBounding?(this: LGraphNode, out: Rect): void onBounding?(this: LGraphNode, out: Rectangle): void
console?: string[] console?: string[]
_level?: number _level?: number
_shape?: RenderShape _shape?: RenderShape
@@ -418,7 +415,7 @@ export class LGraphNode
* Rect describing the node area, including shadows and any protrusions. * Rect describing the node area, including shadows and any protrusions.
* Determines if the node is visible. Calculated once at the start of every frame. * Determines if the node is visible. Calculated once at the start of every frame.
*/ */
get renderArea(): ReadOnlyRect { get renderArea(): Rect {
return this.#renderArea return this.#renderArea
} }
@@ -429,12 +426,12 @@ export class LGraphNode
* *
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame. * Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
*/ */
get boundingRect(): ReadOnlyRectangle { get boundingRect(): Rectangle {
return this.#boundingRect return this.#boundingRect
} }
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */ /** The offset from {@link pos} to the top-left of {@link boundingRect}. */
get boundingOffset(): ReadOnlyPoint { get boundingOffset(): Point {
const { const {
pos: [posX, posY], pos: [posX, posY],
boundingRect: [bX, bY] boundingRect: [bX, bY]
@@ -1978,7 +1975,7 @@ export class LGraphNode
* @param out `x, y, width, height` are written to this array. * @param out `x, y, width, height` are written to this array.
* @param ctx The canvas context to use for measuring text. * @param ctx The canvas context to use for measuring text.
*/ */
measure(out: Rect, ctx: CanvasRenderingContext2D): void { measure(out: Rectangle, ctx: CanvasRenderingContext2D): void {
const titleMode = this.title_mode const titleMode = this.title_mode
const renderTitle = const renderTitle =
titleMode != TitleMode.TRANSPARENT_TITLE && titleMode != TitleMode.TRANSPARENT_TITLE &&
@@ -3844,7 +3841,7 @@ export class LGraphNode
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
} }
#measureSlots(): ReadOnlyRect | null { #measureSlots(): Rect | null {
const slots: (NodeInputSlot | NodeOutputSlot)[] = [] const slots: (NodeInputSlot | NodeOutputSlot)[] = []
for (const [slotIndex, slot] of this.#concreteInputs.entries()) { for (const [slotIndex, slot] of this.#concreteInputs.entries()) {

View File

@@ -1,3 +1,4 @@
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types' import { LayoutSource } from '@/renderer/core/layout/types'
@@ -12,8 +13,8 @@ import type {
LinkSegment, LinkSegment,
Point, Point,
Positionable, Positionable,
ReadOnlyRect, ReadonlyLinkNetwork,
ReadonlyLinkNetwork Rect
} from './interfaces' } from './interfaces'
import { distance, isPointInRect } from './measure' import { distance, isPointInRect } from './measure'
import type { Serialisable, SerialisableReroute } from './types/serialisation' import type { Serialisable, SerialisableReroute } from './types/serialisation'
@@ -87,17 +88,17 @@ export class Reroute
} }
/** @inheritdoc */ /** @inheritdoc */
get boundingRect(): ReadOnlyRect { get boundingRect(): Rectangle {
const { radius } = Reroute const { radius } = Reroute
const [x, y] = this.#pos const [x, y] = this.#pos
return [x - radius, y - radius, 2 * radius, 2 * radius] return Rectangle.from([x - radius, y - radius, 2 * radius, 2 * radius])
} }
/** /**
* Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection. * Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection.
* Eliminates most hover positions using an extremely cheap check. * Eliminates most hover positions using an extremely cheap check.
*/ */
get #hoverArea(): ReadOnlyRect { get #hoverArea(): Rect {
const xOffset = 2 * Reroute.slotOffset const xOffset = 2 * Reroute.slotOffset
const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius) const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius)

View File

@@ -67,7 +67,7 @@ interface IDrawTextInAreaOptions {
*/ */
export function strokeShape( export function strokeShape(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
area: Rect, area: Rect | Rectangle,
{ {
shape = RenderShape.BOX, shape = RenderShape.BOX,
round_radius, round_radius,

View File

@@ -1,10 +1,6 @@
import { clamp } from 'es-toolkit/compat' import { clamp } from 'es-toolkit/compat'
import type { import type { Rect, Size } from '@/lib/litegraph/src/interfaces'
ReadOnlyRect,
ReadOnlySize,
Size
} from '@/lib/litegraph/src/interfaces'
/** /**
* Basic width and height, with min/max constraints. * Basic width and height, with min/max constraints.
@@ -55,15 +51,15 @@ export class ConstrainedSize {
this.desiredHeight = height this.desiredHeight = height
} }
static fromSize(size: ReadOnlySize): ConstrainedSize { static fromSize(size: Size): ConstrainedSize {
return new ConstrainedSize(size[0], size[1]) return new ConstrainedSize(size[0], size[1])
} }
static fromRect(rect: ReadOnlyRect): ConstrainedSize { static fromRect(rect: Rect): ConstrainedSize {
return new ConstrainedSize(rect[2], rect[3]) return new ConstrainedSize(rect[2], rect[3])
} }
setSize(size: ReadOnlySize): void { setSize(size: Size): void {
this.desiredWidth = size[0] this.desiredWidth = size[0]
this.desiredHeight = size[1] this.desiredHeight = size[1]
} }

View File

@@ -1,6 +1,6 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph' import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LLink, ResolvedConnection } from '@/lib/litegraph/src/LLink' import type { LLink, ResolvedConnection } from '@/lib/litegraph/src/LLink'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces' import type { Rect } from '@/lib/litegraph/src/interfaces'
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph' import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
import type { import type {
ExportedSubgraph, ExportedSubgraph,
@@ -29,7 +29,7 @@ export interface LGraphEventMap {
/** The type of subgraph to create. */ /** The type of subgraph to create. */
subgraph: Subgraph subgraph: Subgraph
/** The boundary around every item that was moved into the subgraph. */ /** The boundary around every item that was moved into the subgraph. */
bounds: ReadOnlyRect bounds: Rect
/** The raw data that was used to create the subgraph. */ /** The raw data that was used to create the subgraph. */
exportedSubgraph: ExportedSubgraph exportedSubgraph: ExportedSubgraph
/** The links that were used to create the subgraph. */ /** The links that were used to create the subgraph. */

View File

@@ -1,9 +1,7 @@
import type { import type {
CompassCorners, CompassCorners,
Point, Point,
ReadOnlyPoint, Rect,
ReadOnlyRect,
ReadOnlySize,
Size Size
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { isInRectangle } from '@/lib/litegraph/src/measure' import { isInRectangle } from '@/lib/litegraph/src/measure'
@@ -29,7 +27,7 @@ export class Rectangle extends Array<number> {
this.length = 4 this.length = 4
} }
static override from([x, y, width, height]: ReadOnlyRect): Rectangle { static override from([x, y, width, height]: Rect): Rectangle {
return new Rectangle(x, y, width, height) return new Rectangle(x, y, width, height)
} }
@@ -54,17 +52,13 @@ export class Rectangle extends Array<number> {
* @param height The height of the rectangle. Default: {@link width} * @param height The height of the rectangle. Default: {@link width}
* @returns A new rectangle whose centre is at {@link x} * @returns A new rectangle whose centre is at {@link x}
*/ */
static fromCentre( static fromCentre([x, y]: Point, width: number, height = width): Rectangle {
[x, y]: ReadOnlyPoint,
width: number,
height = width
): Rectangle {
const left = x - width * 0.5 const left = x - width * 0.5
const top = y - height * 0.5 const top = y - height * 0.5
return new Rectangle(left, top, width, height) return new Rectangle(left, top, width, height)
} }
static ensureRect(rect: ReadOnlyRect): Rectangle { static ensureRect(rect: Rect | Rectangle): Rectangle {
return rect instanceof Rectangle return rect instanceof Rectangle
? rect ? rect
: new Rectangle(rect[0], rect[1], rect[2], rect[3]) : new Rectangle(rect[0], rect[1], rect[2], rect[3])
@@ -77,7 +71,7 @@ export class Rectangle extends Array<number> {
return [this[0], this[1]] return [this[0], this[1]]
} }
set pos(value: ReadOnlyPoint) { set pos(value: Point) {
this[0] = value[0] this[0] = value[0]
this[1] = value[1] this[1] = value[1]
} }
@@ -89,7 +83,7 @@ export class Rectangle extends Array<number> {
return [this[2], this[3]] return [this[2], this[3]]
} }
set size(value: ReadOnlySize) { set size(value: Size) {
this[2] = value[0] this[2] = value[0]
this[3] = value[1] this[3] = value[1]
} }
@@ -182,7 +176,7 @@ export class Rectangle extends Array<number> {
* Updates the rectangle to the values of {@link rect}. * Updates the rectangle to the values of {@link rect}.
* @param rect The rectangle to update to. * @param rect The rectangle to update to.
*/ */
updateTo(rect: ReadOnlyRect) { updateTo(rect: Rect) {
this[0] = rect[0] this[0] = rect[0]
this[1] = rect[1] this[1] = rect[1]
this[2] = rect[2] this[2] = rect[2]
@@ -205,7 +199,7 @@ export class Rectangle extends Array<number> {
* @param point The point to check * @param point The point to check
* @returns `true` if {@link point} is inside this rectangle, otherwise `false`. * @returns `true` if {@link point} is inside this rectangle, otherwise `false`.
*/ */
containsPoint([x, y]: ReadOnlyPoint): boolean { containsPoint([x, y]: Point): boolean {
const [left, top, width, height] = this const [left, top, width, height] = this
return x >= left && x < left + width && y >= top && y < top + height return x >= left && x < left + width && y >= top && y < top + height
} }
@@ -216,7 +210,7 @@ export class Rectangle extends Array<number> {
* @param other The rectangle to check * @param other The rectangle to check
* @returns `true` if {@link other} is inside this rectangle, otherwise `false`. * @returns `true` if {@link other} is inside this rectangle, otherwise `false`.
*/ */
containsRect(other: ReadOnlyRect): boolean { containsRect(other: Rect | Rectangle): boolean {
const { right, bottom } = this const { right, bottom } = this
const otherRight = other[0] + other[2] const otherRight = other[0] + other[2]
const otherBottom = other[1] + other[3] const otherBottom = other[1] + other[3]
@@ -241,7 +235,7 @@ export class Rectangle extends Array<number> {
* @param rect The rectangle to check * @param rect The rectangle to check
* @returns `true` if {@link rect} overlaps with this rectangle, otherwise `false`. * @returns `true` if {@link rect} overlaps with this rectangle, otherwise `false`.
*/ */
overlaps(rect: ReadOnlyRect): boolean { overlaps(rect: Rect | Rectangle): boolean {
return ( return (
this.x < rect[0] + rect[2] && this.x < rect[0] + rect[2] &&
this.y < rect[1] + rect[3] && this.y < rect[1] + rect[3] &&
@@ -374,12 +368,12 @@ export class Rectangle extends Array<number> {
} }
/** @returns The offset from the top-left of this rectangle to the point [{@link x}, {@link y}], as a new {@link Point}. */ /** @returns The offset from the top-left of this rectangle to the point [{@link x}, {@link y}], as a new {@link Point}. */
getOffsetTo([x, y]: ReadOnlyPoint): Point { getOffsetTo([x, y]: Point): Point {
return [x - this[0], y - this[1]] return [x - this[0], y - this[1]]
} }
/** @returns The offset from the point [{@link x}, {@link y}] to the top-left of this rectangle, as a new {@link Point}. */ /** @returns The offset from the point [{@link x}, {@link y}] to the top-left of this rectangle, as a new {@link Point}. */
getOffsetFrom([x, y]: ReadOnlyPoint): Point { getOffsetFrom([x, y]: Point): Point {
return [this[0] - x, this[1] - y] return [this[0] - x, this[1] - y]
} }
@@ -460,14 +454,4 @@ export class Rectangle extends Array<number> {
} }
} }
export type ReadOnlyRectangle = Omit< // ReadOnlyRectangle is now just Rectangle since we unified the types
Readonly<Rectangle>,
| 'setHeightBottomAnchored'
| 'setWidthRightAnchored'
| 'resizeTopLeft'
| 'resizeBottomLeft'
| 'resizeTopRight'
| 'resizeBottomRight'
| 'resizeBottomRight'
| 'updateTo'
>

View File

@@ -1,4 +1,4 @@
import type { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle' import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type { ContextMenu } from './ContextMenu' import type { ContextMenu } from './ContextMenu'
@@ -60,7 +60,7 @@ export interface HasBoundingRect {
* @readonly * @readonly
* @see {@link move} * @see {@link move}
*/ */
readonly boundingRect: ReadOnlyRect readonly boundingRect: Rectangle
} }
/** An object containing a set of child objects */ /** An object containing a set of child objects */
@@ -230,50 +230,8 @@ export type Point = [x: number, y: number]
/** A size represented as `[width, height]` */ /** A size represented as `[width, height]` */
export type Size = [width: number, height: number] export type Size = [width: number, height: number]
/** A very firm array */
type ArRect = [x: number, y: number, width: number, height: number]
/** A rectangle starting at top-left coordinates `[x, y, width, height]` */ /** A rectangle starting at top-left coordinates `[x, y, width, height]` */
export type Rect = ArRect | Float32Array | Float64Array | number[] export type Rect = [number, number, number, number]
/** A point represented as `[x, y]` co-ordinates that will not be modified */
export type ReadOnlyPoint =
| readonly [x: number, y: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
| readonly number[]
/** A size represented as `[width, height]` that will not be modified */
export type ReadOnlySize =
| readonly [width: number, height: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
| readonly number[]
/** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */
export type ReadOnlyRect =
| readonly [x: number, y: number, width: number, height: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
| readonly number[]
type TypedArrays =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
type TypedBigIntArrays = BigInt64Array | BigUint64Array
export type ReadOnlyTypedArray<T extends TypedArrays | TypedBigIntArrays> =
Omit<
Readonly<T>,
'fill' | 'copyWithin' | 'reverse' | 'set' | 'sort' | 'subarray'
>
/** Union of property names that are of type Match */ /** Union of property names that are of type Match */
type KeysOfType<T, Match> = Exclude< type KeysOfType<T, Match> = Exclude<
@@ -332,7 +290,7 @@ export interface INodeSlot extends HasBoundingRect {
nameLocked?: boolean nameLocked?: boolean
pos?: Point pos?: Point
/** @remarks Automatically calculated; not included in serialisation. */ /** @remarks Automatically calculated; not included in serialisation. */
boundingRect: Rect boundingRect: Rectangle
/** /**
* A list of floating link IDs that are connected to this slot. * A list of floating link IDs that are connected to this slot.
* This is calculated at runtime; it is **not** serialized. * This is calculated at runtime; it is **not** serialized.

View File

@@ -1,10 +1,5 @@
import type { import type { Rectangle } from './infrastructure/Rectangle'
HasBoundingRect, import type { HasBoundingRect, Point, Rect } from './interfaces'
Point,
ReadOnlyPoint,
ReadOnlyRect,
Rect
} from './interfaces'
import { Alignment, LinkDirection, hasFlag } from './types/globalEnums' import { Alignment, LinkDirection, hasFlag } from './types/globalEnums'
/** /**
@@ -13,7 +8,7 @@ import { Alignment, LinkDirection, hasFlag } from './types/globalEnums'
* @param b Point b as `x, y` * @param b Point b as `x, y`
* @returns Distance between point {@link a} & {@link b} * @returns Distance between point {@link a} & {@link b}
*/ */
export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number { export function distance(a: Point, b: Point): number {
return Math.sqrt( return Math.sqrt(
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])
) )
@@ -61,10 +56,7 @@ export function isInRectangle(
* @param rect The rectangle, as `x, y, width, height` * @param rect The rectangle, as `x, y, width, height`
* @returns `true` if the point is inside the rect, otherwise `false` * @returns `true` if the point is inside the rect, otherwise `false`
*/ */
export function isPointInRect( export function isPointInRect(point: Point, rect: Rect | Rectangle): boolean {
point: ReadOnlyPoint,
rect: ReadOnlyRect
): boolean {
return ( return (
point[0] >= rect[0] && point[0] >= rect[0] &&
point[0] < rect[0] + rect[2] && point[0] < rect[0] + rect[2] &&
@@ -80,7 +72,11 @@ export function isPointInRect(
* @param rect The rectangle, as `x, y, width, height` * @param rect The rectangle, as `x, y, width, height`
* @returns `true` if the point is inside the rect, otherwise `false` * @returns `true` if the point is inside the rect, otherwise `false`
*/ */
export function isInRect(x: number, y: number, rect: ReadOnlyRect): boolean { export function isInRect(
x: number,
y: number,
rect: Rect | Rectangle
): boolean {
return ( return (
x >= rect[0] && x >= rect[0] &&
x < rect[0] + rect[2] && x < rect[0] + rect[2] &&
@@ -121,7 +117,10 @@ export function isInsideRectangle(
* @param b Rectangle B as `x, y, width, height` * @param b Rectangle B as `x, y, width, height`
* @returns `true` if rectangles overlap, otherwise `false` * @returns `true` if rectangles overlap, otherwise `false`
*/ */
export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean { export function overlapBounding(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
const aRight = a[0] + a[2] const aRight = a[0] + a[2]
const aBottom = a[1] + a[3] const aBottom = a[1] + a[3]
const bRight = b[0] + b[2] const bRight = b[0] + b[2]
@@ -137,7 +136,7 @@ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
* @param rect The rectangle, as `x, y, width, height` * @param rect The rectangle, as `x, y, width, height`
* @returns The centre of the rectangle, as `x, y` * @returns The centre of the rectangle, as `x, y`
*/ */
export function getCentre(rect: ReadOnlyRect): Point { export function getCentre(rect: Rect | Rectangle): Point {
return [rect[0] + rect[2] * 0.5, rect[1] + rect[3] * 0.5] return [rect[0] + rect[2] * 0.5, rect[1] + rect[3] * 0.5]
} }
@@ -147,7 +146,10 @@ export function getCentre(rect: ReadOnlyRect): Point {
* @param b Sub-rectangle B as `x, y, width, height` * @param b Sub-rectangle B as `x, y, width, height`
* @returns `true` if {@link a} contains most of {@link b}, otherwise `false` * @returns `true` if {@link a} contains most of {@link b}, otherwise `false`
*/ */
export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean { export function containsCentre(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
const centreX = b[0] + b[2] * 0.5 const centreX = b[0] + b[2] * 0.5
const centreY = b[1] + b[3] * 0.5 const centreY = b[1] + b[3] * 0.5
return isInRect(centreX, centreY, a) return isInRect(centreX, centreY, a)
@@ -159,7 +161,10 @@ export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
* @param b Sub-rectangle B as `x, y, width, height` * @param b Sub-rectangle B as `x, y, width, height`
* @returns `true` if {@link a} wholly contains {@link b}, otherwise `false` * @returns `true` if {@link a} wholly contains {@link b}, otherwise `false`
*/ */
export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean { export function containsRect(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
const aRight = a[0] + a[2] const aRight = a[0] + a[2]
const aBottom = a[1] + a[3] const aBottom = a[1] + a[3]
const bRight = b[0] + b[2] const bRight = b[0] + b[2]
@@ -289,8 +294,8 @@ export function rotateLink(
* the right * the right
*/ */
export function getOrientation( export function getOrientation(
lineStart: ReadOnlyPoint, lineStart: Point,
lineEnd: ReadOnlyPoint, lineEnd: Point,
x: number, x: number,
y: number y: number
): number { ): number {
@@ -310,10 +315,10 @@ export function getOrientation(
*/ */
export function findPointOnCurve( export function findPointOnCurve(
out: Point, out: Point,
a: ReadOnlyPoint, a: Point,
b: ReadOnlyPoint, b: Point,
controlA: ReadOnlyPoint, controlA: Point,
controlB: ReadOnlyPoint, controlB: Point,
t: number = 0.5 t: number = 0.5
): void { ): void {
const iT = 1 - t const iT = 1 - t
@@ -330,7 +335,7 @@ export function findPointOnCurve(
export function createBounds( export function createBounds(
objects: Iterable<HasBoundingRect>, objects: Iterable<HasBoundingRect>,
padding: number = 10 padding: number = 10
): ReadOnlyRect | null { ): Rect | null {
const bounds: [number, number, number, number] = [ const bounds: [number, number, number, number] = [
Infinity, Infinity,
Infinity, Infinity,
@@ -384,11 +389,11 @@ export function snapPoint(pos: Point | Rect, snapTo: number): boolean {
* @returns The original {@link rect}, modified in place. * @returns The original {@link rect}, modified in place.
*/ */
export function alignToContainer( export function alignToContainer(
rect: Rect, rect: Rect | Rectangle,
anchors: Alignment, anchors: Alignment,
[containerX, containerY, containerWidth, containerHeight]: ReadOnlyRect, [containerX, containerY, containerWidth, containerHeight]: Rect | Rectangle,
[insetX, insetY]: ReadOnlyPoint = [0, 0] [insetX, insetY]: Point = [0, 0]
): Rect { ): Rect | Rectangle {
if (hasFlag(anchors, Alignment.Left)) { if (hasFlag(anchors, Alignment.Left)) {
// Left // Left
rect[0] = containerX + insetX rect[0] = containerX + insetX
@@ -427,11 +432,11 @@ export function alignToContainer(
* @returns The original {@link rect}, modified in place. * @returns The original {@link rect}, modified in place.
*/ */
export function alignOutsideContainer( export function alignOutsideContainer(
rect: Rect, rect: Rect | Rectangle,
anchors: Alignment, anchors: Alignment,
[otherX, otherY, otherWidth, otherHeight]: ReadOnlyRect, [otherX, otherY, otherWidth, otherHeight]: Rect | Rectangle,
[outsetX, outsetY]: ReadOnlyPoint = [0, 0] [outsetX, outsetY]: Point = [0, 0]
): Rect { ): Rect | Rectangle {
if (hasFlag(anchors, Alignment.Left)) { if (hasFlag(anchors, Alignment.Left)) {
// Left // Left
rect[0] = otherX - outsetX - rect[2] rect[0] = otherX - outsetX - rect[2]

View File

@@ -5,7 +5,7 @@ import type {
INodeInputSlot, INodeInputSlot,
INodeOutputSlot, INodeOutputSlot,
OptionalProps, OptionalProps,
ReadOnlyPoint Point
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot' import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
@@ -32,7 +32,7 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
this.#widget = widget ? new WeakRef(widget) : undefined this.#widget = widget ? new WeakRef(widget) : undefined
} }
get collapsedPos(): ReadOnlyPoint { get collapsedPos(): Point {
return [0, LiteGraph.NODE_TITLE_HEIGHT * -0.5] return [0, LiteGraph.NODE_TITLE_HEIGHT * -0.5]
} }

View File

@@ -5,7 +5,7 @@ import type {
INodeInputSlot, INodeInputSlot,
INodeOutputSlot, INodeOutputSlot,
OptionalProps, OptionalProps,
ReadOnlyPoint Point
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot' import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
@@ -24,7 +24,7 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
return false return false
} }
get collapsedPos(): ReadOnlyPoint { get collapsedPos(): Point {
return [ return [
this.#node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH, this.#node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH,
LiteGraph.NODE_TITLE_HEIGHT * -0.5 LiteGraph.NODE_TITLE_HEIGHT * -0.5

View File

@@ -8,8 +8,7 @@ import type {
INodeSlot, INodeSlot,
ISubgraphInput, ISubgraphInput,
OptionalProps, OptionalProps,
Point, Point
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { LiteGraph, Rectangle } from '@/lib/litegraph/src/litegraph' import { LiteGraph, Rectangle } from '@/lib/litegraph/src/litegraph'
import { getCentre } from '@/lib/litegraph/src/measure' import { getCentre } from '@/lib/litegraph/src/measure'
@@ -36,7 +35,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
pos?: Point pos?: Point
/** The offset from the parent node to the centre point of this slot. */ /** The offset from the parent node to the centre point of this slot. */
get #centreOffset(): ReadOnlyPoint { get #centreOffset(): Point {
const nodePos = this.node.pos const nodePos = this.node.pos
const { boundingRect } = this const { boundingRect } = this
@@ -52,7 +51,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
} }
/** The center point of this slot when the node is collapsed. */ /** The center point of this slot when the node is collapsed. */
abstract get collapsedPos(): ReadOnlyPoint abstract get collapsedPos(): Point
#node: LGraphNode #node: LGraphNode
get node(): LGraphNode { get node(): LGraphNode {

View File

@@ -7,7 +7,7 @@ import type {
INodeInputSlot, INodeInputSlot,
INodeOutputSlot, INodeOutputSlot,
Point, Point,
ReadOnlyRect Rect
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums' import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
@@ -213,7 +213,7 @@ export class SubgraphInput extends SubgraphSlot {
} }
/** For inputs, x is the right edge of the input node. */ /** For inputs, x is the right edge of the input node. */
override arrange(rect: ReadOnlyRect): void { override arrange(rect: Rect): void {
const [right, top, width, height] = rect const [right, top, width, height] = rect
const { boundingRect: b, pos } = this const { boundingRect: b, pos } = this

View File

@@ -7,7 +7,7 @@ import type {
INodeInputSlot, INodeInputSlot,
INodeOutputSlot, INodeOutputSlot,
Point, Point,
ReadOnlyRect Rect
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums' import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
@@ -119,7 +119,7 @@ export class SubgraphOutput extends SubgraphSlot {
return [x + height, y + height * 0.5] return [x + height, y + height * 0.5]
} }
override arrange(rect: ReadOnlyRect): void { override arrange(rect: Rect): void {
const [left, top, width, height] = rect const [left, top, width, height] = rect
const { boundingRect: b, pos } = this const { boundingRect: b, pos } = this

View File

@@ -11,8 +11,8 @@ import type {
INodeInputSlot, INodeInputSlot,
INodeOutputSlot, INodeOutputSlot,
Point, Point,
ReadOnlyRect, Rect,
ReadOnlySize Size
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { SlotBase } from '@/lib/litegraph/src/node/SlotBase' import { SlotBase } from '@/lib/litegraph/src/node/SlotBase'
@@ -133,7 +133,7 @@ export abstract class SubgraphSlot
} }
} }
measure(): ReadOnlySize { measure(): Size {
const width = LGraphCanvas._measureText?.(this.displayName) ?? 0 const width = LGraphCanvas._measureText?.(this.displayName) ?? 0
const { defaultHeight } = SubgraphSlot const { defaultHeight } = SubgraphSlot
@@ -141,7 +141,7 @@ export abstract class SubgraphSlot
return this.measurement.toSize() return this.measurement.toSize()
} }
abstract arrange(rect: ReadOnlyRect): void abstract arrange(rect: Rect): void
abstract connect( abstract connect(
slot: INodeInputSlot | INodeOutputSlot, slot: INodeInputSlot | INodeOutputSlot,

View File

@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, vi } from 'vitest' import { afterEach, beforeEach, describe, expect, vi } from 'vitest'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { INodeInputSlot, Point } from '@/lib/litegraph/src/interfaces' import type { INodeInputSlot, Point } from '@/lib/litegraph/src/interfaces'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LGraph } from '@/lib/litegraph/src/litegraph' import { LGraph } from '@/lib/litegraph/src/litegraph'
@@ -571,7 +572,7 @@ describe('LGraphNode', () => {
name: 'test_in', name: 'test_in',
type: 'string', type: 'string',
link: null, link: null,
boundingRect: new Float32Array([0, 0, 0, 0]) boundingRect: new Rectangle(0, 0, 0, 0)
} }
}) })
test('should return position based on title height when collapsed', () => { test('should return position based on title height when collapsed', () => {
@@ -594,7 +595,7 @@ describe('LGraphNode', () => {
name: 'test_in_2', name: 'test_in_2',
type: 'number', type: 'number',
link: null, link: null,
boundingRect: new Float32Array([0, 0, 0, 0]) boundingRect: new Rectangle(0, 0, 0, 0)
} }
node.inputs = [inputSlot, inputSlot2] node.inputs = [inputSlot, inputSlot2]
const slotIndex = 0 const slotIndex = 0
@@ -629,13 +630,13 @@ describe('LGraphNode', () => {
name: 'in0', name: 'in0',
type: 'string', type: 'string',
link: null, link: null,
boundingRect: new Float32Array([0, 0, 0, 0]) boundingRect: new Rectangle(0, 0, 0, 0)
} }
const input1: INodeInputSlot = { const input1: INodeInputSlot = {
name: 'in1', name: 'in1',
type: 'number', type: 'number',
link: null, link: null,
boundingRect: new Float32Array([0, 0, 0, 0]), boundingRect: new Rectangle(0, 0, 0, 0),
pos: [5, 45] pos: [5, 45]
} }
node.inputs = [input0, input1] node.inputs = [input0, input1]

View File

@@ -1,5 +1,6 @@
import { test as baseTest } from 'vitest' import { test as baseTest } from 'vitest'
import { Rectangle } from '../src/infrastructure/Rectangle'
import type { Point, Rect } from '../src/interfaces' import type { Point, Rect } from '../src/interfaces'
import { import {
addDirectionalOffset, addDirectionalOffset,
@@ -131,8 +132,8 @@ test('snapPoint correctly snaps points to grid', ({ expect }) => {
test('createBounds correctly creates bounding box', ({ expect }) => { test('createBounds correctly creates bounding box', ({ expect }) => {
const objects = [ const objects = [
{ boundingRect: [0, 0, 10, 10] as Rect }, { boundingRect: new Rectangle(0, 0, 10, 10) },
{ boundingRect: [5, 5, 10, 10] as Rect } { boundingRect: new Rectangle(5, 5, 10, 10) }
] ]
const defaultBounds = createBounds(objects) const defaultBounds = createBounds(objects)

View File

@@ -7,11 +7,14 @@
* Maintains backward compatibility with existing litegraph integration. * Maintains backward compatibility with existing litegraph integration.
*/ */
import type { LGraph } from '@/lib/litegraph/src/LGraph' import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LLink } from '@/lib/litegraph/src/LLink' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink } from '@/lib/litegraph/src/LLink'
import type { Reroute } from '@/lib/litegraph/src/Reroute' import type { Reroute } from '@/lib/litegraph/src/Reroute'
import type { import type {
CanvasColour, CanvasColour,
ReadOnlyPoint INodeInputSlot,
INodeOutputSlot,
Point as LitegraphPoint
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { import {
@@ -24,6 +27,7 @@ import {
type ArrowShape, type ArrowShape,
CanvasPathRenderer, CanvasPathRenderer,
type Direction, type Direction,
type DragLinkData,
type LinkRenderData, type LinkRenderData,
type RenderContext as PathRenderContext, type RenderContext as PathRenderContext,
type Point, type Point,
@@ -205,6 +209,7 @@ export class LitegraphLinkAdapter {
case LinkDirection.DOWN: case LinkDirection.DOWN:
return 'down' return 'down'
case LinkDirection.CENTER: case LinkDirection.CENTER:
case LinkDirection.NONE:
return 'none' return 'none'
default: default:
return 'right' return 'right'
@@ -306,22 +311,22 @@ export class LitegraphLinkAdapter {
* Critically: does nothing for CENTER/NONE directions (no case for them) * Critically: does nothing for CENTER/NONE directions (no case for them)
*/ */
private applySplineOffset( private applySplineOffset(
point: Point, point: LitegraphPoint,
direction: LinkDirection, direction: LinkDirection,
distance: number distance: number
): void { ): void {
switch (direction) { switch (direction) {
case LinkDirection.LEFT: case LinkDirection.LEFT:
point.x -= distance point[0] -= distance
break break
case LinkDirection.RIGHT: case LinkDirection.RIGHT:
point.x += distance point[0] += distance
break break
case LinkDirection.UP: case LinkDirection.UP:
point.y -= distance point[1] -= distance
break break
case LinkDirection.DOWN: case LinkDirection.DOWN:
point.y += distance point[1] += distance
break break
// CENTER and NONE: no offset applied (original behavior) // CENTER and NONE: no offset applied (original behavior)
} }
@@ -333,8 +338,8 @@ export class LitegraphLinkAdapter {
*/ */
renderLinkDirect( renderLinkDirect(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
a: ReadOnlyPoint, a: LitegraphPoint,
b: ReadOnlyPoint, b: LitegraphPoint,
link: LLink | null, link: LLink | null,
skip_border: boolean, skip_border: boolean,
flow: number | boolean | null, flow: number | boolean | null,
@@ -344,8 +349,8 @@ export class LitegraphLinkAdapter {
context: LinkRenderContext, context: LinkRenderContext,
extras: { extras: {
reroute?: Reroute reroute?: Reroute
startControl?: ReadOnlyPoint startControl?: LitegraphPoint
endControl?: ReadOnlyPoint endControl?: LitegraphPoint
num_sublines?: number num_sublines?: number
disabled?: boolean disabled?: boolean
} = {} } = {}
@@ -406,13 +411,19 @@ export class LitegraphLinkAdapter {
y: a[1] + (extras.startControl![1] || 0) y: a[1] + (extras.startControl![1] || 0)
} }
const end = { x: b[0], y: b[1] } const end = { x: b[0], y: b[1] }
this.applySplineOffset(end, endDir, dist * factor) const endArray: LitegraphPoint = [end.x, end.y]
this.applySplineOffset(endArray, endDir, dist * factor)
end.x = endArray[0]
end.y = endArray[1]
cps.push(start, end) cps.push(start, end)
linkData.controlPoints = cps linkData.controlPoints = cps
} else if (!hasStartCtrl && hasEndCtrl) { } else if (!hasStartCtrl && hasEndCtrl) {
// End provided, derive start via direction offset (CENTER => no offset) // End provided, derive start via direction offset (CENTER => no offset)
const start = { x: a[0], y: a[1] } const start = { x: a[0], y: a[1] }
this.applySplineOffset(start, startDir, dist * factor) const startArray: LitegraphPoint = [start.x, start.y]
this.applySplineOffset(startArray, startDir, dist * factor)
start.x = startArray[0]
start.y = startArray[1]
const end = { const end = {
x: b[0] + (extras.endControl![0] || 0), x: b[0] + (extras.endControl![0] || 0),
y: b[1] + (extras.endControl![1] || 0) y: b[1] + (extras.endControl![1] || 0)
@@ -423,8 +434,14 @@ export class LitegraphLinkAdapter {
// Neither provided: derive both from directions (CENTER => no offset) // Neither provided: derive both from directions (CENTER => no offset)
const start = { x: a[0], y: a[1] } const start = { x: a[0], y: a[1] }
const end = { x: b[0], y: b[1] } const end = { x: b[0], y: b[1] }
this.applySplineOffset(start, startDir, dist * factor) const startArray: LitegraphPoint = [start.x, start.y]
this.applySplineOffset(end, endDir, dist * factor) const endArray: LitegraphPoint = [end.x, end.y]
this.applySplineOffset(startArray, startDir, dist * factor)
this.applySplineOffset(endArray, endDir, dist * factor)
start.x = startArray[0]
start.y = startArray[1]
end.x = endArray[0]
end.y = endArray[1]
cps.push(start, end) cps.push(start, end)
linkData.controlPoints = cps linkData.controlPoints = cps
} }
@@ -463,8 +480,8 @@ export class LitegraphLinkAdapter {
if (this.enableLayoutStoreWrites && link && link.id !== -1) { if (this.enableLayoutStoreWrites && link && link.id !== -1) {
// Calculate bounds and center only when writing // Calculate bounds and center only when writing
const bounds = this.calculateLinkBounds( const bounds = this.calculateLinkBounds(
[linkData.startPoint.x, linkData.startPoint.y] as ReadOnlyPoint, [linkData.startPoint.x, linkData.startPoint.y] as LitegraphPoint,
[linkData.endPoint.x, linkData.endPoint.y] as ReadOnlyPoint, [linkData.endPoint.x, linkData.endPoint.y] as LitegraphPoint,
linkData linkData
) )
const centerPos = linkData.centerPos || { const centerPos = linkData.centerPos || {
@@ -497,33 +514,57 @@ export class LitegraphLinkAdapter {
} }
} }
/**
* Render a link being dragged from a slot to mouse position
* Used during link creation/reconnection
*/
renderDraggingLink( renderDraggingLink(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
from: ReadOnlyPoint, fromNode: LGraphNode | null,
to: ReadOnlyPoint, fromSlot: INodeOutputSlot | INodeInputSlot,
colour: CanvasColour, fromSlotIndex: number,
startDir: LinkDirection, toPosition: LitegraphPoint,
endDir: LinkDirection, context: LinkRenderContext,
context: LinkRenderContext options: {
fromInput?: boolean
color?: CanvasColour
disabled?: boolean
} = {}
): void { ): void {
this.renderLinkDirect( if (!fromNode) return
ctx,
from, // Get slot position using layout tree if available
to, const slotPos = getSlotPosition(
null, fromNode,
false, fromSlotIndex,
null, options.fromInput || false
colour,
startDir,
endDir,
{
...context,
linkMarkerShape: LinkMarkerShape.None
},
{
disabled: false
}
) )
if (!slotPos) return
// Get slot direction
const slotDir =
fromSlot.dir ||
(options.fromInput ? LinkDirection.LEFT : LinkDirection.RIGHT)
// Create drag data
const dragData: DragLinkData = {
fixedPoint: { x: slotPos[0], y: slotPos[1] },
fixedDirection: this.convertDirection(slotDir),
dragPoint: { x: toPosition[0], y: toPosition[1] },
color: options.color ? String(options.color) : undefined,
type: fromSlot.type !== undefined ? String(fromSlot.type) : undefined,
disabled: options.disabled || false,
fromInput: options.fromInput || false
}
// Convert context
const pathContext = this.convertToPathRenderContext(context)
// Hide center marker when dragging links
pathContext.style.showCenterMarker = false
// Render using pure renderer
this.pathRenderer.drawDraggingLink(ctx, dragData, pathContext)
} }
/** /**
@@ -531,8 +572,8 @@ export class LitegraphLinkAdapter {
* Includes padding for line width and control points * Includes padding for line width and control points
*/ */
private calculateLinkBounds( private calculateLinkBounds(
startPos: ReadOnlyPoint, startPos: LitegraphPoint,
endPos: ReadOnlyPoint, endPos: LitegraphPoint,
linkData: LinkRenderData linkData: LinkRenderData
): Bounds { ): Bounds {
let minX = Math.min(startPos[0], endPos[0]) let minX = Math.min(startPos[0], endPos[0])

View File

@@ -12,7 +12,7 @@ import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { LLink } from '@/lib/litegraph/src/LLink' import { LLink } from '@/lib/litegraph/src/LLink'
import { Reroute } from '@/lib/litegraph/src/Reroute' import { Reroute } from '@/lib/litegraph/src/Reroute'
import type { ReadOnlyPoint } from '@/lib/litegraph/src/interfaces' import type { Point as LitegraphPoint } from '@/lib/litegraph/src/interfaces'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
@@ -125,7 +125,7 @@ export function useLinkLayoutSync() {
// Special handling for floating input chain // Special handling for floating input chain
const isFloatingInputChain = !sourceNode && targetNode const isFloatingInputChain = !sourceNode && targetNode
const startControl: ReadOnlyPoint = isFloatingInputChain const startControl: LitegraphPoint = isFloatingInputChain
? [0, 0] ? [0, 0]
: [dist * reroute.cos, dist * reroute.sin] : [dist * reroute.cos, dist * reroute.sin]
@@ -161,7 +161,7 @@ export function useLinkLayoutSync() {
(endPos[1] - lastReroute.pos[1]) ** 2 (endPos[1] - lastReroute.pos[1]) ** 2
) )
const finalDist = Math.min(Reroute.maxSplineOffset, finalDistance * 0.25) const finalDist = Math.min(Reroute.maxSplineOffset, finalDistance * 0.25)
const finalStartControl: ReadOnlyPoint = [ const finalStartControl: LitegraphPoint = [
finalDist * lastReroute.cos, finalDist * lastReroute.cos,
finalDist * lastReroute.sin finalDist * lastReroute.sin
] ]

View File

@@ -1,4 +1,4 @@
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces' import type { Rect } from '@/lib/litegraph/src/interfaces'
import type { Bounds } from '@/renderer/core/layout/types' import type { Bounds } from '@/renderer/core/layout/types'
/** /**
@@ -33,9 +33,7 @@ export const lcm = (a: number, b: number): number => {
* @param rectangles - Array of rectangle tuples in [x, y, width, height] format * @param rectangles - Array of rectangle tuples in [x, y, width, height] format
* @returns Bounds object with union rectangle, or null if no rectangles provided * @returns Bounds object with union rectangle, or null if no rectangles provided
*/ */
export function computeUnionBounds( export function computeUnionBounds(rectangles: readonly Rect[]): Bounds | null {
rectangles: readonly ReadOnlyRect[]
): Bounds | null {
const n = rectangles.length const n = rectangles.length
if (n === 0) { if (n === 0) {
return null return null

View File

@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, vi } from 'vitest' import { afterEach, beforeEach, describe, expect, vi } from 'vitest'
import type { INodeInputSlot, Point } from '@/lib/litegraph/src/litegraph' import type { INodeInputSlot, Point } from '@/lib/litegraph/src/litegraph'
import { Rectangle } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LGraph } from '@/lib/litegraph/src/litegraph' import { LGraph } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/litegraph' import { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
@@ -571,7 +572,7 @@ describe('LGraphNode', () => {
name: 'test_in', name: 'test_in',
type: 'string', type: 'string',
link: null, link: null,
boundingRect: new Float32Array([0, 0, 0, 0]) boundingRect: new Rectangle(0, 0, 0, 0)
} }
}) })
test('should return position based on title height when collapsed', () => { test('should return position based on title height when collapsed', () => {
@@ -594,7 +595,7 @@ describe('LGraphNode', () => {
name: 'test_in_2', name: 'test_in_2',
type: 'number', type: 'number',
link: null, link: null,
boundingRect: new Float32Array([0, 0, 0, 0]) boundingRect: new Rectangle(0, 0, 0, 0)
} }
node.inputs = [inputSlot, inputSlot2] node.inputs = [inputSlot, inputSlot2]
const slotIndex = 0 const slotIndex = 0

View File

@@ -1,6 +1,7 @@
// TODO: Fix these tests after migration // TODO: Fix these tests after migration
import { test as baseTest } from 'vitest' import { test as baseTest } from 'vitest'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { Point, Rect } from '@/lib/litegraph/src/interfaces' import type { Point, Rect } from '@/lib/litegraph/src/interfaces'
import { import {
addDirectionalOffset, addDirectionalOffset,
@@ -132,8 +133,8 @@ test('snapPoint correctly snaps points to grid', ({ expect }) => {
test('createBounds correctly creates bounding box', ({ expect }) => { test('createBounds correctly creates bounding box', ({ expect }) => {
const objects = [ const objects = [
{ boundingRect: [0, 0, 10, 10] as Rect }, { boundingRect: new Rectangle(0, 0, 10, 10) },
{ boundingRect: [5, 5, 10, 10] as Rect } { boundingRect: new Rectangle(5, 5, 10, 10) }
] ]
const defaultBounds = createBounds(objects) const defaultBounds = createBounds(objects)

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces' import type { Rect } from '@/lib/litegraph/src/interfaces'
import { computeUnionBounds, gcd, lcm } from '@/utils/mathUtil' import { computeUnionBounds, gcd, lcm } from '@/utils/mathUtil'
describe('mathUtil', () => { describe('mathUtil', () => {
@@ -27,9 +27,9 @@ describe('mathUtil', () => {
expect(computeUnionBounds([])).toBe(null) expect(computeUnionBounds([])).toBe(null)
}) })
// Tests for tuple format (ReadOnlyRect) // Tests for tuple format (Rect)
it('should work with ReadOnlyRect tuple format', () => { it('should work with Rect tuple format', () => {
const tuples: ReadOnlyRect[] = [ const tuples: Rect[] = [
[10, 20, 30, 40] as const, // bounds: 10,20 to 40,60 [10, 20, 30, 40] as const, // bounds: 10,20 to 40,60
[50, 10, 20, 30] as const // bounds: 50,10 to 70,40 [50, 10, 20, 30] as const // bounds: 50,10 to 70,40
] ]
@@ -44,8 +44,8 @@ describe('mathUtil', () => {
}) })
}) })
it('should handle single ReadOnlyRect tuple', () => { it('should handle single Rect tuple', () => {
const tuple: ReadOnlyRect = [10, 20, 30, 40] as const const tuple: Rect = [10, 20, 30, 40] as const
const result = computeUnionBounds([tuple]) const result = computeUnionBounds([tuple])
expect(result).toEqual({ expect(result).toEqual({
@@ -57,7 +57,7 @@ describe('mathUtil', () => {
}) })
it('should handle tuple format with negative dimensions', () => { it('should handle tuple format with negative dimensions', () => {
const tuples: ReadOnlyRect[] = [ const tuples: Rect[] = [
[100, 50, -20, -10] as const, // x+width=80, y+height=40 [100, 50, -20, -10] as const, // x+width=80, y+height=40
[90, 45, 15, 20] as const // x+width=105, y+height=65 [90, 45, 15, 20] as const // x+width=105, y+height=65
] ]
@@ -74,7 +74,7 @@ describe('mathUtil', () => {
it('should maintain optimal performance with SoA tuples', () => { it('should maintain optimal performance with SoA tuples', () => {
// Test that array access is as expected for typical selection sizes // Test that array access is as expected for typical selection sizes
const tuples: ReadOnlyRect[] = Array.from( const tuples: Rect[] = Array.from(
{ length: 10 }, { length: 10 },
(_, i) => (_, i) =>
[ [