Skip to content

Commit 0f18640

Browse files
fix: restore Vue node sizes correctly when loading workflows
Previously, Vue nodes would reset to default size after page refresh because the onMounted callback was incorrectly multiplying canvas-space sizes by zoom scale and calling resize(), which overwrote correctly loaded sizes. Changes: - Set DOM size directly in onMounted, respecting intrinsic content minimum - Extract calculateIntrinsicSize() helper to eliminate code duplication - Remove unused resize() function from useNodeLayout - Add unit tests for calculateIntrinsicSize utility The fix ensures: - Loaded node sizes persist correctly across page refreshes - Nodes respect their minimum content size (intrinsic layout) - Manual resizing continues to work via ResizeObserver - No spurious layout mutations on initial load
1 parent 81b67b2 commit 0f18640

File tree

5 files changed

+193
-35
lines changed

5 files changed

+193
-35
lines changed

src/renderer/extensions/vueNodes/components/LGraphNode.vue

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ import {
153153
import { cn } from '@/utils/tailwindUtil'
154154
155155
import { useNodeResize } from '../composables/useNodeResize'
156+
import { calculateIntrinsicSize } from '../utils/calculateIntrinsicSize'
156157
import NodeContent from './NodeContent.vue'
157158
import NodeHeader from './NodeHeader.vue'
158159
import NodeSlots from './NodeSlots.vue'
@@ -245,7 +246,7 @@ onErrorCaptured((error) => {
245246
})
246247
247248
// Use layout system for node position and dragging
248-
const { position, size, zIndex, resize } = useNodeLayout(() => nodeData.id)
249+
const { position, size, zIndex } = useNodeLayout(() => nodeData.id)
249250
const { pointerHandlers, isDragging, dragStyle } = useNodePointerInteractions(
250251
() => nodeData,
251252
handleNodeSelect
@@ -267,13 +268,19 @@ const handleContextMenu = (event: MouseEvent) => {
267268
}
268269
269270
onMounted(() => {
270-
if (size.value && transformState?.camera) {
271-
const scale = transformState.camera.z
272-
const screenSize = {
273-
width: size.value.width * scale,
274-
height: size.value.height * scale
275-
}
276-
resize(screenSize)
271+
// Set initial DOM size from layout store, but respect intrinsic content minimum
272+
if (size.value && nodeContainerRef.value && transformState) {
273+
const intrinsicMin = calculateIntrinsicSize(
274+
nodeContainerRef.value,
275+
transformState.camera.z
276+
)
277+
278+
// Use the larger of stored size or intrinsic minimum
279+
const finalWidth = Math.max(size.value.width, intrinsicMin.width)
280+
const finalHeight = Math.max(size.value.height, intrinsicMin.height)
281+
282+
nodeContainerRef.value.style.width = `${finalWidth}px`
283+
nodeContainerRef.value.style.height = `${finalHeight}px`
277284
}
278285
})
279286

src/renderer/extensions/vueNodes/composables/useNodeResize.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEventListener } from '@vueuse/core'
22
import { ref } from 'vue'
33

44
import type { TransformState } from '@/renderer/core/layout/injectionKeys'
5+
import { calculateIntrinsicSize } from '@/renderer/extensions/vueNodes/utils/calculateIntrinsicSize'
56

67
interface Size {
78
width: number
@@ -53,29 +54,16 @@ export function useNodeResize(
5354
if (!(nodeElement instanceof HTMLElement)) return
5455

5556
const rect = nodeElement.getBoundingClientRect()
56-
57-
// Calculate intrinsic content size once at start
58-
const originalWidth = nodeElement.style.width
59-
const originalHeight = nodeElement.style.height
60-
nodeElement.style.width = 'auto'
61-
nodeElement.style.height = 'auto'
62-
63-
const intrinsicRect = nodeElement.getBoundingClientRect()
64-
65-
// Restore original size
66-
nodeElement.style.width = originalWidth
67-
nodeElement.style.height = originalHeight
68-
69-
// Convert to canvas coordinates using transform state
7057
const scale = transformState.camera.z
58+
59+
// Calculate current size in canvas coordinates
7160
resizeStartSize.value = {
7261
width: rect.width / scale,
7362
height: rect.height / scale
7463
}
75-
intrinsicMinSize.value = {
76-
width: intrinsicRect.width / scale,
77-
height: intrinsicRect.height / scale
78-
}
64+
65+
// Calculate intrinsic content size (minimum based on content)
66+
intrinsicMinSize.value = calculateIntrinsicSize(nodeElement, scale)
7967

8068
const handlePointerMove = (moveEvent: PointerEvent) => {
8169
if (

src/renderer/extensions/vueNodes/layout/useNodeLayout.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,6 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
167167
mutations.moveNode(nodeId, position)
168168
}
169169

170-
/**
171-
* Update node size
172-
*/
173-
function resize(newSize: { width: number; height: number }) {
174-
mutations.setSource(LayoutSource.Vue)
175-
mutations.resizeNode(nodeId, newSize)
176-
}
177-
178170
return {
179171
// Reactive state (via customRef)
180172
layoutRef,
@@ -187,7 +179,6 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
187179

188180
// Mutations
189181
moveTo,
190-
resize,
191182

192183
// Drag handlers
193184
startDrag,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2+
3+
import { calculateIntrinsicSize } from './calculateIntrinsicSize'
4+
5+
describe('calculateIntrinsicSize', () => {
6+
let element: HTMLElement
7+
8+
beforeEach(() => {
9+
// Create a test element
10+
element = document.createElement('div')
11+
element.style.width = '200px'
12+
element.style.height = '100px'
13+
document.body.appendChild(element)
14+
})
15+
16+
afterEach(() => {
17+
document.body.removeChild(element)
18+
})
19+
20+
it('should calculate intrinsic size and convert to canvas coordinates', () => {
21+
// Mock getBoundingClientRect to return specific dimensions
22+
const originalGetBoundingClientRect = element.getBoundingClientRect
23+
element.getBoundingClientRect = () => ({
24+
width: 300,
25+
height: 150,
26+
top: 0,
27+
left: 0,
28+
bottom: 150,
29+
right: 300,
30+
x: 0,
31+
y: 0,
32+
toJSON: () => ({})
33+
})
34+
35+
const scale = 2
36+
const result = calculateIntrinsicSize(element, scale)
37+
38+
// Should divide by scale to convert from screen to canvas coordinates
39+
expect(result).toEqual({
40+
width: 150, // 300 / 2
41+
height: 75 // 150 / 2
42+
})
43+
44+
element.getBoundingClientRect = originalGetBoundingClientRect
45+
})
46+
47+
it('should restore original size after measuring', () => {
48+
const originalWidth = element.style.width
49+
const originalHeight = element.style.height
50+
51+
element.getBoundingClientRect = () => ({
52+
width: 300,
53+
height: 150,
54+
top: 0,
55+
left: 0,
56+
bottom: 150,
57+
right: 300,
58+
x: 0,
59+
y: 0,
60+
toJSON: () => ({})
61+
})
62+
63+
calculateIntrinsicSize(element, 1)
64+
65+
// Should restore original styles
66+
expect(element.style.width).toBe(originalWidth)
67+
expect(element.style.height).toBe(originalHeight)
68+
})
69+
70+
it('should handle scale of 1 correctly', () => {
71+
element.getBoundingClientRect = () => ({
72+
width: 400,
73+
height: 200,
74+
top: 0,
75+
left: 0,
76+
bottom: 200,
77+
right: 400,
78+
x: 0,
79+
y: 0,
80+
toJSON: () => ({})
81+
})
82+
83+
const result = calculateIntrinsicSize(element, 1)
84+
85+
expect(result).toEqual({
86+
width: 400,
87+
height: 200
88+
})
89+
})
90+
91+
it('should handle fractional scales', () => {
92+
element.getBoundingClientRect = () => ({
93+
width: 300,
94+
height: 150,
95+
top: 0,
96+
left: 0,
97+
bottom: 150,
98+
right: 300,
99+
x: 0,
100+
y: 0,
101+
toJSON: () => ({})
102+
})
103+
104+
const result = calculateIntrinsicSize(element, 0.5)
105+
106+
expect(result).toEqual({
107+
width: 600, // 300 / 0.5
108+
height: 300 // 150 / 0.5
109+
})
110+
})
111+
112+
it('should temporarily set width and height to auto during measurement', () => {
113+
let widthDuringMeasurement = ''
114+
let heightDuringMeasurement = ''
115+
116+
element.getBoundingClientRect = function (this: HTMLElement) {
117+
widthDuringMeasurement = this.style.width
118+
heightDuringMeasurement = this.style.height
119+
return {
120+
width: 300,
121+
height: 150,
122+
top: 0,
123+
left: 0,
124+
bottom: 150,
125+
right: 300,
126+
x: 0,
127+
y: 0,
128+
toJSON: () => ({})
129+
}
130+
}
131+
132+
calculateIntrinsicSize(element, 1)
133+
134+
// During measurement, styles should be set to 'auto'
135+
expect(widthDuringMeasurement).toBe('auto')
136+
expect(heightDuringMeasurement).toBe('auto')
137+
})
138+
})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Calculate the intrinsic (minimum content-based) size of a node element
3+
*
4+
* Temporarily sets the element to auto-size to measure its natural content dimensions,
5+
* then converts from screen coordinates to canvas coordinates using the camera scale.
6+
*
7+
* @param element - The node element to measure
8+
* @param scale - Camera zoom scale for coordinate conversion
9+
* @returns The intrinsic minimum size in canvas coordinates
10+
*/
11+
export function calculateIntrinsicSize(
12+
element: HTMLElement,
13+
scale: number
14+
): { width: number; height: number } {
15+
// Store original size to restore later
16+
const originalWidth = element.style.width
17+
const originalHeight = element.style.height
18+
19+
// Temporarily set to auto to measure natural content size
20+
element.style.width = 'auto'
21+
element.style.height = 'auto'
22+
23+
const intrinsicRect = element.getBoundingClientRect()
24+
25+
// Restore original size
26+
element.style.width = originalWidth
27+
element.style.height = originalHeight
28+
29+
// Convert from screen coordinates to canvas coordinates
30+
return {
31+
width: intrinsicRect.width / scale,
32+
height: intrinsicRect.height / scale
33+
}
34+
}

0 commit comments

Comments
 (0)