Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 135 additions & 39 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
/** The start position of the drag zoom and original read-only state. */
#dragZoomStart: { pos: Point; scale: number; readOnly: boolean } | null = null

/** If true, enable live selection during drag. Nodes are selected/deselected in real-time. */
liveSelection: boolean = false

getMenuOptions?(): IContextMenuValue<string>[]
getExtraMenuOptions?(
canvas: LGraphCanvas,
Expand Down Expand Up @@ -2627,7 +2630,20 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.processSelect(clickedItem, eUp)
}
pointer.onDragStart = () => (this.dragging_rectangle = dragRect)
pointer.onDragEnd = (upEvent) => this.#handleMultiSelect(upEvent, dragRect)

if (this.liveSelection) {
const initialSelection = new Set(this.selectedItems)

pointer.onDrag = (eMove) =>
this.handleLiveSelect(eMove, dragRect, initialSelection)

pointer.onDragEnd = () => this.finalizeLiveSelect()
} else {
// Classic mode: select only when drag ends
pointer.onDragEnd = (upEvent) =>
this.#handleMultiSelect(upEvent, dragRect)
}

pointer.finally = () => (this.dragging_rectangle = null)
}

Expand Down Expand Up @@ -4087,76 +4103,156 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.setDirty(true)
}

#handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect) {
// Process drag
// Convert Point pair (pos, offset) to Rect
const { graph, selectedItems, subgraph } = this
if (!graph) throw new NullGraphError()

/**
* Normalizes a drag rectangle to have positive width and height.
* @param dragRect The drag rectangle to normalize (modified in place)
* @returns The normalized rectangle
*/
#normalizeDragRect(dragRect: Rect): Rect {
const w = Math.abs(dragRect[2])
const h = Math.abs(dragRect[3])
if (dragRect[2] < 0) dragRect[0] -= w
if (dragRect[3] < 0) dragRect[1] -= h
dragRect[2] = w
dragRect[3] = h
return dragRect
}

/**
* Gets all positionable items that overlap with the given rectangle.
* @param rect The rectangle to check against
* @returns Set of positionable items that overlap with the rectangle
*/
#getItemsInRect(rect: Rect): Set<Positionable> {
const { graph, subgraph } = this
if (!graph) throw new NullGraphError()

// Select nodes - any part of the node is in the select area
const isSelected = new Set<Positionable>()
const notSelected: Positionable[] = []
const items = new Set<Positionable>()

if (subgraph) {
const { inputNode, outputNode } = subgraph
if (overlapBounding(rect, inputNode.boundingRect)) items.add(inputNode)
if (overlapBounding(rect, outputNode.boundingRect)) items.add(outputNode)
}

if (overlapBounding(dragRect, inputNode.boundingRect)) {
addPositionable(inputNode)
}
if (overlapBounding(dragRect, outputNode.boundingRect)) {
addPositionable(outputNode)
}
for (const node of graph._nodes) {
if (overlapBounding(rect, node.boundingRect)) items.add(node)
}

