Skip to content

Commit f14a6be

Browse files
christian-byrneDrJKLactions-user
authored
allow Vue nodes to be resized from all 4 corners (#6187)
## Summary Enables Vue nodes to resize from all four corners and consolidated the interaction pipeline. ## Changes - **What**: Added four-corner handles to `LGraphNode`, wired them through the refactored `useNodeResize` composable, and centralized the math/preset helpers under `interactions/resize/` with cleaner pure functions and lint-compliant markup. ## Review Focus Corner-to-corner resizing accuracy (position + size), pinned-node guard preventing resize start, and snap-to-grid behavior at varied zoom levels. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6187-allow-Vue-nodes-to-be-resized-from-all-4-corners-2936d73d365081c8bf14e944ab24c27f) by [Unito](https://www.unito.io) --------- Co-authored-by: DrJKL <DrJKL@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com>
1 parent aeabc24 commit f14a6be

7 files changed

Lines changed: 438 additions & 77 deletions

File tree

src/locales/en/main.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
"icon": "Icon",
6363
"color": "Color",
6464
"error": "Error",
65+
"resizeFromBottomRight": "Resize from bottom-right corner",
66+
"resizeFromTopRight": "Resize from top-right corner",
67+
"resizeFromBottomLeft": "Resize from bottom-left corner",
68+
"resizeFromTopLeft": "Resize from top-left corner",
6569
"info": "Node Info",
6670
"bookmark": "Save to Library",
6771
"moreOptions": "More Options",

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

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
:class="
1010
cn(
1111
'bg-node-component-surface',
12-
'lg-node absolute rounded-2xl touch-none flex flex-col',
12+
'lg-node absolute rounded-2xl touch-none flex flex-col group',
1313
'border-1 border-solid border-node-component-border',
1414
// hover (only when node should handle events)
1515
shouldHandleNodePointerEvents &&
@@ -107,19 +107,25 @@
107107
</div>
108108
</template>
109109

110-
<!-- Resize handle -->
111-
<div
112-
v-if="!isCollapsed"
113-
class="absolute right-0 bottom-0 h-3 w-3 cursor-se-resize opacity-0 transition-opacity duration-200 hover:bg-white hover:opacity-20"
114-
@pointerdown.stop="startResize"
115-
/>
110+
<!-- Resize handles -->
111+
<template v-if="!isCollapsed">
112+
<div
113+
v-for="handle in cornerResizeHandles"
114+
:key="handle.id"
115+
role="button"
116+
:aria-label="handle.ariaLabel"
117+
:class="cn(baseResizeHandleClasses, handle.classes)"
118+
@pointerdown.stop="handleResizePointerDown(handle.direction)($event)"
119+
/>
120+
</template>
116121
</div>
117122
</template>
118123

119124
<script setup lang="ts">
120125
import { whenever } from '@vueuse/core'
121126
import { storeToRefs } from 'pinia'
122127
import { computed, inject, onErrorCaptured, onMounted, ref } from 'vue'
128+
import { useI18n } from 'vue-i18n'
123129
124130
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
125131
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
@@ -146,7 +152,8 @@ import {
146152
} from '@/utils/graphTraversalUtil'
147153
import { cn } from '@/utils/tailwindUtil'
148154
149-
import { useNodeResize } from '../composables/useNodeResize'
155+
import type { ResizeHandleDirection } from '../interactions/resize/resizeMath'
156+
import { useNodeResize } from '../interactions/resize/useNodeResize'
150157
import { calculateIntrinsicSize } from '../utils/calculateIntrinsicSize'
151158
import LivePreview from './LivePreview.vue'
152159
import NodeContent from './NodeContent.vue'
@@ -164,6 +171,8 @@ interface LGraphNodeProps {
164171
165172
const { nodeData, error = null } = defineProps<LGraphNodeProps>()
166173
174+
const { t } = useI18n()
175+
167176
const {
168177
handleNodeCollapse,
169178
handleNodeTitleUpdate,
@@ -242,8 +251,7 @@ onErrorCaptured((error) => {
242251
return false // Prevent error propagation
243252
})
244253
245-
// Use layout system for node position and dragging
246-
const { position, size, zIndex } = useNodeLayout(() => nodeData.id)
254+
const { position, size, zIndex, moveNodeTo } = useNodeLayout(() => nodeData.id)
247255
const { pointerHandlers, isDragging, dragStyle } = useNodePointerInteractions(
248256
() => nodeData,
249257
handleNodeSelect
@@ -281,19 +289,73 @@ onMounted(() => {
281289
}
282290
})
283291
292+
const baseResizeHandleClasses =
293+
'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
294+
const POSITION_EPSILON = 0.01
295+
296+
type CornerResizeHandle = {
297+
id: string
298+
direction: ResizeHandleDirection
299+
classes: string
300+
ariaLabel: string
301+
}
302+
303+
const cornerResizeHandles: CornerResizeHandle[] = [
304+
{
305+
id: 'se',
306+
direction: { horizontal: 'right', vertical: 'bottom' },
307+
classes: 'right-0 bottom-0 cursor-se-resize',
308+
ariaLabel: t('g.resizeFromBottomRight')
309+
},
310+
{
311+
id: 'ne',
312+
direction: { horizontal: 'right', vertical: 'top' },
313+
classes: 'right-0 top-0 cursor-ne-resize',
314+
ariaLabel: t('g.resizeFromTopRight')
315+
},
316+
{
317+
id: 'sw',
318+
direction: { horizontal: 'left', vertical: 'bottom' },
319+
classes: 'left-0 bottom-0 cursor-sw-resize',
320+
ariaLabel: t('g.resizeFromBottomLeft')
321+
},
322+
{
323+
id: 'nw',
324+
direction: { horizontal: 'left', vertical: 'top' },
325+
classes: 'left-0 top-0 cursor-nw-resize',
326+
ariaLabel: t('g.resizeFromTopLeft')
327+
}
328+
]
329+
284330
const { startResize } = useNodeResize(
285-
(newSize, element) => {
286-
// Apply size directly to DOM element - ResizeObserver will pick this up
331+
(result, element) => {
287332
if (isCollapsed.value) return
288333
289-
element.style.width = `${newSize.width}px`
290-
element.style.height = `${newSize.height}px`
334+
// Apply size directly to DOM element - ResizeObserver will pick this up
335+
element.style.width = `${result.size.width}px`
336+
element.style.height = `${result.size.height}px`
337+
338+
const currentPosition = position.value
339+
const deltaX = Math.abs(result.position.x - currentPosition.x)
340+
const deltaY = Math.abs(result.position.y - currentPosition.y)
341+
342+
if (deltaX > POSITION_EPSILON || deltaY > POSITION_EPSILON) {
343+
moveNodeTo(result.position)
344+
}
291345
},
292346
{
293347
transformState
294348
}
295349
)
296350
351+
const handleResizePointerDown = (direction: ResizeHandleDirection) => {
352+
return (event: PointerEvent) => {
353+
if (nodeData.flags?.pinned) return
354+
355+
startResize(event, direction, { ...position.value })
356+
}
357+
}
358+
297359
whenever(isCollapsed, () => {
298360
const element = nodeContainerRef.value
299361
if (!element) return
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import type { Point, Size } from '@/renderer/core/layout/types'
2+
3+
export type ResizeHandleDirection = {
4+
horizontal: 'left' | 'right'
5+
vertical: 'top' | 'bottom'
6+
}
7+
8+
function applyHandleDelta(
9+
startSize: Size,
10+
delta: Point,
11+
handle: ResizeHandleDirection
12+
): Size {
13+
const horizontalMultiplier = handle.horizontal === 'right' ? 1 : -1
14+
const verticalMultiplier = handle.vertical === 'bottom' ? 1 : -1
15+
16+
return {
17+
width: startSize.width + delta.x * horizontalMultiplier,
18+
height: startSize.height + delta.y * verticalMultiplier
19+
}
20+
}
21+
22+
function clampToMinSize(size: Size, minSize: Size): Size {
23+
return {
24+
width: Math.max(size.width, minSize.width),
25+
height: Math.max(size.height, minSize.height)
26+
}
27+
}
28+
29+
function snapSize(
30+
size: Size,
31+
minSize: Size,
32+
snapFn?: (size: Size) => Size
33+
): Size {
34+
if (!snapFn) return size
35+
const snapped = snapFn(size)
36+
return {
37+
width: Math.max(minSize.width, snapped.width),
38+
height: Math.max(minSize.height, snapped.height)
39+
}
40+
}
41+
42+
function computeAdjustedPosition(
43+
startPosition: Point,
44+
startSize: Size,
45+
nextSize: Size,
46+
handle: ResizeHandleDirection
47+
): Point {
48+
const widthDelta = startSize.width - nextSize.width
49+
const heightDelta = startSize.height - nextSize.height
50+
51+
return {
52+
x:
53+
handle.horizontal === 'left'
54+
? startPosition.x + widthDelta
55+
: startPosition.x,
56+
y:
57+
handle.vertical === 'top'
58+
? startPosition.y + heightDelta
59+
: startPosition.y
60+
}
61+
}
62+
63+
/**
64+
* Computes the resulting size and position of a node given pointer movement
65+
* and handle orientation.
66+
*/
67+
export function computeResizeOutcome({
68+
startSize,
69+
startPosition,
70+
delta,
71+
minSize,
72+
handle,
73+
snapFn
74+
}: {
75+
startSize: Size
76+
startPosition: Point
77+
delta: Point
78+
minSize: Size
79+
handle: ResizeHandleDirection
80+
snapFn?: (size: Size) => Size
81+
}): { size: Size; position: Point } {
82+
const resized = applyHandleDelta(startSize, delta, handle)
83+
const clamped = clampToMinSize(resized, minSize)
84+
const snapped = snapSize(clamped, minSize, snapFn)
85+
const position = computeAdjustedPosition(
86+
startPosition,
87+
startSize,
88+
snapped,
89+
handle
90+
)
91+
92+
return {
93+
size: snapped,
94+
position
95+
}
96+
}
97+
98+
export function createResizeSession(config: {
99+
startSize: Size
100+
startPosition: Point
101+
minSize: Size
102+
handle: ResizeHandleDirection
103+
}) {
104+
const startSize = { ...config.startSize }
105+
const startPosition = { ...config.startPosition }
106+
const minSize = { ...config.minSize }
107+
const handle = config.handle
108+
109+
return (delta: Point, snapFn?: (size: Size) => Size) =>
110+
computeResizeOutcome({
111+
startSize,
112+
startPosition,
113+
minSize,
114+
handle,
115+
delta,
116+
snapFn
117+
})
118+
}
119+
120+
export function toCanvasDelta(
121+
startPointer: Point,
122+
currentPointer: Point,
123+
scale: number
124+
): Point {
125+
const safeScale = scale === 0 ? 1 : scale
126+
return {
127+
x: (currentPointer.x - startPointer.x) / safeScale,
128+
y: (currentPointer.y - startPointer.y) / safeScale
129+
}
130+
}

0 commit comments

Comments
 (0)