Skip to content

Commit 44a66fd

Browse files
add vue node resizing
1 parent 2cb078c commit 44a66fd

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
@@ -113,6 +113,13 @@
113113
</div>
114114
</div>
115115
</template>
116+
117+
<!-- Resize handle -->
118+
<div
119+
v-if="!readonly"
120+
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"
121+
@pointerdown.stop="startResize"
122+
/>
116123
</div>
117124
</template>
118125

@@ -145,6 +152,7 @@ import {
145152
} from '@/utils/graphTraversalUtil'
146153
import { cn } from '@/utils/tailwindUtil'
147154
155+
import { useNodeResize } from '../composables/useNodeResize'
148156
import NodeContent from './NodeContent.vue'
149157
import NodeHeader from './NodeHeader.vue'
150158
import NodeSlots from './NodeSlots.vue'
@@ -173,6 +181,11 @@ const { selectedNodeIds } = storeToRefs(useCanvasStore())
173181
174182
// Inject transform state for coordinate conversion
175183
const transformState = inject(TransformStateKey)
184+
if (!transformState) {
185+
throw new Error(
186+
'TransformState must be provided for node resize functionality'
187+
)
188+
}
176189
177190
// Computed selection state - only this node re-evaluates when its selection changes
178191
const isSelected = computed(() => {
@@ -264,6 +277,19 @@ onMounted(() => {
264277
}
265278
})
266279
280+
const { startResize } = useNodeResize(
281+
(newSize, element) => {
282+
// Apply size directly to DOM element - ResizeObserver will pick this up
283+
if (!isCollapsed.value) {
284+
element.style.width = `${newSize.width}px`
285+
element.style.height = `${newSize.height}px`
286+
}
287+
},
288+
{
289+
transformState
290+
}
291+
)
292+
267293
// Track collapsed state
268294
const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)
269295
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)