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

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