mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 00:20:07 +00:00
Add multi select & group nesting (#262)
* Add multi-select all canvas items (groups, nodes) * Add Feat: Group & Node Multi-Select / Nesting - Groups can now contain groups - Nested groups re-order on top of parent groups - Groups can be added / removed from selection - Uses new Positionable interface - easily extensible to new types * Enhance add / remove from selection UX More in line with normal desktop UX. Structured for keys to be customisable (if impl. later). * Fix regression in link highlight Legacy selection code still in use * Allow nested groups to align perfectly on edges * Remove group-move position rounding Did not work under all circumstances, and resulted in misalignment more often than it helped.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { CanvasColour, Dictionary, Direction, IBoundaryNodes, IContextMenuOptions, INodeSlot, INodeInputSlot, INodeOutputSlot, IOptionalSlotData, Point, Rect, Rect32, Size, IContextMenuValue, ISlotType, ConnectingLink, NullableProperties } from "./interfaces"
|
||||
import type { CanvasColour, Dictionary, Direction, IBoundaryNodes, IContextMenuOptions, INodeSlot, INodeInputSlot, INodeOutputSlot, IOptionalSlotData, Point, Rect, Rect32, Size, IContextMenuValue, ISlotType, ConnectingLink, NullableProperties, Positionable } from "./interfaces"
|
||||
import type { IWidget, TWidgetValue } from "./types/widgets"
|
||||
import type { LGraphNode, NodeId } from "./LGraphNode"
|
||||
import { LGraphNode, type NodeId } from "./LGraphNode"
|
||||
import type { CanvasDragEvent, CanvasMouseEvent, CanvasWheelEvent, CanvasEventDetail, CanvasPointerEvent } from "./types/events"
|
||||
import type { IClipboardContents } from "./types/serialisation"
|
||||
import type { LLink } from "./LLink"
|
||||
@@ -8,7 +8,7 @@ import type { LGraph } from "./LGraph"
|
||||
import type { ContextMenu } from "./ContextMenu"
|
||||
import { EaseFunction, LGraphEventMode, LinkDirection, LinkRenderType, RenderShape, TitleMode } from "./types/globalEnums"
|
||||
import { LGraphGroup } from "./LGraphGroup"
|
||||
import { isInsideRectangle, distance, overlapBounding, isPointInRectangle } from "./measure"
|
||||
import { isInsideRectangle, distance, overlapBounding, isPointInRectangle, containsRect } from "./measure"
|
||||
import { drawSlot, LabelPosition } from "./draw"
|
||||
import { DragAndScale } from "./DragAndScale"
|
||||
import { LinkReleaseContextExtended, LiteGraph, clamp } from "./litegraph"
|
||||
@@ -239,6 +239,7 @@ export class LGraphCanvas {
|
||||
onDrawForeground?: (arg0: CanvasRenderingContext2D, arg1: any) => void
|
||||
connections_width: number
|
||||
round_radius: number
|
||||
/** The current node being drawn by {@link drawNode}. This should NOT be used to determine the currently selected node. See {@link selectedItems} */
|
||||
current_node: LGraphNode | null
|
||||
/** used for widgets */
|
||||
node_widget?: [LGraphNode, IWidget] | null
|
||||
@@ -255,9 +256,10 @@ export class LGraphCanvas {
|
||||
last_draw_time = 0
|
||||
render_time = 0
|
||||
fps = 0
|
||||
/** @deprecated See {@link LGraphCanvas.selectedItems} */
|
||||
selected_nodes: Dictionary<LGraphNode> = {}
|
||||
/** @deprecated Temporary implementation only - will be replaced with `selectedItems: Set<Positionable>`. */
|
||||
selectedGroups: Set<LGraphGroup> = new Set()
|
||||
/** All selected nodes, groups, and reroutes */
|
||||
selectedItems: Set<Positionable> = new Set()
|
||||
selected_group: LGraphGroup | null = null
|
||||
visible_nodes: LGraphNode[] = []
|
||||
node_dragged?: LGraphNode
|
||||
@@ -1198,7 +1200,7 @@ export class LGraphCanvas {
|
||||
// @ts-expect-error Doesn't exist anywhere...
|
||||
subgraph_node.buildFromNodes(nodes_list)
|
||||
|
||||
canvas.deselectAllNodes()
|
||||
canvas.deselectAll()
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
static onMenuNodeClone(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void {
|
||||
@@ -1206,9 +1208,9 @@ export class LGraphCanvas {
|
||||
const graph = node.graph
|
||||
graph.beforeChange()
|
||||
|
||||
const newSelected: Dictionary<LGraphNode> = {}
|
||||
const newSelected = new Set<LGraphNode>()
|
||||
|
||||
const fApplyMultiNode = function (node) {
|
||||
const fApplyMultiNode = function (node: LGraphNode, newNodes: Set<LGraphNode>): void {
|
||||
if (node.clonable === false) return
|
||||
|
||||
const newnode = node.clone()
|
||||
@@ -1216,20 +1218,20 @@ export class LGraphCanvas {
|
||||
|
||||
newnode.pos = [node.pos[0] + 5, node.pos[1] + 5]
|
||||
node.graph.add(newnode)
|
||||
newSelected[newnode.id] = newnode
|
||||
newNodes.add(newnode)
|
||||
}
|
||||
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) {
|
||||
fApplyMultiNode(node)
|
||||
fApplyMultiNode(node, newSelected)
|
||||
} else {
|
||||
for (const i in canvas.selected_nodes) {
|
||||
fApplyMultiNode(canvas.selected_nodes[i])
|
||||
fApplyMultiNode(canvas.selected_nodes[i], newSelected)
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(newSelected).length) {
|
||||
canvas.selectNodes(newSelected)
|
||||
if (newSelected.size) {
|
||||
canvas.selectNodes([...newSelected])
|
||||
}
|
||||
|
||||
graph.afterChange()
|
||||
@@ -1251,8 +1253,6 @@ export class LGraphCanvas {
|
||||
this.dragging_rectangle = null
|
||||
|
||||
this.selected_nodes = {}
|
||||
/** All selected groups */
|
||||
this.selectedGroups = null
|
||||
/** The group currently being resized */
|
||||
this.selected_group = null
|
||||
|
||||
@@ -1767,9 +1767,7 @@ export class LGraphCanvas {
|
||||
this.node_dragged = node
|
||||
this.isDragging = true
|
||||
}
|
||||
if (!this.selected_nodes[node.id]) {
|
||||
this.processNodeSelected(node, e)
|
||||
}
|
||||
this.processSelect(node, e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1962,7 +1960,7 @@ export class LGraphCanvas {
|
||||
}
|
||||
|
||||
//double clicking
|
||||
if (this.allow_interaction && is_double_click && this.selected_nodes[node.id]) {
|
||||
if (this.allow_interaction && is_double_click && this.selectedItems.has(node)) {
|
||||
// Check if it's a double click on the title bar
|
||||
// Note: pos[1] is the y-coordinate of the node's body
|
||||
// If clicking on node header (title), pos[1] is negative
|
||||
@@ -2003,14 +2001,10 @@ export class LGraphCanvas {
|
||||
}
|
||||
// Account for shift + click + drag
|
||||
if (!(e.shiftKey && !e.ctrlKey && !e.altKey) || !node.selected) {
|
||||
this.processNodeSelected(node, e)
|
||||
this.processSelect(node, e)
|
||||
}
|
||||
} else { // double-click
|
||||
/**
|
||||
* 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.selected) this.processNodeSelected(node, e)
|
||||
} else if (!node.selected) {
|
||||
this.processSelect(node, e)
|
||||
}
|
||||
|
||||
this.dirty_canvas = true
|
||||
@@ -2063,27 +2057,25 @@ export class LGraphCanvas {
|
||||
this.ctx.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
|
||||
this.selected_group = this.graph.getGroupOnPos(e.canvasX, e.canvasY)
|
||||
this.selected_group_resizing = false
|
||||
|
||||
const group = this.selected_group
|
||||
if (this.selected_group && !this.read_only) {
|
||||
if (group && !this.read_only) {
|
||||
if (e.ctrlKey) {
|
||||
this.dragging_rectangle = null
|
||||
}
|
||||
|
||||
const dist = distance([e.canvasX, e.canvasY], [this.selected_group.pos[0] + this.selected_group.size[0], this.selected_group.pos[1] + this.selected_group.size[1]])
|
||||
const dist = distance([e.canvasX, e.canvasY], [group.pos[0] + group.size[0], group.pos[1] + group.size[1]])
|
||||
if (dist * this.ds.scale < 10) {
|
||||
this.selected_group_resizing = true
|
||||
} else {
|
||||
const f = group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
|
||||
const headerHeight = f * 1.4
|
||||
if (isInsideRectangle(e.canvasX, e.canvasY, group.pos[0], group.pos[1], group.size[0], headerHeight)) {
|
||||
this.selected_group.recomputeInsideNodes()
|
||||
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) this.deselectAllNodes()
|
||||
this.selectedGroups ??= new Set()
|
||||
this.selectedGroups.add(group)
|
||||
group.selected = true
|
||||
group.recomputeInsideNodes()
|
||||
this.processSelect(group, e, true)
|
||||
|
||||
this.isDragging = true
|
||||
skip_action = true
|
||||
@@ -2195,14 +2187,8 @@ export class LGraphCanvas {
|
||||
|
||||
// is it hover a node ?
|
||||
if (node) {
|
||||
if (Object.keys(this.selected_nodes).length
|
||||
&& (this.selected_nodes[node.id] || e.shiftKey || e.ctrlKey || e.metaKey)) {
|
||||
// is multiselected or using shift to include the now node
|
||||
if (!this.selected_nodes[node.id]) this.selectNodes([node], true) // add this if not present
|
||||
} else {
|
||||
// update selection
|
||||
this.selectNodes([node])
|
||||
}
|
||||
// add this if not present
|
||||
this.processSelect(node, e, true)
|
||||
}
|
||||
|
||||
// Show context menu for the node or group under the pointer
|
||||
@@ -2440,35 +2426,26 @@ export class LGraphCanvas {
|
||||
this.node_capturing_input.onMouseMove?.(e, [e.canvasX - this.node_capturing_input.pos[0], e.canvasY - this.node_capturing_input.pos[1]], this)
|
||||
}
|
||||
|
||||
//node being dragged
|
||||
// Items being dragged
|
||||
if (this.isDragging && !this.live_mode) {
|
||||
//console.log("draggin!",this.selected_nodes);
|
||||
const nodes = new Set<LGraphNode>()
|
||||
const deltax = delta[0] / this.ds.scale
|
||||
const deltay = delta[1] / this.ds.scale
|
||||
for (const i in this.selected_nodes) {
|
||||
const n = this.selected_nodes[i]
|
||||
nodes.add(n)
|
||||
n.pos[0] += delta[0] / this.ds.scale
|
||||
n.pos[1] += delta[1] / this.ds.scale
|
||||
}
|
||||
const selected = this.selectedItems
|
||||
const allItems = e.ctrlKey ? selected : new Set<Positionable>()
|
||||
|
||||
if (this.selectedGroups) {
|
||||
for (const group of this.selectedGroups) {
|
||||
group.move(deltax, deltay, true)
|
||||
if (!e.ctrlKey) {
|
||||
for (const node of group._nodes) {
|
||||
if (!nodes.has(node)) {
|
||||
node.pos[0] += deltax
|
||||
node.pos[1] += deltay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!e.ctrlKey)
|
||||
selected?.forEach(x => addToSetRecursively(x, allItems))
|
||||
|
||||
const deltaX = delta[0] / this.ds.scale
|
||||
const deltaY = delta[1] / this.ds.scale
|
||||
allItems.forEach(x => x.move(deltaX, deltaY, true))
|
||||
|
||||
this.dirty_canvas = true
|
||||
this.dirty_bgcanvas = true
|
||||
|
||||
function addToSetRecursively(item: Positionable, items: Set<Positionable>): void {
|
||||
if (items.has(item)) return
|
||||
items.add(item)
|
||||
item.children?.forEach(x => addToSetRecursively(x, items))
|
||||
}
|
||||
}
|
||||
|
||||
if (this.resizing_node && !this.live_mode) {
|
||||
@@ -2526,20 +2503,7 @@ export class LGraphCanvas {
|
||||
this.node_widget = null
|
||||
|
||||
if (this.selected_group) {
|
||||
const diffx = this.selected_group.pos[0] -
|
||||
Math.round(this.selected_group.pos[0])
|
||||
const diffy = this.selected_group.pos[1] -
|
||||
Math.round(this.selected_group.pos[1])
|
||||
this.selected_group.move(diffx, diffy, e.ctrlKey)
|
||||
this.selected_group.pos[0] = Math.round(
|
||||
this.selected_group.pos[0]
|
||||
)
|
||||
this.selected_group.pos[1] = Math.round(
|
||||
this.selected_group.pos[1]
|
||||
)
|
||||
if (this.selected_group._nodes.length) {
|
||||
this.dirty_canvas = true
|
||||
}
|
||||
this.dirty_canvas = true
|
||||
this.selected_group = null
|
||||
}
|
||||
this.selected_group_resizing = false
|
||||
@@ -2574,32 +2538,21 @@ export class LGraphCanvas {
|
||||
if (!node || (w > 10 && h > 10)) {
|
||||
//test against all nodes (not visible because the rectangle maybe start outside
|
||||
const to_select = []
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
const nodeX = nodes[i]
|
||||
for (const nodeX of nodes) {
|
||||
nodeX.getBounding(node_bounding)
|
||||
if (!overlapBounding(
|
||||
this.dragging_rectangle,
|
||||
node_bounding
|
||||
)) {
|
||||
continue
|
||||
} //out of the visible area
|
||||
if (!overlapBounding(this.dragging_rectangle, node_bounding)) continue
|
||||
|
||||
to_select.push(nodeX)
|
||||
}
|
||||
if (to_select.length) {
|
||||
this.selectNodes(to_select, e.shiftKey) // add to selection with shift
|
||||
}
|
||||
// add to selection with shift
|
||||
if (to_select.length) this.selectNodes(to_select, e.shiftKey)
|
||||
|
||||
// Select groups
|
||||
if (!e.shiftKey) this.deselectGroups()
|
||||
this.selectedGroups ??= new Set()
|
||||
|
||||
const groups = this.graph.groups
|
||||
for (const group of groups) {
|
||||
const r = this.dragging_rectangle
|
||||
const pos = group.pos
|
||||
const size = group.size
|
||||
if (!isInsideRectangle(pos[0], pos[1], r[0], r[1], r[2], r[3]) || !isInsideRectangle(pos[0] + size[0], pos[1] + size[1], r[0], r[1], r[2], r[3])) continue
|
||||
this.selectedGroups.add(group)
|
||||
if (!containsRect(r, group._bounding)) continue
|
||||
this.selectedItems.add(group)
|
||||
group.recomputeInsideNodes()
|
||||
group.selected = true
|
||||
}
|
||||
@@ -2731,7 +2684,7 @@ export class LGraphCanvas {
|
||||
)
|
||||
|
||||
if (!node && e.click_time < 300 && !this.graph.groups.some(x => x.isPointInTitlebar(e.canvasX, e.canvasY))) {
|
||||
this.deselectAllNodes()
|
||||
this.deselectAll()
|
||||
}
|
||||
|
||||
this.dirty_canvas = true
|
||||
@@ -3216,122 +3169,167 @@ export class LGraphCanvas {
|
||||
|
||||
this.setDirty(true)
|
||||
}
|
||||
processNodeSelected(node: LGraphNode, e: CanvasMouseEvent): void {
|
||||
this.selectNode(node, e && (e.shiftKey || e.metaKey || e.ctrlKey || this.multi_select))
|
||||
this.onNodeSelected?.(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* selects a given node (or adds it to the current selection)
|
||||
**/
|
||||
* Determines whether to select or deselect an item that has received a pointer event. Will deselect other nodes if
|
||||
* @param item Canvas item to select/deselect
|
||||
* @param e The MouseEvent to handle
|
||||
* @param sticky Prevents deselecting individual nodes (as used by aux/right-click)
|
||||
* @remarks
|
||||
* Accessibility: anyone using {@link mutli_select} always deselects when clicking empty space.
|
||||
*/
|
||||
processSelect<TPositionable extends Positionable = LGraphNode>(item: TPositionable | null, e: CanvasMouseEvent, sticky: boolean = false): void {
|
||||
const addModifier = e?.shiftKey
|
||||
const subtractModifier = e != null && (e.metaKey || e.ctrlKey)
|
||||
const eitherModifier = addModifier || subtractModifier
|
||||
const modifySelection = eitherModifier || this.multi_select
|
||||
|
||||
if (!item) {
|
||||
if (!eitherModifier || this.multi_select) this.deselectAll()
|
||||
|
||||
} else if (!item.selected || !this.selectedItems.has(item)) {
|
||||
if (!modifySelection) this.deselectAll(item)
|
||||
this.select(item)
|
||||
} else if (modifySelection && !sticky) {
|
||||
this.deselect(item)
|
||||
} else if (!sticky) {
|
||||
this.deselectAll(item)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.onSelectionChange?.(this.selected_nodes)
|
||||
this.setDirty(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a {@link Positionable} item.
|
||||
* @param item The canvas item to add to the selection.
|
||||
*/
|
||||
select<TPositionable extends Positionable = LGraphNode>(item: TPositionable): void {
|
||||
if (item.selected && this.selectedItems.has(item)) return
|
||||
|
||||
item.selected = true
|
||||
this.selectedItems.add(item)
|
||||
if (!(item instanceof LGraphNode)) return
|
||||
|
||||
// Node-specific handling
|
||||
item.onSelected?.()
|
||||
this.selected_nodes[item.id] = item
|
||||
|
||||
this.onNodeSelected?.(item)
|
||||
|
||||
// Highlight links
|
||||
item.inputs?.forEach(input => this.highlighted_links[input.link] = true)
|
||||
item.outputs?.flatMap(x => x.links)
|
||||
.forEach(id => this.highlighted_links[id] = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselects a {@link Positionable} item.
|
||||
* @param item The canvas item to remove from the selection.
|
||||
*/
|
||||
deselect<TPositionable extends Positionable = LGraphNode>(item: TPositionable): void {
|
||||
if (!item.selected && !this.selectedItems.has(item)) return
|
||||
|
||||
item.selected = false
|
||||
this.selectedItems.delete(item)
|
||||
if (!(item instanceof LGraphNode)) return
|
||||
|
||||
// Node-specific handling
|
||||
item.onDeselected?.()
|
||||
delete this.selected_nodes[item.id]
|
||||
|
||||
this.onNodeDeselected?.(item)
|
||||
|
||||
// Clear link highlight
|
||||
item.inputs?.forEach(input => delete this.highlighted_links[input.link])
|
||||
item.outputs?.flatMap(x => x.links)
|
||||
.forEach(id => delete this.highlighted_links[id])
|
||||
}
|
||||
|
||||
/** @deprecated See {@link LGraphCanvas.processSelect} */
|
||||
processNodeSelected(item: LGraphNode, e: CanvasMouseEvent): void {
|
||||
this.processSelect(item, e, e && (e.shiftKey || e.metaKey || e.ctrlKey || this.multi_select))
|
||||
}
|
||||
|
||||
/** @deprecated See {@link LGraphCanvas.select} */
|
||||
selectNode(node: LGraphNode, add_to_current_selection?: boolean): void {
|
||||
if (node == null) {
|
||||
this.deselectAllNodes()
|
||||
this.deselectAll()
|
||||
} else {
|
||||
this.selectNodes([node], add_to_current_selection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* selects several nodes (or adds them to the current selection)
|
||||
**/
|
||||
selectNodes(nodes?: LGraphNode[] | Dictionary<LGraphNode>, add_to_current_selection?: boolean): void {
|
||||
if (!add_to_current_selection) {
|
||||
this.deselectAllNodes()
|
||||
}
|
||||
selectNodes(nodes?: LGraphNode[], add_to_current_selection?: boolean): void {
|
||||
nodes ||= this.graph._nodes
|
||||
|
||||
nodes = nodes || this.graph._nodes
|
||||
if (typeof nodes == "string") nodes = [nodes]
|
||||
for (const i in nodes) {
|
||||
const node: LGraphNode = nodes[i]
|
||||
if (node.selected) {
|
||||
this.deselectNode(node)
|
||||
continue
|
||||
}
|
||||
if (!add_to_current_selection) this.deselectAll()
|
||||
|
||||
if (!node.selected) {
|
||||
node.onSelected?.()
|
||||
}
|
||||
node.selected = true
|
||||
this.selected_nodes[node.id] = node
|
||||
|
||||
if (node.inputs) {
|
||||
for (let j = 0; j < node.inputs.length; ++j) {
|
||||
this.highlighted_links[node.inputs[j].link] = true
|
||||
}
|
||||
}
|
||||
if (node.outputs) {
|
||||
for (let j = 0; j < node.outputs.length; ++j) {
|
||||
const out = node.outputs[j]
|
||||
if (out.links) {
|
||||
for (let k = 0; k < out.links.length; ++k) {
|
||||
this.highlighted_links[out.links[k]] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const node of nodes) {
|
||||
if (node.selected || this.selectedItems.has(node)) {
|
||||
this.deselect(node)
|
||||
} else {
|
||||
this.select(node)
|
||||
}
|
||||
}
|
||||
|
||||
this.onSelectionChange?.(this.selected_nodes)
|
||||
|
||||
this.setDirty(true)
|
||||
}
|
||||
/**
|
||||
* removes a node from the current selection
|
||||
**/
|
||||
|
||||
/** @deprecated See {@link LGraphCanvas.deselect} */
|
||||
deselectNode(node: LGraphNode): void {
|
||||
if (!node.selected) return
|
||||
node.onDeselected?.()
|
||||
node.selected = false
|
||||
delete this.selected_nodes[node.id]
|
||||
|
||||
this.onNodeDeselected?.(node)
|
||||
|
||||
//remove highlighted
|
||||
if (node.inputs) {
|
||||
for (let i = 0; i < node.inputs.length; ++i) {
|
||||
delete this.highlighted_links[node.inputs[i].link]
|
||||
}
|
||||
}
|
||||
if (node.outputs) {
|
||||
for (let i = 0; i < node.outputs.length; ++i) {
|
||||
const out = node.outputs[i]
|
||||
if (out.links) {
|
||||
for (let j = 0; j < out.links.length; ++j) {
|
||||
delete this.highlighted_links[out.links[j]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.deselect(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* removes all nodes from the current selection
|
||||
**/
|
||||
deselectAllNodes(): void {
|
||||
* Deselects all items on the canvas.
|
||||
* @param keepSelected If set, this item will not be removed from the selection.
|
||||
*/
|
||||
deselectAll(keepSelected?: Positionable): void {
|
||||
if (!this.graph) return
|
||||
const nodes = this.graph._nodes
|
||||
for (let i = 0, l = nodes.length; i < l; ++i) {
|
||||
const node = nodes[i]
|
||||
if (!node.selected) {
|
||||
|
||||
const selected = this.selectedItems
|
||||
let wasSelected: Positionable
|
||||
for (const sel of selected) {
|
||||
if (sel === keepSelected) {
|
||||
wasSelected = sel
|
||||
continue
|
||||
}
|
||||
node.onDeselected?.()
|
||||
node.selected = false
|
||||
this.onNodeDeselected?.(node)
|
||||
sel.onDeselected?.()
|
||||
sel.selected = false
|
||||
}
|
||||
selected.clear()
|
||||
if (wasSelected) selected.add(wasSelected)
|
||||
|
||||
this.setDirty(true)
|
||||
|
||||
// Legacy code
|
||||
const oldNode = keepSelected?.id == null ? null : this.selected_nodes[keepSelected.id]
|
||||
this.selected_nodes = {}
|
||||
this.current_node = null
|
||||
this.highlighted_links = {}
|
||||
this.deselectGroups()
|
||||
|
||||
if (keepSelected instanceof LGraphNode) {
|
||||
// Handle old object lookup
|
||||
if (oldNode) this.selected_nodes[oldNode.id] = oldNode
|
||||
|
||||
// Highlight links
|
||||
keepSelected.inputs?.forEach(input => this.highlighted_links[input.link] = true)
|
||||
keepSelected.outputs?.flatMap(x => x.links)
|
||||
.forEach(id => this.highlighted_links[id] = true)
|
||||
}
|
||||
|
||||
this.onSelectionChange?.(this.selected_nodes)
|
||||
this.setDirty(true)
|
||||
}
|
||||
|
||||
deselectGroups() {
|
||||
if (!this.selectedGroups) return
|
||||
for (const group of this.selectedGroups) {
|
||||
delete group.selected
|
||||
}
|
||||
this.selectedGroups = null
|
||||
/** @deprecated See {@link LGraphCanvas.deselectAll} */
|
||||
deselectAllNodes(): void {
|
||||
this.deselectAll()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { LGraph } from "./LGraph"
|
||||
import type { ISerialisedGroup } from "./types/serialisation"
|
||||
import { LiteGraph } from "./litegraph"
|
||||
import { LGraphCanvas } from "./LGraphCanvas"
|
||||
import { isInsideRectangle, overlapBounding } from "./measure"
|
||||
import { isInsideRectangle, containsCentre, containsRect, isPointInRectangle } from "./measure"
|
||||
import { LGraphNode } from "./LGraphNode"
|
||||
import { RenderShape, TitleMode } from "./types/globalEnums"
|
||||
|
||||
@@ -20,6 +20,7 @@ export class LGraphGroup implements Positionable {
|
||||
_bounding: Float32Array = new Float32Array([10, 10, 140, 80])
|
||||
_pos: Point = this._bounding.subarray(0, 2)
|
||||
_size: Size = this._bounding.subarray(2, 4)
|
||||
/** @deprecated See {@link _children} */
|
||||
_nodes: LGraphNode[] = []
|
||||
_children: Set<Positionable> = new Set()
|
||||
graph: LGraph | null = null
|
||||
@@ -69,6 +70,10 @@ export class LGraphGroup implements Positionable {
|
||||
return this.font_size * 1.4
|
||||
}
|
||||
|
||||
get children(): ReadonlySet<Positionable> {
|
||||
return this._children
|
||||
}
|
||||
|
||||
get pinned() {
|
||||
return !!this.flags.pinned
|
||||
}
|
||||
@@ -95,12 +100,7 @@ export class LGraphGroup implements Positionable {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
bounding: [
|
||||
Math.round(b[0]),
|
||||
Math.round(b[1]),
|
||||
Math.round(b[2]),
|
||||
Math.round(b[3])
|
||||
],
|
||||
bounding: [...b],
|
||||
color: this.color,
|
||||
font_size: this.font_size,
|
||||
flags: this.flags,
|
||||
@@ -168,19 +168,33 @@ export class LGraphGroup implements Positionable {
|
||||
}
|
||||
|
||||
recomputeInsideNodes(): void {
|
||||
const { nodes } = this.graph
|
||||
const { nodes, groups } = this.graph
|
||||
const children = this._children
|
||||
const node_bounding = new Float32Array(4)
|
||||
this._nodes.length = 0
|
||||
this._children.clear()
|
||||
children.clear()
|
||||
|
||||
// move any nodes we partially overlap
|
||||
for (const node of nodes) {
|
||||
node.getBounding(node_bounding)
|
||||
// Node overlaps with group
|
||||
if (overlapBounding(this._bounding, node_bounding)) {
|
||||
if (containsCentre(this._bounding, node_bounding)) {
|
||||
this._nodes.push(node)
|
||||
this._children.add(node)
|
||||
children.add(node)
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
if (containsRect(this._bounding, group._bounding))
|
||||
children.add(group)
|
||||
}
|
||||
|
||||
groups.sort((a, b) => {
|
||||
if (a === this) {
|
||||
return children.has(b) ? -1 : 0
|
||||
} else if (b === this) {
|
||||
return children.has(a) ? 1 : 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +228,7 @@ export class LGraphGroup implements Positionable {
|
||||
*/
|
||||
addNodes(nodes: LGraphNode[], padding: number = 10): void {
|
||||
if (!this._nodes && nodes.length === 0) return
|
||||
this.resizeTo([...this._nodes, ...nodes], padding)
|
||||
this.resizeTo([...this.children, ...this._nodes, ...nodes], padding)
|
||||
}
|
||||
|
||||
getMenuOptions(): IContextMenuValue[] {
|
||||
|
||||
@@ -12,6 +12,11 @@ export type NullableProperties<T> = {
|
||||
|
||||
export type CanvasColour = string | CanvasGradient | CanvasPattern
|
||||
|
||||
/**
|
||||
* An object that can be positioned, selected, and moved.
|
||||
*
|
||||
* May contain other {@link Positionable} objects.
|
||||
*/
|
||||
export interface Positionable {
|
||||
id: NodeId | number
|
||||
/** Position in graph coordinates. Default: 0,0 */
|
||||
|
||||
@@ -124,10 +124,16 @@ export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
||||
const bRight = b[0] + b[2]
|
||||
const bBottom = b[1] + b[3]
|
||||
|
||||
return a[0] < b[0]
|
||||
&& a[1] < b[1]
|
||||
&& aRight > bRight
|
||||
&& aBottom > bBottom
|
||||
const identical = a[0] === b[0]
|
||||
&& a[1] === b[1]
|
||||
&& aRight === bRight
|
||||
&& aBottom === bBottom
|
||||
|
||||
return !identical
|
||||
&& a[0] <= b[0]
|
||||
&& a[1] <= b[1]
|
||||
&& aRight >= bRight
|
||||
&& aBottom >= bBottom
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user