Skip to content

Commit 5932b7c

Browse files
add vue node resizing
1 parent ac72999 commit 5932b7c

File tree

4 files changed

+165
-2
lines changed

4 files changed

+165
-2
lines changed

src/renderer/core/layout/injectionKeys.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { useTransformState } from '@/renderer/core/layout/transform/useTran
2121
* const state = inject(TransformStateKey)!
2222
* const screen = state.canvasToScreen({ x: 100, y: 50 })
2323
*/
24-
interface TransformState
24+
export interface TransformState
2525
extends Pick<
2626
ReturnType<typeof useTransformState>,
2727
'screenToCanvas' | 'canvasToScreen' | 'camera' | 'isNodeInViewport'

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@
139139
</div>
140140
</div>
141141
</template>
142+
143+
<!-- Resize handle -->
144+
<div
145+
v-if="!readonly"
146+
class="absolute bottom-0 right-0 w-3 h-3 cursor-se-resize opacity-0 hover:opacity-20 hover:bg-white transition-opacity duration-200"
147+
@pointerdown.stop="startResize"
148+
/>
142149
</div>
143150
</template>
144151

@@ -171,6 +178,7 @@ import {
171178
} from '@/utils/graphTraversalUtil'
172179
import { cn } from '@/utils/tailwindUtil'
173180
181+
import { useNodeResize } from '../composables/useNodeResize'
174182
import NodeContent from './NodeContent.vue'
175183
import NodeHeader from './NodeHeader.vue'
176184
import NodeSlots from './NodeSlots.vue'
@@ -204,6 +212,11 @@ const { selectedNodeIds } = storeToRefs(useCanvasStore())
204212
205213
// Inject transform state for coordinate conversion
206214
const transformState = inject(TransformStateKey)
215+
if (!transformState) {
216+
throw new Error(
217+
'TransformState must be provided for node resize functionality'
218+
)
219+
}
207220
208221
// Computed selection state - only this node re-evaluates when its selection changes
209222
const isSelected = computed(() => {
@@ -294,6 +307,19 @@ onMounted(() => {
294307
}
295308
})
296309
310+
const { startResize } = useNodeResize(
311+
(newSize, element) => {
312+
// Apply size directly to DOM element - ResizeObserver will pick this up
313+
if (!isCollapsed.value) {
314+
element.style.width = `${newSize.width}px`
315+
element.style.height = `${newSize.height}px`
316+
}
317+
},
318+
{
319+
transformState
320+
}
321+
)
322+
297323
// Track collapsed state
298324
const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)
299325
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { useEventListener } from '@vueuse/core'
2+
import { ref } from 'vue'
3+
4+
import type { TransformState } from '@/renderer/core/layout/injectionKeys'
5+
6+
interface UseNodeResizeOptions {
7+
/** Transform state for coordinate conversion */
8+
transformState: TransformState
9+
/** Callback when resize ends */
10+
onEnd?: () => void
11+
}
12+
13+
/**
14+
* Composable for node resizing functionality
15+
*
16+
* Provides resize handle interaction that integrates with the layout system.
17+
* Handles pointer capture, coordinate calculations, and size constraints.
18+
*/
19+
export function useNodeResize(
20+
resizeCallback: (
21+
size: { width: number; height: number },
22+
element: HTMLElement
23+
) => void,
24+
options: UseNodeResizeOptions
25+
) {
26+
const { transformState } = options
27+
28+
const isResizing = ref(false)
29+
const resizeStartPos = ref<{ x: number; y: number } | null>(null)
30+
const resizeStartSize = ref<{ width: number; height: number } | null>(null)
31+
const intrinsicMinSize = ref<{ width: number; height: number } | null>(null)
32+
33+
const startResize = (event: PointerEvent) => {
34+
event.preventDefault()
35+
event.stopPropagation()
36+
37+
const target = event.currentTarget
38+
if (!(target instanceof HTMLElement)) return
39+
40+
// Capture pointer to ensure we get all move/up events
41+
target.setPointerCapture(event.pointerId)
42+
43+
isResizing.value = true
44+
resizeStartPos.value = { x: event.clientX, y: event.clientY }
45+
46+
// Get current node size from the DOM and calculate intrinsic min size
47+
const nodeElement = target.closest('[data-node-id]')
48+
if (!(nodeElement instanceof HTMLElement)) return
49+
50+
const rect = nodeElement.getBoundingClientRect()
51+
52+
// Calculate intrinsic content size once at start
53+
const originalWidth = nodeElement.style.width
54+
const originalHeight = nodeElement.style.height
55+
nodeElement.style.width = 'auto'
56+
nodeElement.style.height = 'auto'
57+
58+
const intrinsicRect = nodeElement.getBoundingClientRect()
59+
60+
// Restore original size
61+
nodeElement.style.width = originalWidth
62+
nodeElement.style.height = originalHeight
63+
64+
// Convert to canvas coordinates using transform state
65+
const scale = transformState.camera.z
66+
resizeStartSize.value = {
67+
width: rect.width / scale,
68+
height: rect.height / scale
69+
}
70+
intrinsicMinSize.value = {
71+
width: intrinsicRect.width / scale,
72+
height: intrinsicRect.height / scale
73+
}
74+
75+
const handlePointerMove = (moveEvent: PointerEvent) => {
76+
if (
77+
!isResizing.value ||
78+
!resizeStartPos.value ||
79+
!resizeStartSize.value ||
80+
!intrinsicMinSize.value
81+
)
82+
return
83+
84+
const dx = moveEvent.clientX - resizeStartPos.value.x
85+
const dy = moveEvent.clientY - resizeStartPos.value.y
86+
87+
// Apply scale factor from transform state
88+
const scale = transformState.camera.z
89+
const scaledDx = dx / scale
90+
const scaledDy = dy / scale
91+
92+
// Apply constraints: only minimum size based on content, no maximum
93+
const newWidth = Math.max(
94+
intrinsicMinSize.value.width,
95+
resizeStartSize.value.width + scaledDx
96+
)
97+
const newHeight = Math.max(
98+
intrinsicMinSize.value.height,
99+
resizeStartSize.value.height + scaledDy
100+
)
101+
102+
// Get the node element to apply size directly
103+
const nodeElement = target.closest('[data-node-id]')
104+
if (nodeElement instanceof HTMLElement) {
105+
resizeCallback({ width: newWidth, height: newHeight }, nodeElement)
106+
}
107+
}
108+
109+
const handlePointerUp = (upEvent: PointerEvent) => {
110+
if (isResizing.value) {
111+
isResizing.value = false
112+
resizeStartPos.value = null
113+
resizeStartSize.value = null
114+
intrinsicMinSize.value = null
115+
116+
target.releasePointerCapture(upEvent.pointerId)
117+
stopMoveListen()
118+
stopUpListen()
119+
options.onEnd?.()
120+
}
121+
}
122+
123+
const stopMoveListen = useEventListener('pointermove', handlePointerMove)
124+
const stopUpListen = useEventListener('pointerup', handlePointerUp)
125+
}
126+
127+
return {
128+
startResize,
129+
isResizing
130+
}
131+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useSharedCanvasPositionConversion } from '@/composables/element/useCanv
2020
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
2121
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
2222
import type { Bounds, NodeId } from '@/renderer/core/layout/types'
23+
import { LayoutSource } from '@/renderer/core/layout/types'
2324

2425
import { syncNodeSlotLayoutsFromDOM } from './useSlotElementTracking'
2526

@@ -124,10 +125,15 @@ const resizeObserver = new ResizeObserver((entries) => {
124125
}
125126
}
126127

128+
// Set source to Vue before processing DOM-driven updates
129+
layoutStore.setSource(LayoutSource.Vue)
130+
127131
// Flush per-type
128132
for (const [type, updates] of updatesByType) {
129133
const config = trackingConfigs.get(type)
130-
if (config && updates.length) config.updateHandler(updates)
134+
if (config && updates.length) {
135+
config.updateHandler(updates)
136+
}
131137
}
132138

133139
// After node bounds are updated, refresh slot cached offsets and layouts

0 commit comments

Comments
 (0)