mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
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:
@@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,6 +59,7 @@ export type ISerialisedGraph<
|
||||
|
||||
/** Serialised LGraphGroup */
|
||||
export interface ISerialisedGroup {
|
||||
id: number
|
||||
title: string
|
||||
bounding: number[]
|
||||
color: string
|
||||
|
||||
Reference in New Issue
Block a user