mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 13:32:11 +00:00
Compare commits
7 Commits
pysssss/cu
...
perf/fix-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d6f1064f9 | ||
|
|
d0e617af49 | ||
|
|
30dad84223 | ||
|
|
e03949b7fb | ||
|
|
85ea84635a | ||
|
|
2576e30243 | ||
|
|
22cc0728af |
241
src/components/graph/DomWidgets.bench.test.ts
Normal file
241
src/components/graph/DomWidgets.bench.test.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Reactive-write regression tests for DomWidgets.updateWidgets.
|
||||
*
|
||||
* `updateWidgets` runs every canvas draw frame (60fps). Each reactive write to
|
||||
* widgetState fields (pos / size / zIndex / readonly / computedDisabled) fires
|
||||
* the downstream watchers in DomWidget.vue, which recompute style and call
|
||||
* setStyle on the DOM element. Before the equality-check optimization, idle
|
||||
* frames produced N writes per widget per frame across 5 fields = ~5N setStyle
|
||||
* calls per frame for free.
|
||||
*
|
||||
* These tests pin down:
|
||||
* - Idle frames produce zero reactive writes per widget after init.
|
||||
* - Pan frames force pos reassignment but skip the other 4 fields.
|
||||
* - Selected-node movement forces pos reassignment on all visible widgets
|
||||
* (so non-selected widgets refresh their clip-path).
|
||||
*/
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import { render } from '@testing-library/vue'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
|
||||
type TestWidget = BaseDOMWidget<object | string>
|
||||
|
||||
function createNode(graph: LGraph, id: number, pos: [number, number]) {
|
||||
const node = new LGraphNode(`n${id}`)
|
||||
node.id = id
|
||||
node.pos = [...pos]
|
||||
node.size = [240, 120]
|
||||
graph.add(node)
|
||||
return node
|
||||
}
|
||||
|
||||
function createWidget(id: string, node: LGraphNode): TestWidget {
|
||||
return fromPartial<TestWidget>({
|
||||
id,
|
||||
node,
|
||||
name: 'w',
|
||||
type: 'custom',
|
||||
value: '',
|
||||
options: {},
|
||||
y: 12,
|
||||
width: 120,
|
||||
computedHeight: 40,
|
||||
margin: 10,
|
||||
isVisible: () => true
|
||||
})
|
||||
}
|
||||
|
||||
function createCanvas(graph: LGraph): LGraphCanvas {
|
||||
return fromPartial<LGraphCanvas>({
|
||||
graph,
|
||||
low_quality: false,
|
||||
read_only: false,
|
||||
isNodeVisible: vi.fn(() => true),
|
||||
ds: { offset: [0, 0], scale: 1 },
|
||||
selected_nodes: {}
|
||||
})
|
||||
}
|
||||
|
||||
function drawFrame(canvas: LGraphCanvas) {
|
||||
canvas.onDrawForeground?.({} as CanvasRenderingContext2D, new Rectangle())
|
||||
}
|
||||
|
||||
interface WriteCounts {
|
||||
pos: number
|
||||
size: number
|
||||
zIndex: number
|
||||
readonly: number
|
||||
computedDisabled: number
|
||||
}
|
||||
|
||||
function setupScene(nWidgets: number) {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
const canvasStore = useCanvasStore()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const graph = new LGraph()
|
||||
const widgetIds: string[] = []
|
||||
for (let i = 0; i < nWidgets; i++) {
|
||||
const node = createNode(graph, i, [i * 50, 0])
|
||||
const w = createWidget(`w${i}`, node)
|
||||
domWidgetStore.registerWidget(w)
|
||||
widgetIds.push(w.id)
|
||||
}
|
||||
|
||||
const canvas = createCanvas(graph)
|
||||
canvasStore.canvas = canvas
|
||||
|
||||
render(DomWidgets, { global: { stubs: { DomWidget: true } } })
|
||||
|
||||
const counts: WriteCounts = {
|
||||
pos: 0,
|
||||
size: 0,
|
||||
zIndex: 0,
|
||||
readonly: 0,
|
||||
computedDisabled: 0
|
||||
}
|
||||
for (const id of widgetIds) {
|
||||
const s = domWidgetStore.widgetStates.get(id)!
|
||||
watch(
|
||||
() => s.pos,
|
||||
() => counts.pos++,
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
watch(
|
||||
() => s.size,
|
||||
() => counts.size++,
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
watch(
|
||||
() => s.zIndex,
|
||||
() => counts.zIndex++,
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
watch(
|
||||
() => s.readonly,
|
||||
() => counts.readonly++,
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
watch(
|
||||
() => s.computedDisabled,
|
||||
() => counts.computedDisabled++,
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
}
|
||||
|
||||
return { canvas, counts, graph, domWidgetStore }
|
||||
}
|
||||
|
||||
describe('DomWidgets reactive-write budget', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('writes nothing on idle frames after the initial frame', () => {
|
||||
const N = 20
|
||||
const { canvas, counts } = setupScene(N)
|
||||
|
||||
drawFrame(canvas) // init frame seeds widgetState
|
||||
counts.pos = 0
|
||||
counts.size = 0
|
||||
counts.zIndex = 0
|
||||
counts.readonly = 0
|
||||
counts.computedDisabled = 0
|
||||
|
||||
for (let i = 0; i < 60; i++) drawFrame(canvas)
|
||||
|
||||
expect(counts.pos).toBe(0)
|
||||
expect(counts.size).toBe(0)
|
||||
expect(counts.zIndex).toBe(0)
|
||||
expect(counts.readonly).toBe(0)
|
||||
expect(counts.computedDisabled).toBe(0)
|
||||
})
|
||||
|
||||
it('on viewport pan, forces pos writes only — size/zIndex/readonly stay quiet', () => {
|
||||
const N = 20
|
||||
const { canvas, counts } = setupScene(N)
|
||||
|
||||
drawFrame(canvas)
|
||||
counts.pos = 0
|
||||
counts.size = 0
|
||||
counts.zIndex = 0
|
||||
counts.readonly = 0
|
||||
counts.computedDisabled = 0
|
||||
|
||||
const FRAMES = 60
|
||||
for (let i = 1; i <= FRAMES; i++) {
|
||||
canvas.ds.offset[0] = i * 2
|
||||
drawFrame(canvas)
|
||||
}
|
||||
|
||||
expect(counts.pos).toBe(N * FRAMES)
|
||||
expect(counts.size).toBe(0)
|
||||
expect(counts.zIndex).toBe(0)
|
||||
expect(counts.readonly).toBe(0)
|
||||
expect(counts.computedDisabled).toBe(0)
|
||||
})
|
||||
|
||||
it('on selected-node drag, forces pos writes on all visible widgets — size/zIndex/readonly stay quiet', () => {
|
||||
const N = 20
|
||||
const { canvas, counts, graph } = setupScene(N)
|
||||
const draggedNode = graph.getNodeById(0)!
|
||||
canvas.selected_nodes = { [draggedNode.id]: draggedNode }
|
||||
|
||||
drawFrame(canvas)
|
||||
counts.pos = 0
|
||||
counts.size = 0
|
||||
counts.zIndex = 0
|
||||
counts.readonly = 0
|
||||
counts.computedDisabled = 0
|
||||
|
||||
const FRAMES = 60
|
||||
for (let i = 1; i <= FRAMES; i++) {
|
||||
draggedNode.pos[0] = i * 3
|
||||
drawFrame(canvas)
|
||||
}
|
||||
|
||||
// Selected node moves → all N widgets re-evaluate pos so their clip-path
|
||||
// refreshes against the moved selection bounds.
|
||||
expect(counts.pos).toBe(N * FRAMES)
|
||||
expect(counts.size).toBe(0)
|
||||
expect(counts.zIndex).toBe(0)
|
||||
expect(counts.readonly).toBe(0)
|
||||
expect(counts.computedDisabled).toBe(0)
|
||||
})
|
||||
|
||||
it('on non-selected single-node movement, only the moved widget re-writes pos', () => {
|
||||
const N = 20
|
||||
const { canvas, counts, graph } = setupScene(N)
|
||||
|
||||
drawFrame(canvas)
|
||||
counts.pos = 0
|
||||
counts.size = 0
|
||||
counts.zIndex = 0
|
||||
counts.readonly = 0
|
||||
counts.computedDisabled = 0
|
||||
|
||||
const movedNode = graph.getNodeById(5)!
|
||||
const FRAMES = 60
|
||||
for (let i = 1; i <= FRAMES; i++) {
|
||||
movedNode.pos[0] = 250 + i
|
||||
drawFrame(canvas)
|
||||
}
|
||||
|
||||
expect(counts.pos).toBe(FRAMES) // 1 widget × FRAMES, not N × FRAMES
|
||||
expect(counts.size).toBe(0)
|
||||
expect(counts.zIndex).toBe(0)
|
||||
expect(counts.readonly).toBe(0)
|
||||
expect(counts.computedDisabled).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -49,7 +49,9 @@ function createCanvas(graph: LGraph): LGraphCanvas {
|
||||
graph,
|
||||
low_quality: false,
|
||||
read_only: false,
|
||||
isNodeVisible: vi.fn(() => true)
|
||||
isNodeVisible: vi.fn(() => true),
|
||||
ds: { offset: [0, 0], scale: 1 },
|
||||
selected_nodes: {}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,6 +150,125 @@ describe('DomWidgets transition grace characterization', () => {
|
||||
expect(widgetState.pos).toEqual([310, 428])
|
||||
})
|
||||
|
||||
it('forces pos reassignment on viewport pan even when canvas-space pos is unchanged', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const graph = new LGraph()
|
||||
const node = createNode(graph, 1, 'node', [100, 200])
|
||||
const widget = createWidget('viewport-widget', node, 12)
|
||||
domWidgetStore.registerWidget(widget)
|
||||
|
||||
const canvas = createCanvas(graph)
|
||||
canvasStore.canvas = canvas
|
||||
|
||||
render(DomWidgets, {
|
||||
global: { stubs: { DomWidget: true } }
|
||||
})
|
||||
|
||||
drawFrame(canvas)
|
||||
const widgetState = domWidgetStore.widgetStates.get(widget.id)
|
||||
if (!widgetState) throw new Error('Widget state not registered')
|
||||
const posAfterFirstFrame = widgetState.pos
|
||||
expect(posAfterFirstFrame).toEqual([110, 222])
|
||||
|
||||
// Canvas pan: ds.offset is non-reactive, so the downstream watcher only
|
||||
// fires if widgetState.pos is reassigned (a new array identity).
|
||||
canvas.ds.offset[0] = 50
|
||||
canvas.ds.offset[1] = 60
|
||||
drawFrame(canvas)
|
||||
|
||||
expect(widgetState.pos).not.toBe(posAfterFirstFrame)
|
||||
})
|
||||
|
||||
it('skips pos reassignment when viewport and canvas-space pos are both stable', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const graph = new LGraph()
|
||||
const node = createNode(graph, 1, 'node', [100, 200])
|
||||
const widget = createWidget('idle-widget', node, 12)
|
||||
domWidgetStore.registerWidget(widget)
|
||||
|
||||
const canvas = createCanvas(graph)
|
||||
canvasStore.canvas = canvas
|
||||
|
||||
render(DomWidgets, {
|
||||
global: { stubs: { DomWidget: true } }
|
||||
})
|
||||
|
||||
drawFrame(canvas)
|
||||
const widgetState = domWidgetStore.widgetStates.get(widget.id)
|
||||
if (!widgetState) throw new Error('Widget state not registered')
|
||||
const posAfterFirstFrame = widgetState.pos
|
||||
|
||||
// No pan, no node movement — pos array identity must be preserved
|
||||
// (this is the perf optimization being protected).
|
||||
drawFrame(canvas)
|
||||
expect(widgetState.pos).toBe(posAfterFirstFrame)
|
||||
})
|
||||
|
||||
it('mirrors widget.computedDisabled into widgetState each frame', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const graph = new LGraph()
|
||||
const node = createNode(graph, 1, 'node', [100, 200])
|
||||
const widget = createWidget('disabled-widget', node, 12)
|
||||
Object.assign(widget, { computedDisabled: false })
|
||||
domWidgetStore.registerWidget(widget)
|
||||
|
||||
const canvas = createCanvas(graph)
|
||||
canvasStore.canvas = canvas
|
||||
|
||||
render(DomWidgets, {
|
||||
global: { stubs: { DomWidget: true } }
|
||||
})
|
||||
|
||||
drawFrame(canvas)
|
||||
const widgetState = domWidgetStore.widgetStates.get(widget.id)
|
||||
if (!widgetState) throw new Error('Widget state not registered')
|
||||
expect(widgetState.computedDisabled).toBe(false)
|
||||
|
||||
// Simulate litegraph connecting an input -> widget.computedDisabled flips.
|
||||
Object.assign(widget, { computedDisabled: true })
|
||||
drawFrame(canvas)
|
||||
expect(widgetState.computedDisabled).toBe(true)
|
||||
})
|
||||
|
||||
it('forces pos reassignment for widgets when the selected node moves', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const graph = new LGraph()
|
||||
const movingNode = createNode(graph, 1, 'moving', [100, 100])
|
||||
const otherNode = createNode(graph, 2, 'other', [400, 100])
|
||||
const widget = createWidget('clipped-widget', otherNode, 12)
|
||||
domWidgetStore.registerWidget(widget)
|
||||
|
||||
const canvas = createCanvas(graph)
|
||||
// movingNode is the selected node — its renderArea drives clipping for
|
||||
// widgets owned by other nodes.
|
||||
canvas.selected_nodes = { 1: movingNode }
|
||||
canvasStore.canvas = canvas
|
||||
|
||||
render(DomWidgets, {
|
||||
global: { stubs: { DomWidget: true } }
|
||||
})
|
||||
|
||||
drawFrame(canvas)
|
||||
const widgetState = domWidgetStore.widgetStates.get(widget.id)
|
||||
if (!widgetState) throw new Error('Widget state not registered')
|
||||
const posAfterFirstFrame = widgetState.pos
|
||||
|
||||
// Drag the selected node — otherNode (and its widget) hasn't moved, but
|
||||
// the widget's clip-path depends on movingNode.renderArea, so the
|
||||
// downstream pos watcher must re-fire.
|
||||
movingNode.pos[0] = 150
|
||||
drawFrame(canvas)
|
||||
expect(widgetState.pos).not.toBe(posAfterFirstFrame)
|
||||
})
|
||||
|
||||
it('cleans orphaned transition-grace ids after widget removal', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
@@ -25,6 +25,27 @@ const overrideTransitionGrace = new Set<string>()
|
||||
|
||||
const widgetStates = computed(() => [...domWidgetStore.widgetStates.values()])
|
||||
|
||||
// Track canvas viewport between frames. Screen-space position depends on
|
||||
// lgCanvas.ds.offset and ds.scale, which are non-reactive. When the user
|
||||
// pans or zooms, canvas-space `pos` is unchanged but the rendered style
|
||||
// must update — force pos reassignment whenever the viewport changes so
|
||||
// the downstream watcher in DomWidget recomputes style with current ds.
|
||||
let lastViewportOffsetX = Number.NaN
|
||||
let lastViewportOffsetY = Number.NaN
|
||||
let lastViewportScale = Number.NaN
|
||||
|
||||
// Track the selected node's identity and bounds between frames. DOM widget
|
||||
// clipping is computed against the selected node's renderArea (non-reactive).
|
||||
// When the user drags or resizes the selected node, widgets owned by other
|
||||
// nodes must re-evaluate their clip-path even though their own pos hasn't
|
||||
// changed — force pos reassignment on those widgets so the downstream
|
||||
// watcher in DomWidget re-runs updateDomClipping().
|
||||
let lastSelectedNodeId: string | number | undefined
|
||||
let lastSelectedPosX = 0
|
||||
let lastSelectedPosY = 0
|
||||
let lastSelectedWidth = 0
|
||||
let lastSelectedHeight = 0
|
||||
|
||||
const updateWidgets = () => {
|
||||
const lgCanvas = canvasStore.canvas
|
||||
if (!lgCanvas) return
|
||||
@@ -33,6 +54,36 @@ const updateWidgets = () => {
|
||||
const currentGraph = lgCanvas.graph
|
||||
const seenWidgetIds = new Set<string>()
|
||||
|
||||
const viewportOffsetX = lgCanvas.ds.offset[0]
|
||||
const viewportOffsetY = lgCanvas.ds.offset[1]
|
||||
const viewportScale = lgCanvas.ds.scale
|
||||
const viewportChanged =
|
||||
lastViewportOffsetX !== viewportOffsetX ||
|
||||
lastViewportOffsetY !== viewportOffsetY ||
|
||||
lastViewportScale !== viewportScale
|
||||
lastViewportOffsetX = viewportOffsetX
|
||||
lastViewportOffsetY = viewportOffsetY
|
||||
lastViewportScale = viewportScale
|
||||
|
||||
const selectedNode = Object.values(lgCanvas.selected_nodes ?? {})[0]
|
||||
const selectedNodeId = selectedNode?.id
|
||||
const selectedPosX = selectedNode ? selectedNode.pos[0] : 0
|
||||
const selectedPosY = selectedNode ? selectedNode.pos[1] : 0
|
||||
const selectedWidth = selectedNode ? selectedNode.size[0] : 0
|
||||
const selectedHeight = selectedNode ? selectedNode.size[1] : 0
|
||||
const selectionChanged =
|
||||
lastSelectedNodeId !== selectedNodeId ||
|
||||
(!!selectedNode &&
|
||||
(lastSelectedPosX !== selectedPosX ||
|
||||
lastSelectedPosY !== selectedPosY ||
|
||||
lastSelectedWidth !== selectedWidth ||
|
||||
lastSelectedHeight !== selectedHeight))
|
||||
lastSelectedNodeId = selectedNodeId
|
||||
lastSelectedPosX = selectedPosX
|
||||
lastSelectedPosY = selectedPosY
|
||||
lastSelectedWidth = selectedWidth
|
||||
lastSelectedHeight = selectedHeight
|
||||
|
||||
for (const widgetState of widgetStates.value) {
|
||||
const widget = widgetState.widget
|
||||
seenWidgetIds.add(widget.id)
|
||||
@@ -83,16 +134,42 @@ const updateWidgets = () => {
|
||||
|
||||
if (widgetState.visible) {
|
||||
const margin = widget.margin
|
||||
widgetState.pos = [
|
||||
posNode.pos[0] + margin,
|
||||
posNode.pos[1] + margin + posWidget.y
|
||||
]
|
||||
widgetState.size = [
|
||||
(posWidget.width ?? posNode.width) - margin * 2,
|
||||
(posWidget.computedHeight ?? 50) - margin * 2
|
||||
]
|
||||
widgetState.zIndex = getDomWidgetZIndex(posNode, currentGraph)
|
||||
widgetState.readonly = lgCanvas.read_only
|
||||
const newPosX = posNode.pos[0] + margin
|
||||
const newPosY = posNode.pos[1] + margin + posWidget.y
|
||||
if (
|
||||
viewportChanged ||
|
||||
selectionChanged ||
|
||||
widgetState.pos[0] !== newPosX ||
|
||||
widgetState.pos[1] !== newPosY
|
||||
) {
|
||||
widgetState.pos = [newPosX, newPosY]
|
||||
}
|
||||
|
||||
const newWidth = (posWidget.width ?? posNode.width) - margin * 2
|
||||
const newHeight = (posWidget.computedHeight ?? 50) - margin * 2
|
||||
if (
|
||||
widgetState.size[0] !== newWidth ||
|
||||
widgetState.size[1] !== newHeight
|
||||
) {
|
||||
widgetState.size = [newWidth, newHeight]
|
||||
}
|
||||
|
||||
const newZIndex = getDomWidgetZIndex(posNode, currentGraph)
|
||||
if (widgetState.zIndex !== newZIndex) {
|
||||
widgetState.zIndex = newZIndex
|
||||
}
|
||||
|
||||
const newReadonly = lgCanvas.read_only
|
||||
if (widgetState.readonly !== newReadonly) {
|
||||
widgetState.readonly = newReadonly
|
||||
}
|
||||
|
||||
const newComputedDisabled = useOverride
|
||||
? (override.widget.computedDisabled ?? widget.computedDisabled ?? false)
|
||||
: (widget.computedDisabled ?? false)
|
||||
if (widgetState.computedDisabled !== newComputedDisabled) {
|
||||
widgetState.computedDisabled = newComputedDisabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ vi.mock('@/platform/settings/settingStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
function createWidgetState(overrideDisabled: boolean): DomWidgetState {
|
||||
function createWidgetState(computedDisabled: boolean): DomWidgetState {
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
const node = createMockLGraphNode({
|
||||
id: 1,
|
||||
@@ -76,7 +76,7 @@ function createWidgetState(overrideDisabled: boolean): DomWidgetState {
|
||||
domWidgetStore.registerWidget(widget)
|
||||
domWidgetStore.setPositionOverride(widget.id, {
|
||||
node: createMockLGraphNode({ id: 2 }),
|
||||
widget: { computedDisabled: overrideDisabled } as DomWidgetState['widget']
|
||||
widget: { computedDisabled } as DomWidgetState['widget']
|
||||
})
|
||||
|
||||
const state = domWidgetStore.widgetStates.get(widget.id)
|
||||
@@ -84,6 +84,7 @@ function createWidgetState(overrideDisabled: boolean): DomWidgetState {
|
||||
|
||||
state.zIndex = 2
|
||||
state.size = [100, 40]
|
||||
state.computedDisabled = computedDisabled
|
||||
|
||||
return reactive(state)
|
||||
}
|
||||
@@ -98,7 +99,7 @@ describe('DomWidget disabled style', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('uses disabled style when promoted override widget is computedDisabled', async () => {
|
||||
it('uses disabled style when widgetState.computedDisabled is true', async () => {
|
||||
const widgetState = createWidgetState(true)
|
||||
const { container } = render(DomWidget, {
|
||||
props: {
|
||||
|
||||
@@ -104,10 +104,7 @@ const updateDomClipping = () => {
|
||||
const { left, top } = useElementBounding(canvasStore.getCanvas().canvas)
|
||||
|
||||
function composeStyle() {
|
||||
const override = widgetState.positionOverride
|
||||
const isDisabled = override
|
||||
? (override.widget.computedDisabled ?? widget.computedDisabled)
|
||||
: widget.computedDisabled
|
||||
const isDisabled = widgetState.computedDisabled
|
||||
|
||||
style.value = {
|
||||
...positionStyle.value,
|
||||
@@ -122,15 +119,40 @@ function composeStyle() {
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => widgetState, left, top, enableDomClipping],
|
||||
([widgetState]) => {
|
||||
[
|
||||
() => widgetState.pos,
|
||||
() => widgetState.size,
|
||||
// Visibility transitions (e.g. LOD low_quality flipping) must refresh
|
||||
// style: while invisible, DomWidgets.vue does not update widgetState
|
||||
// and ds.offset/ds.scale are non-reactive, so updatePosition must be
|
||||
// re-run against the current viewport when the widget reappears.
|
||||
() => widgetState.visible,
|
||||
left,
|
||||
top
|
||||
],
|
||||
() => {
|
||||
updatePosition(widgetState)
|
||||
if (enableDomClipping.value) {
|
||||
updateDomClipping()
|
||||
}
|
||||
composeStyle()
|
||||
},
|
||||
{ deep: true }
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
[
|
||||
() => widgetState.zIndex,
|
||||
() => widgetState.readonly,
|
||||
() => widgetState.computedDisabled,
|
||||
() => widgetState.positionOverride,
|
||||
enableDomClipping
|
||||
],
|
||||
() => {
|
||||
if (enableDomClipping.value) {
|
||||
updateDomClipping()
|
||||
}
|
||||
composeStyle()
|
||||
}
|
||||
)
|
||||
|
||||
// Recompose style when clippingStyle updates asynchronously via RAF.
|
||||
|
||||
@@ -20,6 +20,13 @@ export interface DomWidgetState extends PositionConfig {
|
||||
widget: Raw<BaseDOMWidget<object | string>>
|
||||
visible: boolean
|
||||
readonly: boolean
|
||||
/**
|
||||
* Mirrors `widget.computedDisabled` (set by litegraph when a widget input
|
||||
* is connected). The underlying property is non-reactive, so DomWidgets.vue
|
||||
* snapshots it into widgetState each frame and DomWidget.vue watches the
|
||||
* snapshot to refresh opacity/pointer-events.
|
||||
*/
|
||||
computedDisabled: boolean
|
||||
zIndex: number
|
||||
/** If the widget belongs to the current graph/subgraph. */
|
||||
active: boolean
|
||||
@@ -42,6 +49,7 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
||||
widget: markRaw(widget) as unknown as Raw<BaseDOMWidget<object | string>>,
|
||||
visible: true,
|
||||
readonly: false,
|
||||
computedDisabled: false,
|
||||
zIndex: 0,
|
||||
pos: [0, 0],
|
||||
size: [0, 0],
|
||||
|
||||
Reference in New Issue
Block a user