for (const nodeX of graph._nodes) {
if (overlapBounding(dragRect, nodeX.boundingRect)) {
addPositionable(nodeX)
// Check groups (must be wholly inside)
for (const group of graph.groups) {
if (containsRect(rect, group._bounding)) {
group.recomputeInsideNodes()
items.add(group)
}
}

// Select groups - the group is wholly inside the select area
for (const group of graph.groups) {
if (!containsRect(dragRect, group._bounding)) continue
// Check reroutes (center point must be inside)
for (const reroute of graph.reroutes.values()) {
if (isPointInRect(reroute.pos, rect)) items.add(reroute)
}

group.recomputeInsideNodes()
addPositionable(group)
return items
}

/**
* Handles live selection updates during drag. Called on each pointer move.
* @param e The pointer move event
* @param dragRect The current drag rectangle
* @param initialSelection The selection state before the drag started
*/
private handleLiveSelect(
e: CanvasPointerEvent,
dragRect: Rect,
initialSelection: Set<Positionable>
): void {
// Ensure rect is current even if pointer.onDrag fires before processMouseMove updates it
dragRect[2] = e.canvasX - dragRect[0]
dragRect[3] = e.canvasY - dragRect[1]

// Create a normalized copy for overlap checking
const normalizedRect: Rect = [
dragRect[0],
dragRect[1],
dragRect[2],
dragRect[3]
]
this.#normalizeDragRect(normalizedRect)

const itemsInRect = this.#getItemsInRect(normalizedRect)

const desired = new Set<Positionable>()
if (e.shiftKey && !e.altKey) {
for (const item of initialSelection) desired.add(item)
for (const item of itemsInRect) desired.add(item)
} else if (e.altKey && !e.shiftKey) {
for (const item of initialSelection)
if (!itemsInRect.has(item)) desired.add(item)
} else {
for (const item of itemsInRect) desired.add(item)
}

// Select reroutes - the centre point is inside the select area
for (const reroute of graph.reroutes.values()) {
if (!isPointInRect(reroute.pos, dragRect)) continue
let changed = false
for (const item of [...this.selectedItems]) {
if (!desired.has(item)) {
this.deselect(item)
changed = true
}
}
for (const item of desired) {
if (!this.selectedItems.has(item)) {
this.select(item)
changed = true
}
}

selectedItems.add(reroute)
reroute.selected = true
addPositionable(reroute)
if (changed) {
this.onSelectionChange?.(this.selected_nodes)
this.setDirty(true)
}
}

/**
* Finalizes the live selection when drag ends.
*/
private finalizeLiveSelect(): void {
// Selection is already updated by handleLiveSelect
// Just trigger the final selection change callback
this.onSelectionChange?.(this.selected_nodes)
}

/**
* Handles multi-select when drag ends (classic mode).
* @param e The pointer up event
* @param dragRect The drag rectangle
*/
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect): void {
const normalizedRect: Rect = [
dragRect[0],
dragRect[1],
dragRect[2],
dragRect[3]
]
this.#normalizeDragRect(normalizedRect)

const itemsInRect = this.#getItemsInRect(normalizedRect)
const { selectedItems } = this

if (e.shiftKey) {
// Add to selection
for (const item of notSelected) this.select(item)
for (const item of itemsInRect) this.select(item)
} else if (e.altKey) {
// Remove from selection
for (const item of isSelected) this.deselect(item)
for (const item of itemsInRect) this.deselect(item)
} else {
// Replace selection
for (const item of selectedItems.values()) {
if (!isSelected.has(item)) this.deselect(item)
if (!itemsInRect.has(item)) this.deselect(item)
}
for (const item of notSelected) this.select(item)
for (const item of itemsInRect) this.select(item)
}
this.onSelectionChange?.(this.selected_nodes)

function addPositionable(item: Positionable): void {
if (!item.selected || !selectedItems.has(item)) notSelected.push(item)
else isSelected.add(item)
}
this.onSelectionChange?.(this.selected_nodes)
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/platform/settings/composables/useLitegraphSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export const useLitegraphSettings = () => {
if (canvas) canvas.dragZoomEnabled = dragZoomEnabled
})

watchEffect(() => {
const liveSelection = settingStore.get('Comfy.Graph.LiveSelection')
const { canvas } = canvasStore
if (canvas) canvas.liveSelection = liveSelection
})

watchEffect(() => {
CanvasPointer.doubleClickTime = settingStore.get(
'Comfy.Pointer.DoubleClickTime'
Expand Down
10 changes: 10 additions & 0 deletions src/platform/settings/constants/coreSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,16 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultValue: true,
versionAdded: '1.4.0'
},
{
id: 'Comfy.Graph.LiveSelection',
category: ['LiteGraph', 'Canvas', 'LiveSelection'],
name: 'Live selection',
tooltip:
'When enabled, nodes are selected/deselected in real-time as you drag the selection rectangle, similar to other design tools.',
type: 'boolean',
defaultValue: false,
versionAdded: '1.36.1'
},
{
id: 'Comfy.Pointer.ClickDrift',
category: ['LiteGraph', 'Pointer', 'ClickDrift'],
Expand Down
1 change: 1 addition & 0 deletions src/schemas/apiSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ const zSettings = z.object({
'Comfy.Graph.CanvasInfo': z.boolean(),
'Comfy.Graph.CanvasMenu': z.boolean(),
'Comfy.Graph.CtrlShiftZoom': z.boolean(),
'Comfy.Graph.LiveSelection': z.boolean(),
'Comfy.Graph.LinkMarkers': z.nativeEnum(LinkMarkerShape),
'Comfy.Graph.ZoomSpeed': z.number(),
'Comfy.Group.DoubleClickTitleToEdit': z.boolean(),
Expand Down