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
This commit is contained in:
filtered
2024-11-04 03:12:21 +11:00
committed by GitHub
parent d9efeb819d
commit e661decddc
7 changed files with 238 additions and 159 deletions

View File

@@ -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<NodeId, LGraphNode>
_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))
}
/**

View File

@@ -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(

View File

@@ -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<string, unknown> {
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<Positionable> = 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<Positionable>, 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[] {

View File

@@ -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)
}
/**

View File

@@ -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<T> = {
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<Positionable>
/**
* 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

View File

@@ -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)
}
/**

View File

@@ -59,6 +59,7 @@ export type ISerialisedGraph<
/** Serialised LGraphGroup */
export interface ISerialisedGroup {
id: number
title: string
bounding: number[]
color: string