mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
refactor: route collapsed size through layoutStore and measure()
Per review feedback, replace the parallel collapsedSizes Map with a first-class collapsedSize field stored directly in ynodes. measure() reads collapsed dimensions via a LiteGraph.getCollapsedSize accessor injected by the Vue layer, making boundingRect automatically correct. - Add LiteGraph.getCollapsedSize accessor (dependency injection) - measure() uses accessor for collapsed nodes when available - Store collapsedSize in ynode via mappers (layoutToYNode/yNodeToLayout) - Remove separate collapsedSizes Map and getter spread-merge - getNodeCollapsedSize reads directly from ynode (no yNodeToLayout) - clearNodeCollapsedSize on expand to prevent stale data - Restore selectionBorder.ts to original createBounds (no special path) - useVueFeatureFlags injects accessor on Vue mode enable
This commit is contained in:
@@ -6,6 +6,7 @@ import { createSharedComposable } from '@vueuse/core'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
|
||||
import { LiteGraph } from '../lib/litegraph/src/litegraph'
|
||||
|
||||
@@ -25,6 +26,10 @@ function useVueFeatureFlagsIndividual() {
|
||||
shouldRenderVueNodes,
|
||||
() => {
|
||||
LiteGraph.vueNodesMode = shouldRenderVueNodes.value
|
||||
LiteGraph.getCollapsedSize = shouldRenderVueNodes.value
|
||||
? (nodeId) =>
|
||||
layoutStore.getNodeCollapsedSize(String(nodeId)) ?? undefined
|
||||
: undefined
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
@@ -1,93 +1,68 @@
|
||||
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphCanvas, Rectangle } from '@/lib/litegraph/src/litegraph'
|
||||
import { createBounds, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
function getSelectionBounds(canvas: LGraphCanvas): ReadOnlyRect | null {
|
||||
const selectedItems = canvas.selectedItems
|
||||
if (selectedItems.size <= 1) return null
|
||||
|
||||
if (!LiteGraph.vueNodesMode) return createBounds(selectedItems, 10)
|
||||
|
||||
// In Vue mode, use layoutStore.collapsedSize for collapsed nodes
|
||||
// to get accurate dimensions instead of litegraph's fallback values.
|
||||
const padding = 10
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
for (const item of selectedItems) {
|
||||
const rect = item.boundingRect
|
||||
const id = 'id' in item ? String(item.id) : null
|
||||
const isCollapsed =
|
||||
'flags' in item &&
|
||||
!!(item as { flags?: { collapsed?: boolean } }).flags?.collapsed
|
||||
const collapsedSize =
|
||||
id && isCollapsed ? layoutStore.getNodeCollapsedSize(id) : undefined
|
||||
|
||||
if (collapsedSize) {
|
||||
minX = Math.min(minX, rect[0])
|
||||
minY = Math.min(minY, rect[1])
|
||||
maxX = Math.max(maxX, rect[0] + collapsedSize.width)
|
||||
maxY = Math.max(maxY, rect[1] + collapsedSize.height)
|
||||
} else {
|
||||
minX = Math.min(minX, rect[0])
|
||||
minY = Math.min(minY, rect[1])
|
||||
maxX = Math.max(maxX, rect[0] + rect[2])
|
||||
maxY = Math.max(maxY, rect[1] + rect[3])
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(minX)) return null
|
||||
return [
|
||||
minX - padding,
|
||||
minY - padding,
|
||||
maxX - minX + 2 * padding,
|
||||
maxY - minY + 2 * padding
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a dashed border around selected items that maintains constant pixel size
|
||||
* regardless of zoom level, similar to the DOM selection overlay.
|
||||
*/
|
||||
function drawSelectionBorder(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvas: LGraphCanvas
|
||||
) {
|
||||
const bounds = getSelectionBounds(canvas)
|
||||
const selectedItems = canvas.selectedItems
|
||||
|
||||
// Only draw if multiple items selected
|
||||
if (selectedItems.size <= 1) return
|
||||
|
||||
// Use the same bounds calculation as the toolbox
|
||||
const bounds = createBounds(selectedItems, 10)
|
||||
if (!bounds) return
|
||||
|
||||
const [x, y, width, height] = bounds
|
||||
|
||||
// Save context state
|
||||
ctx.save()
|
||||
|
||||
const borderWidth = 2 / canvas.ds.scale
|
||||
// Set up dashed line style that doesn't scale with zoom
|
||||
const borderWidth = 2 / canvas.ds.scale // Constant 2px regardless of zoom
|
||||
ctx.lineWidth = borderWidth
|
||||
ctx.strokeStyle =
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--border-color')
|
||||
.trim() || '#ffffff66'
|
||||
|
||||
// Create dash pattern that maintains visual size
|
||||
const dashSize = 5 / canvas.ds.scale
|
||||
ctx.setLineDash([dashSize, dashSize])
|
||||
|
||||
// Draw the border using the bounds directly
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, width, height, 8 / canvas.ds.scale)
|
||||
ctx.stroke()
|
||||
|
||||
// Restore context
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension that adds a dashed selection border for multiple selected nodes
|
||||
*/
|
||||
const ext = {
|
||||
name: 'Comfy.SelectionBorder',
|
||||
|
||||
async init() {
|
||||
// Hook into the canvas drawing
|
||||
const originalDrawForeground = app.canvas.onDrawForeground
|
||||
|
||||
app.canvas.onDrawForeground = function (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
visibleArea: Rectangle
|
||||
) {
|
||||
// Call original if it exists
|
||||
originalDrawForeground?.call(this, ctx, visibleArea)
|
||||
|
||||
// Draw our selection border
|
||||
drawSelectionBorder(ctx, app.canvas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2092,16 +2092,22 @@ export class LGraphNode
|
||||
out[2] = this.size[0]
|
||||
out[3] = this.size[1] + titleHeight
|
||||
} else {
|
||||
if (ctx) ctx.font = this.innerFontStyle
|
||||
this._collapsed_width = Math.min(
|
||||
this.size[0],
|
||||
ctx
|
||||
? cachedMeasureText(ctx, this.getTitle() ?? '') +
|
||||
LiteGraph.NODE_TITLE_HEIGHT * 2
|
||||
: 0
|
||||
)
|
||||
out[2] = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH
|
||||
out[3] = LiteGraph.NODE_TITLE_HEIGHT
|
||||
const collapsedSize = LiteGraph.getCollapsedSize?.(this.id)
|
||||
if (collapsedSize) {
|
||||
out[2] = collapsedSize.width
|
||||
out[3] = collapsedSize.height
|
||||
} else {
|
||||
if (ctx) ctx.font = this.innerFontStyle
|
||||
this._collapsed_width = Math.min(
|
||||
this.size[0],
|
||||
ctx
|
||||
? cachedMeasureText(ctx, this.getTitle() ?? '') +
|
||||
LiteGraph.NODE_TITLE_HEIGHT * 2
|
||||
: 0
|
||||
)
|
||||
out[2] = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH
|
||||
out[3] = LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -355,6 +355,15 @@ export class LiteGraphGlobal {
|
||||
*/
|
||||
vueNodesMode: boolean = false
|
||||
|
||||
/**
|
||||
* Optional accessor for collapsed node dimensions in Vue mode.
|
||||
* Set by the Vue layer to provide DOM-measured collapsed sizes
|
||||
* that measure() can use instead of canvas text measurement.
|
||||
*/
|
||||
getCollapsedSize?: (
|
||||
nodeId: string | number
|
||||
) => { width: number; height: number } | undefined
|
||||
|
||||
// Special Rendering Values pulled out of app.ts patches
|
||||
nodeOpacity = 1
|
||||
nodeLightness: number | undefined = undefined
|
||||
|
||||
@@ -155,6 +155,7 @@ LiteGraphGlobal {
|
||||
"dialog_close_on_mouse_leave_delay": 500,
|
||||
"distance": [Function],
|
||||
"do_add_triggers_slots": false,
|
||||
"getCollapsedSize": undefined,
|
||||
"highlight_selected_group": true,
|
||||
"isInsideRectangle": [Function],
|
||||
"leftMouseClickBehavior": "panning",
|
||||
|
||||
@@ -130,7 +130,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
// CustomRef cache and trigger functions
|
||||
private nodeRefs = new Map<NodeId, Ref<NodeLayout | null>>()
|
||||
private nodeTriggers = new Map<NodeId, () => void>()
|
||||
private collapsedSizes = new Map<NodeId, Size>()
|
||||
|
||||
// New data structures for hit testing
|
||||
private linkLayouts = new Map<LinkId, LinkLayout>()
|
||||
@@ -243,11 +242,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
get: () => {
|
||||
track()
|
||||
const ynode = this.ynodes.get(nodeId)
|
||||
const layout = ynode ? yNodeToLayout(ynode) : null
|
||||
const collapsedSize = this.collapsedSizes.get(nodeId)
|
||||
return layout && collapsedSize
|
||||
? { ...layout, collapsedSize }
|
||||
: layout
|
||||
return ynode ? yNodeToLayout(ynode) : null
|
||||
},
|
||||
set: (newLayout: NodeLayout | null) => {
|
||||
if (newLayout === null) {
|
||||
@@ -1006,7 +1001,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
): void {
|
||||
this.ydoc.transact(() => {
|
||||
this.ynodes.clear()
|
||||
this.collapsedSizes.clear()
|
||||
// Note: We intentionally do NOT clear nodeRefs and nodeTriggers here.
|
||||
// Vue components may already hold references to these refs, and clearing
|
||||
// them would break the reactivity chain. The refs will be reused when
|
||||
@@ -1554,12 +1548,22 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
|
||||
updateNodeCollapsedSize(nodeId: NodeId, size: Size): void {
|
||||
this.collapsedSizes.set(nodeId, size)
|
||||
const ynode = this.ynodes.get(nodeId)
|
||||
if (!ynode) return
|
||||
ynode.set('collapsedSize', size)
|
||||
this.nodeTriggers.get(nodeId)?.()
|
||||
}
|
||||
|
||||
getNodeCollapsedSize(nodeId: NodeId): Size | undefined {
|
||||
return this.collapsedSizes.get(nodeId)
|
||||
return this.ynodes.get(nodeId)?.get('collapsedSize') as Size | undefined
|
||||
}
|
||||
|
||||
clearNodeCollapsedSize(nodeId: NodeId): void {
|
||||
const ynode = this.ynodes.get(nodeId)
|
||||
if (ynode?.has('collapsedSize')) {
|
||||
ynode.delete('collapsedSize')
|
||||
this.nodeTriggers.get(nodeId)?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export function layoutToYNode(layout: NodeLayout): NodeLayoutMap {
|
||||
ynode.set('zIndex', layout.zIndex)
|
||||
ynode.set('visible', layout.visible)
|
||||
ynode.set('bounds', layout.bounds)
|
||||
if (layout.collapsedSize) ynode.set('collapsedSize', layout.collapsedSize)
|
||||
return ynode
|
||||
}
|
||||
|
||||
@@ -34,7 +35,7 @@ function getOr<K extends keyof NodeLayout>(
|
||||
}
|
||||
|
||||
export function yNodeToLayout(ynode: NodeLayoutMap): NodeLayout {
|
||||
return {
|
||||
const layout: NodeLayout = {
|
||||
id: getOr(ynode, 'id', NODE_LAYOUT_DEFAULTS.id),
|
||||
position: getOr(ynode, 'position', NODE_LAYOUT_DEFAULTS.position),
|
||||
size: getOr(ynode, 'size', NODE_LAYOUT_DEFAULTS.size),
|
||||
@@ -42,4 +43,8 @@ export function yNodeToLayout(ynode: NodeLayoutMap): NodeLayout {
|
||||
visible: getOr(ynode, 'visible', NODE_LAYOUT_DEFAULTS.visible),
|
||||
bounds: getOr(ynode, 'bounds', NODE_LAYOUT_DEFAULTS.bounds)
|
||||
}
|
||||
const collapsedSize = ynode.get('collapsedSize')
|
||||
if (collapsedSize)
|
||||
layout.collapsedSize = collapsedSize as NodeLayout['collapsedSize']
|
||||
return layout
|
||||
}
|
||||
|
||||
@@ -68,7 +68,9 @@ vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
|
||||
batchUpdateNodeBounds: testState.batchUpdateNodeBounds,
|
||||
setSource: testState.setSource,
|
||||
getNodeLayoutRef: (nodeId: NodeId): Ref<NodeLayout | null> =>
|
||||
ref<NodeLayout | null>(testState.nodeLayouts.get(nodeId) ?? null)
|
||||
ref<NodeLayout | null>(testState.nodeLayouts.get(nodeId) ?? null),
|
||||
clearNodeCollapsedSize: vi.fn(),
|
||||
updateNodeCollapsedSize: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
@@ -161,6 +161,11 @@ const resizeObserver = new ResizeObserver((entries) => {
|
||||
continue
|
||||
}
|
||||
|
||||
// Clear stale collapsedSize when node is expanded
|
||||
if (elementType === 'node' && nodeId) {
|
||||
layoutStore.clearNodeCollapsedSize(nodeId)
|
||||
}
|
||||
|
||||
// Measure the full root element (including footer in flow).
|
||||
// min-height is applied to the root, so footer height in node.size
|
||||
// does not accumulate on Vue/legacy mode switching.
|
||||
|
||||
Reference in New Issue
Block a user