From e661decddc1752408c65db619c004f5a33507da5 Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Mon, 4 Nov 2024 03:12:21 +1100 Subject: [PATCH] Positionable: Common interface for canvas items (#256) * Add Positionable interface to canvas elements * Add group resizeTo children Refactor out duplicated code from Node * Remove redundant "is_selected" check * Improve measure pass - interface, caching Node bounds once per render Cached results * Use cached bounds for repeat canvas calls - Removes margin param from getNodeOnPos - Removes margin param from getGroupOnPos - Hitboxes now uniform for render / mouse features - Simplifies code * nit - Refactor * Fix top-left edge of hitbox missing * Add ID to groups --- src/LGraph.ts | 27 +++---- src/LGraphCanvas.ts | 74 ++++++++----------- src/LGraphGroup.ts | 102 +++++++++++++------------- src/LGraphNode.ts | 142 ++++++++++++++++++++++++------------- src/interfaces.ts | 31 +++++++- src/measure.ts | 20 +++++- src/types/serialisation.ts | 1 + 7 files changed, 238 insertions(+), 159 deletions(-) diff --git a/src/LGraph.ts b/src/LGraph.ts index 3a7d8f6c2..0c6dd90af 100644 --- a/src/LGraph.ts +++ b/src/LGraph.ts @@ -55,6 +55,7 @@ export class LGraph { last_link_id: number /** The largest ID created by this graph */ last_reroute_id: number + lastGroupId: number = -1 _nodes: LGraphNode[] _nodes_by_id: Record _nodes_in_order: LGraphNode[] @@ -647,6 +648,10 @@ export class LGraph { // LEGACY: This was changed from constructor === LGraphGroup //groups if (node instanceof LGraphGroup) { + // Assign group ID + if (node.id == null || node.id === -1) node.id = ++this.lastGroupId + if (node.id > this.lastGroupId) this.lastGroupId = node.id + this._groups.push(node) this.setDirtyCanvas(true) this.change() @@ -847,19 +852,17 @@ export class LGraph { * Returns the top-most node in this position of the canvas * @param {number} x the x coordinate in canvas space * @param {number} y the y coordinate in canvas space - * @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph + * @param {Array} nodeList a list with all the nodes to search from, by default is all the nodes in the graph * @return {LGraphNode} the node at this position or null */ - getNodeOnPos(x: number, y: number, nodes_list?: LGraphNode[], margin?: number): LGraphNode | null { - nodes_list = nodes_list || this._nodes - const nRet = null - for (let i = nodes_list.length - 1; i >= 0; i--) { - const n = nodes_list[i] - const skip_title = n.constructor.title_mode == TitleMode.NO_TITLE - if (n.isPointInside(x, y, margin, skip_title)) - return n + getNodeOnPos(x: number, y: number, nodeList?: LGraphNode[]): LGraphNode | null { + const nodes = nodeList || this._nodes + let i = nodes.length + while (--i >= 0) { + const node = nodes[i] + if (node.isPointInside(x, y)) return node } - return nRet + return null } /** * Returns the top-most group in that position @@ -867,8 +870,8 @@ export class LGraph { * @param y The y coordinate in canvas space * @return The group or null */ - getGroupOnPos(x: number, y: number, { margin = 2 } = {}): LGraphGroup | undefined { - return this._groups.toReversed().find(g => g.isPointInside(x, y, margin, true)) + getGroupOnPos(x: number, y: number): LGraphGroup | undefined { + return this._groups.toReversed().find(g => g.isPointInside(x, y)) } /** diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 4efc45ff9..fa2e6e40b 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -1719,7 +1719,7 @@ export class LGraphCanvas { if (!is_inside) return - let node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes, 5) + let node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes) let skip_action = false const now = LiteGraph.getTime() const is_primary = (e.isPrimary === undefined || !e.isPrimary) @@ -2002,7 +2002,7 @@ export class LGraphCanvas { this.isDragging = true } // Account for shift + click + drag - if (!(e.shiftKey && !e.ctrlKey && !e.altKey) || !node.is_selected) { + if (!(e.shiftKey && !e.ctrlKey && !e.altKey) || !node.selected) { this.processNodeSelected(node, e) } } else { // double-click @@ -2010,7 +2010,7 @@ export class LGraphCanvas { * Don't call the function if the block is already selected. * Otherwise, it could cause the block to be unselected while its panel is open. */ - if (!node.is_selected) this.processNodeSelected(node, e) + if (!node.selected) this.processNodeSelected(node, e) } this.dirty_canvas = true @@ -2451,12 +2451,6 @@ export class LGraphCanvas { nodes.add(n) n.pos[0] += delta[0] / this.ds.scale n.pos[1] += delta[1] / this.ds.scale - /* - * Don't call the function if the block is already selected. - * Otherwise, it could cause the block to be unselected while dragging. - */ - if (!n.is_selected) this.processNodeSelected(n, e) - } if (this.selectedGroups) { @@ -3248,15 +3242,15 @@ export class LGraphCanvas { if (typeof nodes == "string") nodes = [nodes] for (const i in nodes) { const node: LGraphNode = nodes[i] - if (node.is_selected) { + if (node.selected) { this.deselectNode(node) continue } - if (!node.is_selected) { + if (!node.selected) { node.onSelected?.() } - node.is_selected = true + node.selected = true this.selected_nodes[node.id] = node if (node.inputs) { @@ -3284,9 +3278,9 @@ export class LGraphCanvas { * removes a node from the current selection **/ deselectNode(node: LGraphNode): void { - if (!node.is_selected) return + if (!node.selected) return node.onDeselected?.() - node.is_selected = false + node.selected = false delete this.selected_nodes[node.id] this.onNodeDeselected?.(node) @@ -3316,11 +3310,11 @@ export class LGraphCanvas { const nodes = this.graph._nodes for (let i = 0, l = nodes.length; i < l; ++i) { const node = nodes[i] - if (!node.is_selected) { + if (!node.selected) { continue } node.onDeselected?.() - node.is_selected = false + node.selected = false this.onNodeDeselected?.(node) } this.selected_nodes = {} @@ -3474,16 +3468,17 @@ export class LGraphCanvas { computeVisibleNodes(nodes?: LGraphNode[], out?: LGraphNode[]): LGraphNode[] { const visible_nodes = out || [] visible_nodes.length = 0 - nodes ||= this.graph._nodes - for (let i = 0, l = nodes.length; i < l; ++i) { - const n = nodes[i] + const _nodes = nodes || this.graph._nodes + for (const node of _nodes) { //skip rendering nodes in live mode - if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) continue - // Not in visible area - if (!overlapBounding(this.visible_area, n.getBounding(LGraphCanvas.#temp, true))) continue + if (this.live_mode && !node.onDrawBackground && !node.onDrawForeground) continue - visible_nodes.push(n) + node.updateArea() + // Not in visible area + if (!overlapBounding(this.visible_area, node.renderArea)) continue + + visible_nodes.push(node) } return visible_nodes } @@ -3499,25 +3494,24 @@ export class LGraphCanvas { this.render_time = (now - this.last_draw_time) * 0.001 this.last_draw_time = now - if (this.graph) { - this.ds.computeVisibleArea(this.viewport) - } + if (this.graph) this.ds.computeVisibleArea(this.viewport) + + // Compute node size before drawing links. + if (this.dirty_canvas || force_canvas) + this.computeVisibleNodes(null, this.visible_nodes) if (this.dirty_bgcanvas || force_bgcanvas || this.always_render_background || - (this.graph && - this.graph._last_trigger_time && + (this.graph?._last_trigger_time && now - this.graph._last_trigger_time < 1000)) { this.drawBackCanvas() } - if (this.dirty_canvas || force_canvas) { - this.drawFrontCanvas() - } + if (this.dirty_canvas || force_canvas) this.drawFrontCanvas() this.fps = this.render_time ? 1.0 / this.render_time : 0 - this.frame += 1 + this.frame++ } /** * draws the front canvas (the one containing all the nodes) @@ -3581,10 +3575,7 @@ export class LGraphCanvas { this.ds.toCanvasContext(ctx) //draw nodes - const visible_nodes = this.computeVisibleNodes( - null, - this.visible_nodes - ) + const visible_nodes = this.visible_nodes for (let i = 0; i < visible_nodes.length; ++i) { const node = visible_nodes[i] @@ -3784,9 +3775,7 @@ export class LGraphCanvas { const { strokeStyle, lineWidth } = ctx - const area = LGraphCanvas.#tmp_area - node.measure(area) - node.onBounding?.(area) + const area = node.boundingRect const gap = 3 const radius = this.round_radius + gap @@ -4223,8 +4212,7 @@ export class LGraphCanvas { ctx.finish?.() this.dirty_bgcanvas = false - //to force to repaint the front canvas with the bgcanvas - // But why would you actually want to do this? + // Forces repaint of the front canvas. this.dirty_canvas = true } /** @@ -4313,7 +4301,7 @@ export class LGraphCanvas { size, color, bgcolor, - node.is_selected + node.selected ) if (!low_quality) { @@ -4590,7 +4578,7 @@ export class LGraphCanvas { * @param size Size of the background to draw, in graph units. Differs from node size if collapsed, etc. * @param fgcolor Foreground colour - used for text * @param bgcolor Background colour of the node - * @param selected Whether to render the node as selected. Likely to be removed in future, as current usage is simply the is_selected property of the node. + * @param selected Whether to render the node as selected. Likely to be removed in future, as current usage is simply the selected property of the node. * @param mouse_over Deprecated */ drawNodeShape( diff --git a/src/LGraphGroup.ts b/src/LGraphGroup.ts index eec7a0d47..34d65d626 100644 --- a/src/LGraphGroup.ts +++ b/src/LGraphGroup.ts @@ -1,4 +1,4 @@ -import type { IContextMenuValue, Point, Size } from "./interfaces" +import type { IContextMenuValue, Point, Positionable, Size } from "./interfaces" import type { LGraph } from "./LGraph" import type { ISerialisedGroup } from "./types/serialisation" import { LiteGraph } from "./litegraph" @@ -11,7 +11,8 @@ export interface IGraphGroupFlags extends Record { pinned?: true } -export class LGraphGroup { +export class LGraphGroup implements Positionable { + id: number color: string title: string font?: string @@ -20,11 +21,14 @@ export class LGraphGroup { _pos: Point = this._bounding.subarray(0, 2) _size: Size = this._bounding.subarray(2, 4) _nodes: LGraphNode[] = [] + _children: Set = new Set() graph: LGraph | null = null flags: IGraphGroupFlags = {} selected?: boolean - constructor(title?: string) { + constructor(title?: string, id?: number) { + // TODO: Object instantiation pattern requires too much boilerplate and null checking. ID should be passed in via constructor. + this.id = id ?? -1 this.title = title || "Group" this.color = LGraphCanvas.node_colors.pale_blue ? LGraphCanvas.node_colors.pale_blue.groupcolor @@ -53,6 +57,10 @@ export class LGraphGroup { this._size[1] = Math.max(80, v[1]) } + get boundingRect() { + return this._bounding + } + get nodes() { return this._nodes } @@ -74,6 +82,7 @@ export class LGraphGroup { } configure(o: ISerialisedGroup): void { + this.id = o.id this.title = o.title this._bounding.set(o.bounding) this.color = o.color @@ -84,6 +93,7 @@ export class LGraphGroup { serialize(): ISerialisedGroup { const b = this._bounding return { + id: this.id, title: this.title, bounding: [ Math.round(b[0]), @@ -145,36 +155,57 @@ export class LGraphGroup { this._size[1] = height } - move(deltax: number, deltay: number, ignore_nodes = false): void { + move(deltaX: number, deltaY: number, skipChildren: boolean = false): void { if (this.pinned) return - this._pos[0] += deltax - this._pos[1] += deltay - if (ignore_nodes) return + this._pos[0] += deltaX + this._pos[1] += deltaY + if (skipChildren === true) return - for (let i = 0; i < this._nodes.length; ++i) { - const node = this._nodes[i] - node.pos[0] += deltax - node.pos[1] += deltay + for (const item of this._children) { + item.move(deltaX, deltaY) } } recomputeInsideNodes(): void { - this._nodes.length = 0 - const nodes = this.graph._nodes + const { nodes } = this.graph const node_bounding = new Float32Array(4) + this._nodes.length = 0 + this._children.clear() - for (let i = 0; i < nodes.length; ++i) { - const node = nodes[i] + for (const node of nodes) { node.getBounding(node_bounding) - //out of the visible area - if (!overlapBounding(this._bounding, node_bounding)) - continue - - this._nodes.push(node) + // Node overlaps with group + if (overlapBounding(this._bounding, node_bounding)) { + this._nodes.push(node) + this._children.add(node) + } } } + /** + * Resizes and moves the group to neatly fit all given {@link objects}. + * @param objects All objects that should be inside the group + * @param padding Value in graph units to add to all sides of the group. Default: 10 + */ + resizeTo(objects: Iterable, padding: number = 10): void { + const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity]) + + for (const obj of objects) { + const rect = obj.boundingRect + bounds[0] = Math.min(bounds[0], rect[0]) + bounds[1] = Math.min(bounds[1], rect[1]) + bounds[2] = Math.max(bounds[2], rect[0] + rect[2]) + bounds[3] = Math.max(bounds[3], rect[1] + rect[3]) + } + if (!bounds.every(x => isFinite(x))) return + + this.pos[0] = bounds[0] - padding + this.pos[1] = bounds[1] - padding - this.titleHeight + this.size[0] = bounds[2] - bounds[0] + (2 * padding) + this.size[1] = bounds[3] - bounds[1] + (2 * padding) + this.titleHeight + } + /** * Add nodes to the group and adjust the group's position and size accordingly * @param {LGraphNode[]} nodes - The nodes to add to the group @@ -183,36 +214,7 @@ export class LGraphGroup { */ addNodes(nodes: LGraphNode[], padding: number = 10): void { if (!this._nodes && nodes.length === 0) return - - const allNodes = [...(this._nodes || []), ...nodes] - - const bounds = allNodes.reduce((acc, node) => { - const [x, y] = node.pos - const [width, height] = node.size - const isReroute = node.type === "Reroute" - const isCollapsed = node.flags?.collapsed - - const top = y - (isReroute ? 0 : LiteGraph.NODE_TITLE_HEIGHT) - const bottom = isCollapsed ? top + LiteGraph.NODE_TITLE_HEIGHT : y + height - const right = isCollapsed && node._collapsed_width ? x + Math.round(node._collapsed_width) : x + width - - return { - left: Math.min(acc.left, x), - top: Math.min(acc.top, top), - right: Math.max(acc.right, right), - bottom: Math.max(acc.bottom, bottom) - } - }, { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity }) - - this.pos = [ - bounds.left - padding, - bounds.top - padding - this.titleHeight - ] - - this.size = [ - bounds.right - bounds.left + padding * 2, - bounds.bottom - bounds.top + padding * 2 + this.titleHeight - ] + this.resizeTo([...this._nodes, ...nodes], padding) } getMenuOptions(): IContextMenuValue[] { diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index afccfb962..3f9fce761 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -1,4 +1,4 @@ -import type { Dictionary, IContextMenuValue, IFoundSlot, INodeFlags, INodeInputSlot, INodeOutputSlot, IOptionalSlotData, ISlotType, Point, Rect, Size } from "./interfaces" +import type { Dictionary, IContextMenuValue, IFoundSlot, INodeFlags, INodeInputSlot, INodeOutputSlot, IOptionalSlotData, ISlotType, Point, Positionable, ReadOnlyRect, Rect, Size } from "./interfaces" import type { LGraph } from "./LGraph" import type { IWidget, TWidgetValue } from "./types/widgets" import type { ISerialisedNode } from "./types/serialisation" @@ -8,7 +8,7 @@ import type { DragAndScale } from "./DragAndScale" import { LGraphEventMode, NodeSlotType, TitleMode, RenderShape } from "./types/globalEnums" import { BadgePosition, LGraphBadge } from "./LGraphBadge" import { type LGraphNodeConstructor, LiteGraph } from "./litegraph" -import { isInsideRectangle } from "./measure" +import { isInsideRectangle, isXyInRectangle } from "./measure" import { LLink } from "./LLink" export type NodeId = number | string @@ -111,7 +111,7 @@ export interface LGraphNode { * Base Class for all the node type classes * @param {String} name a name for the node */ -export class LGraphNode { +export class LGraphNode implements Positionable { // Static properties used by dynamic child classes static title?: string static MAX_CONSOLE?: number @@ -133,8 +133,6 @@ export class LGraphNode { properties_info: INodePropertyInfo[] = [] flags: INodeFlags = {} widgets?: IWidget[] - - size: Size locked?: boolean // Execution order, automatically computed during run @@ -158,6 +156,7 @@ export class LGraphNode { onOutputRemoved?(this: LGraphNode, slot: number): void onInputRemoved?(this: LGraphNode, slot: number, input: INodeInputSlot): void _collapsed_width: number + /** Called once at the start of every frame. Caller may change the values in {@link out}, which will be reflected in {@link boundingRect}. */ onBounding?(this: LGraphNode, out: Rect): void horizontal?: boolean console?: string[] @@ -166,7 +165,6 @@ export class LGraphNode { subgraph?: LGraph skip_subgraph_button?: boolean mouseOver?: IMouseOverData - is_selected?: boolean redraw_on_mouse?: boolean // Appears unused optional_inputs? @@ -180,9 +178,36 @@ export class LGraphNode { has_errors?: boolean removable?: boolean block_delete?: boolean + selected?: boolean showAdvanced?: boolean - _pos: Point = new Float32Array([10, 10]) + /** @inheritdoc {@link renderArea} */ + #renderArea: Float32Array = new Float32Array(4) + /** + * Rect describing the node area, including shadows and any protrusions. + * Determines if the node is visible. Calculated once at the start of every frame. + */ + get renderArea(): ReadOnlyRect { + return this.#renderArea + } + + + /** @inheritdoc {@link boundingRect} */ + #boundingRect: Float32Array = new Float32Array(4) + /** + * Cached node position & area as `x, y, width, height`. Includes changes made by {@link onBounding}, if present. + * + * Determines the node hitbox and other rendering effects. Calculated once at the start of every frame. + */ + get boundingRect(): ReadOnlyRect { + return this.#boundingRect + } + + /** {@link pos} and {@link size} values are backed by this {@link Rect}. */ + _posSize: Float32Array = new Float32Array(4) + _pos: Point = this._posSize.subarray(0, 2) + _size: Size = this._posSize.subarray(2, 4) + public get pos() { return this._pos } @@ -193,6 +218,16 @@ export class LGraphNode { this._pos[1] = value[1] } + public get size() { + return this._size + } + public set size(value) { + if (!value || value.length < 2) return + + this._size[0] = value[0] + this._size[1] = value[1] + } + get shape(): RenderShape { return this._shape } @@ -218,6 +253,13 @@ export class LGraphNode { } } + public get is_selected(): boolean { + return this.selected + } + public set is_selected(value: boolean) { + this.selected = value + } + // Used in group node setInnerNodes?(this: LGraphNode, nodes: LGraphNode[]): void @@ -291,6 +333,7 @@ export class LGraphNode { this.id = LiteGraph.use_uuids ? LiteGraph.uuidv4() : -1 this.title = title || "Unnamed" this.size = [LiteGraph.NODE_WIDTH, 60] + this.pos = [10, 10] } /** @@ -1393,9 +1436,17 @@ export class LGraphNode { return custom_widget } + move(deltaX: number, deltaY: number): void { + this.pos[0] += deltaX + this.pos[1] += deltaY + } + /** - * Measures the node for rendering, populating {@link out} with the results in graph space. - * @param out Results (x, y, width, height) are inserted into this array. + * Internal method to measure the node for rendering. Prefer {@link boundingRect} where possible. + * + * Populates {@link out} with the results in graph space. + * Adjusts for title and collapsed status, but does not call {@link onBounding}. + * @param out `x, y, width, height` are written to this array. * @param pad Expands the area by this amount on each side. Default: 0 */ measure(out: Rect, pad = 0): void { @@ -1417,58 +1468,49 @@ export class LGraphNode { /** * returns the bounding of the object, used for rendering purposes * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage - * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation + * @param includeExternal {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] */ - getBounding(out?: Float32Array, compute_outer?: boolean): Float32Array { - out = out || new Float32Array(4) - this.measure(out) - if (compute_outer) { - // 4 offset for collapsed node connection points - out[0] -= 4 - out[1] -= 4 - // Add shadow & left offset - out[2] += 6 + 4 - // Add shadow & top offsets - out[3] += 5 + 4 - } - this.onBounding?.(out) + getBounding(out?: Rect, includeExternal?: boolean): Rect { + out ||= new Float32Array(4) + + const rect = includeExternal ? this.renderArea : this.boundingRect + out[0] = rect[0] + out[1] = rect[1] + out[2] = rect[2] + out[3] = rect[3] + return out } + /** + * Calculates the render area of this node, populating both {@link boundingRect} and {@link renderArea}. + * Called automatically at the start of every frame. + */ + updateArea(): void { + const bounds = this.#boundingRect + this.measure(bounds) + this.onBounding?.(bounds) + + const renderArea = this.#renderArea + renderArea.set(bounds) + // 4 offset for collapsed node connection points + renderArea[0] -= 4 + renderArea[1] -= 4 + // Add shadow & left offset + renderArea[2] += 6 + 4 + // Add shadow & top offsets + renderArea[3] += 5 + 4 + } + /** * checks if a point is inside the shape of a node * @param {number} x * @param {number} y * @return {boolean} */ - isPointInside(x: number, y: number, margin?: number, skip_title?: boolean): boolean { - margin ||= 0 - - const margin_top = skip_title || this.graph?.isLive() - ? 0 - : LiteGraph.NODE_TITLE_HEIGHT - - if (this.flags.collapsed) { - //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS) - if (isInsideRectangle( - x, - y, - this.pos[0] - margin, - this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, - (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + - 2 * margin, - LiteGraph.NODE_TITLE_HEIGHT + 2 * margin - )) { - return true - } - } else if (this.pos[0] - 4 - margin < x && - this.pos[0] + this.size[0] + 4 + margin > x && - this.pos[1] - margin_top - margin < y && - this.pos[1] + this.size[1] + margin > y) { - return true - } - return false + isPointInside(x: number, y: number): boolean { + return isXyInRectangle(x, y, this.boundingRect) } /** diff --git a/src/interfaces.ts b/src/interfaces.ts index 68036b500..4561fc0d9 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,5 +1,5 @@ import type { ContextMenu } from "./ContextMenu" -import type { LGraphNode } from "./LGraphNode" +import type { LGraphNode, NodeId } from "./LGraphNode" import type { LinkDirection, RenderShape } from "./types/globalEnums" import type { LinkId } from "./LLink" @@ -12,6 +12,35 @@ export type NullableProperties = { export type CanvasColour = string | CanvasGradient | CanvasPattern +export interface Positionable { + id: NodeId | number + /** Position in graph coordinates. Default: 0,0 */ + pos: Point + /** true if this object is part of the selection, otherwise false. */ + selected?: boolean + + readonly children?: ReadonlySet + + /** + * Adds a delta to the current position. + * @param deltaX X value to add to current position + * @param deltaY Y value to add to current position + * @param skipChildren If true, any child objects like group contents will not be moved + */ + move(deltaX: number, deltaY: number, skipChildren?: boolean): void + + /** + * Cached position & size as `x, y, width, height`. + * @readonly See {@link move} + */ + readonly boundingRect: ReadOnlyRect + + /** Called whenever the item is selected */ + onSelected?(): void + /** Called whenever the item is deselected */ + onDeselected?(): void +} + export interface IInputOrOutput { // If an input, this will be defined input?: INodeInputSlot diff --git a/src/measure.ts b/src/measure.ts index d02fd6964..53c6d8545 100644 --- a/src/measure.ts +++ b/src/measure.ts @@ -31,12 +31,26 @@ export function dist2(a: ReadOnlyPoint, b: ReadOnlyPoint): number { * @returns `true` if the point is inside the rect, otherwise `false` */ export function isPointInRectangle(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean { - return rect[0] < point[0] + return rect[0] <= point[0] && rect[0] + rect[2] > point[0] - && rect[1] < point[1] + && rect[1] <= point[1] && rect[1] + rect[3] > point[1] } +/** + * Determines whether a point is inside a rectangle. + * @param x X co-ordinate of the point to check + * @param y Y co-ordinate of the point to check + * @param rect The rectangle, as `x, y, width, height` + * @returns `true` if the point is inside the rect, otherwise `false` + */ +export function isXyInRectangle(x: number, y: number, rect: ReadOnlyRect): boolean { + return rect[0] <= x + && rect[0] + rect[2] > x + && rect[1] <= y + && rect[1] + rect[3] > y +} + /** * Determines whether a point is inside a rectangle. * @param x Point x @@ -95,7 +109,7 @@ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean { export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean { const centreX = b[0] + (b[2] * 0.5) const centreY = b[1] + (b[3] * 0.5) - return isInsideRectangle(centreX, centreY, a[0], a[1], a[2], a[3]) + return isXyInRectangle(centreX, centreY, a) } /** diff --git a/src/types/serialisation.ts b/src/types/serialisation.ts index 24ae6b885..fbb96cbd2 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -59,6 +59,7 @@ export type ISerialisedGraph< /** Serialised LGraphGroup */ export interface ISerialisedGroup { + id: number title: string bounding: number[] color: string