mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 22:34:15 +00:00
feat(vueNodes): support resizing from all four corners (#8845)
## Summary (Not sure we need this, and I don't know the reason why we only have one cornor support previously, but it is requested by QA reporting in Notion) Add resize handles at all four corners (NW, NE, SW, SE) of Vue nodes, matching litegraph's multi-corner resize behavior. Vue nodes previously only supported resizing from the bottom-right (SE) corner. This adds handles at all four corners with direction-aware delta math, snap-to-grid support, and minimum size enforcement that keeps the opposite corner anchored. The content-driven minimum height is measured from the DOM at resize start to prevent the node from sliding when dragged past its minimum size. ## Screenshots (if applicable) https://github.com/user-attachments/assets/c9d30d93-8243-4c44-a417-5ca6e9978b3b
This commit is contained in:
@@ -162,33 +162,40 @@
|
||||
</template>
|
||||
</button>
|
||||
</Button>
|
||||
<!-- Resize handle (bottom-right only) -->
|
||||
<div
|
||||
v-if="!isCollapsed && nodeData.resizable !== false"
|
||||
role="button"
|
||||
:aria-label="t('g.resizeFromBottomRight')"
|
||||
:class="
|
||||
cn(
|
||||
baseResizeHandleClasses,
|
||||
'-right-1 -bottom-1 cursor-se-resize group-hover/node:opacity-100'
|
||||
)
|
||||
"
|
||||
@pointerdown.stop="handleResizePointerDown"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 12"
|
||||
class="w-2/5 h-2/5 top-1 left-1 absolute"
|
||||
<template v-if="!isCollapsed && nodeData.resizable !== false">
|
||||
<div
|
||||
v-for="handle in RESIZE_HANDLES"
|
||||
:key="handle.corner"
|
||||
role="button"
|
||||
:aria-label="t(handle.i18nKey)"
|
||||
:class="
|
||||
cn(
|
||||
baseResizeHandleClasses,
|
||||
handle.positionClasses,
|
||||
handle.cursorClass,
|
||||
'group-hover/node:opacity-100'
|
||||
)
|
||||
"
|
||||
@pointerdown.stop="handleResizePointerDown($event, handle.corner)"
|
||||
>
|
||||
<path
|
||||
d="M11 1L1 11M11 6L6 11"
|
||||
stroke="var(--color-muted-foreground)"
|
||||
stroke-width="0.975"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 12"
|
||||
:class="cn('w-2/5 h-2/5 absolute', handle.svgPositionClasses)"
|
||||
:style="
|
||||
handle.svgTransform ? { transform: handle.svgTransform } : undefined
|
||||
"
|
||||
>
|
||||
<path
|
||||
d="M11 1L1 11M11 6L6 11"
|
||||
stroke="var(--color-muted-foreground)"
|
||||
stroke-width="0.975"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -249,6 +256,10 @@ import {
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import type { CompassCorners } from '@/lib/litegraph/src/interfaces'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
|
||||
import { RESIZE_HANDLES } from '../interactions/resize/resizeHandleConfig'
|
||||
import { useNodeResize } from '../interactions/resize/useNodeResize'
|
||||
import LivePreview from './LivePreview.vue'
|
||||
import NodeContent from './NodeContent.vue'
|
||||
@@ -423,6 +434,7 @@ const baseResizeHandleClasses =
|
||||
'absolute h-5 w-5 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
|
||||
|
||||
const MIN_NODE_WIDTH = 225
|
||||
const mutations = useLayoutMutations()
|
||||
|
||||
const { startResize } = useNodeResize((result, element) => {
|
||||
if (isCollapsed.value) return
|
||||
@@ -433,14 +445,23 @@ const { startResize } = useNodeResize((result, element) => {
|
||||
// Apply size directly to DOM element - ResizeObserver will pick this up
|
||||
element.style.setProperty('--node-width', `${clampedWidth}px`)
|
||||
element.style.setProperty('--node-height', `${result.size.height}px`)
|
||||
|
||||
// Update position for non-SE corner resizing
|
||||
if (result.position) {
|
||||
mutations.setSource(LayoutSource.Vue)
|
||||
mutations.moveNode(nodeData.id, result.position)
|
||||
}
|
||||
})
|
||||
|
||||
const handleResizePointerDown = (event: PointerEvent) => {
|
||||
const handleResizePointerDown = (
|
||||
event: PointerEvent,
|
||||
corner: CompassCorners
|
||||
) => {
|
||||
if (event.button !== 0) return
|
||||
if (!shouldHandleNodePointerEvents.value) return
|
||||
if (nodeData.flags?.pinned) return
|
||||
if (nodeData.resizable === false) return
|
||||
startResize(event)
|
||||
startResize(event, corner)
|
||||
}
|
||||
|
||||
watch(isCollapsed, (collapsed) => {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { CompassCorners } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
interface ResizeHandle {
|
||||
corner: CompassCorners
|
||||
positionClasses: string
|
||||
cursorClass: string
|
||||
i18nKey: string
|
||||
svgPositionClasses: string
|
||||
svgTransform: string
|
||||
}
|
||||
|
||||
export const RESIZE_HANDLES: ResizeHandle[] = [
|
||||
{
|
||||
corner: 'SE',
|
||||
positionClasses: '-right-1 -bottom-1',
|
||||
cursorClass: 'cursor-se-resize',
|
||||
i18nKey: 'g.resizeFromBottomRight',
|
||||
svgPositionClasses: 'top-1 left-1',
|
||||
svgTransform: ''
|
||||
},
|
||||
{
|
||||
corner: 'NE',
|
||||
positionClasses: '-right-1 -top-1',
|
||||
cursorClass: 'cursor-ne-resize',
|
||||
i18nKey: 'g.resizeFromTopRight',
|
||||
svgPositionClasses: 'bottom-1 left-1',
|
||||
svgTransform: 'scaleY(-1)'
|
||||
},
|
||||
{
|
||||
corner: 'SW',
|
||||
positionClasses: '-left-1 -bottom-1',
|
||||
cursorClass: 'cursor-sw-resize',
|
||||
i18nKey: 'g.resizeFromBottomLeft',
|
||||
svgPositionClasses: 'top-1 right-1',
|
||||
svgTransform: 'scaleX(-1)'
|
||||
},
|
||||
{
|
||||
corner: 'NW',
|
||||
positionClasses: '-left-1 -top-1',
|
||||
cursorClass: 'cursor-nw-resize',
|
||||
i18nKey: 'g.resizeFromTopLeft',
|
||||
svgPositionClasses: 'bottom-1 right-1',
|
||||
svgTransform: 'scale(-1, -1)'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,273 @@
|
||||
import type { MockInstance } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { CompassCorners } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
import type { ResizeCallbackPayload } from './useNodeResize'
|
||||
|
||||
type ResizeCallback = (
|
||||
payload: ResizeCallbackPayload,
|
||||
element: HTMLElement
|
||||
) => void
|
||||
|
||||
// Capture pointermove/pointerup handlers registered via useEventListener
|
||||
const eventHandlers = vi.hoisted(() => ({
|
||||
pointermove: null as ((e: PointerEvent) => void) | null,
|
||||
pointerup: null as ((e: PointerEvent) => void) | null
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useEventListener: vi.fn(
|
||||
(eventName: string, handler: (...args: unknown[]) => void) => {
|
||||
if (eventName === 'pointermove' || eventName === 'pointerup') {
|
||||
eventHandlers[eventName] = handler as (e: PointerEvent) => void
|
||||
}
|
||||
return vi.fn()
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/layout/transform/useTransformState', () => ({
|
||||
useTransformState: () => ({
|
||||
camera: { x: 0, y: 0, z: 1 }
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/extensions/vueNodes/composables/useNodeSnap', () => ({
|
||||
useNodeSnap: () => ({
|
||||
shouldSnap: vi.fn(() => false),
|
||||
applySnapToPosition: vi.fn((pos: { x: number; y: number }) => pos),
|
||||
applySnapToSize: vi.fn((size: { width: number; height: number }) => size)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/extensions/vueNodes/composables/useShiftKeySync', () => ({
|
||||
useShiftKeySync: () => ({
|
||||
trackShiftKey: vi.fn(() => vi.fn())
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
|
||||
layoutStore: {
|
||||
isResizingVueNodes: { value: false },
|
||||
getNodeLayoutRef: vi.fn(() => ({
|
||||
value: {
|
||||
position: { x: 100, y: 200 },
|
||||
size: { width: 300, height: 400 }
|
||||
}
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
function createMockNodeElement(
|
||||
width = 300,
|
||||
height = 400,
|
||||
minContentHeight = 150
|
||||
): HTMLElement {
|
||||
const element = document.createElement('div')
|
||||
element.setAttribute('data-node-id', 'test-node')
|
||||
element.style.setProperty('min-width', '225px')
|
||||
element.getBoundingClientRect = () => {
|
||||
// When --node-height is '0px', return the content-driven minimum height
|
||||
const nodeHeight = element.style.getPropertyValue('--node-height')
|
||||
const h = nodeHeight === '0px' ? minContentHeight : height
|
||||
return {
|
||||
width,
|
||||
height: h,
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: width,
|
||||
bottom: h,
|
||||
toJSON: () => {}
|
||||
} as DOMRect
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
function createMockHandle(nodeElement: HTMLElement): HTMLElement {
|
||||
const handle = document.createElement('div')
|
||||
nodeElement.appendChild(handle)
|
||||
handle.setPointerCapture = vi.fn()
|
||||
handle.releasePointerCapture = vi.fn()
|
||||
return handle
|
||||
}
|
||||
|
||||
function createPointerEvent(
|
||||
type: string,
|
||||
overrides: Partial<PointerEvent> = {}
|
||||
): PointerEvent {
|
||||
return {
|
||||
type,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
pointerId: 1,
|
||||
shiftKey: false,
|
||||
currentTarget: null,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
...overrides
|
||||
} as unknown as PointerEvent
|
||||
}
|
||||
|
||||
function startResizeAt(
|
||||
startResize: (event: PointerEvent, corner: CompassCorners) => void,
|
||||
handle: HTMLElement,
|
||||
corner: CompassCorners,
|
||||
clientX = 500,
|
||||
clientY = 500
|
||||
) {
|
||||
const downEvent = createPointerEvent('pointerdown', {
|
||||
currentTarget: handle,
|
||||
clientX,
|
||||
clientY
|
||||
} as Partial<PointerEvent>)
|
||||
startResize(downEvent, corner)
|
||||
}
|
||||
|
||||
function simulateMove(
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
startX = 500,
|
||||
startY = 500
|
||||
) {
|
||||
const moveEvent = createPointerEvent('pointermove', {
|
||||
clientX: startX + deltaX,
|
||||
clientY: startY + deltaY
|
||||
})
|
||||
eventHandlers.pointermove?.(moveEvent)
|
||||
}
|
||||
|
||||
describe('useNodeResize', () => {
|
||||
let callback: ResizeCallback & MockInstance<ResizeCallback>
|
||||
let nodeElement: HTMLElement
|
||||
let handle: HTMLElement
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
eventHandlers.pointermove = null
|
||||
eventHandlers.pointerup = null
|
||||
|
||||
callback = vi.fn<ResizeCallback>()
|
||||
nodeElement = createMockNodeElement()
|
||||
handle = createMockHandle(nodeElement)
|
||||
|
||||
// Need fresh import after mocks are set up
|
||||
const { useNodeResize } = await import('./useNodeResize')
|
||||
const { startResize } = useNodeResize(callback)
|
||||
|
||||
// Store startResize for access in tests
|
||||
;(globalThis as Record<string, unknown>).__testStartResize = startResize
|
||||
})
|
||||
|
||||
function getStartResize() {
|
||||
return (globalThis as Record<string, unknown>).__testStartResize as (
|
||||
event: PointerEvent,
|
||||
corner: CompassCorners
|
||||
) => void
|
||||
}
|
||||
|
||||
describe('SE corner (default)', () => {
|
||||
it('increases size when dragging right and down', () => {
|
||||
startResizeAt(getStartResize(), handle, 'SE')
|
||||
simulateMove(50, 30)
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
size: { width: 350, height: 430 }
|
||||
}),
|
||||
nodeElement
|
||||
)
|
||||
})
|
||||
|
||||
it('does not include position in payload', () => {
|
||||
startResizeAt(getStartResize(), handle, 'SE')
|
||||
simulateMove(50, 30)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
expect(payload.position).toBeUndefined()
|
||||
})
|
||||
|
||||
it('clamps width to minimum', () => {
|
||||
startResizeAt(getStartResize(), handle, 'SE')
|
||||
simulateMove(-200, 0)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
expect(payload.size.width).toBe(225)
|
||||
})
|
||||
})
|
||||
|
||||
describe('NE corner', () => {
|
||||
it('increases width right, decreases height upward, shifts y position', () => {
|
||||
startResizeAt(getStartResize(), handle, 'NE')
|
||||
simulateMove(50, -30)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
expect(payload.size).toEqual({ width: 350, height: 430 })
|
||||
expect(payload.position).toEqual({ x: 100, y: 170 })
|
||||
})
|
||||
|
||||
it('clamps height to content minimum and fixes bottom edge', () => {
|
||||
startResizeAt(getStartResize(), handle, 'NE')
|
||||
simulateMove(0, 500)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
// minContentHeight = 150, so height clamps to 150
|
||||
expect(payload.size.height).toBe(150)
|
||||
// y = startY + startHeight - minContentHeight = 200 + 400 - 150 = 450
|
||||
expect(payload.position!.y).toBe(450)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SW corner', () => {
|
||||
it('decreases width leftward, increases height downward, shifts x position', () => {
|
||||
startResizeAt(getStartResize(), handle, 'SW')
|
||||
simulateMove(-50, 30)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
expect(payload.size).toEqual({ width: 350, height: 430 })
|
||||
expect(payload.position).toEqual({ x: 50, y: 200 })
|
||||
})
|
||||
|
||||
it('clamps width to minimum and fixes right edge', () => {
|
||||
startResizeAt(getStartResize(), handle, 'SW')
|
||||
simulateMove(200, 0)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
expect(payload.size.width).toBe(225)
|
||||
expect(payload.position!.x).toBe(175)
|
||||
})
|
||||
})
|
||||
|
||||
describe('NW corner', () => {
|
||||
it('decreases width leftward, decreases height upward, shifts both x and y', () => {
|
||||
startResizeAt(getStartResize(), handle, 'NW')
|
||||
simulateMove(-50, -30)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
expect(payload.size).toEqual({ width: 350, height: 430 })
|
||||
expect(payload.position).toEqual({ x: 50, y: 170 })
|
||||
})
|
||||
|
||||
it('clamps width to minimum and fixes right edge', () => {
|
||||
startResizeAt(getStartResize(), handle, 'NW')
|
||||
simulateMove(200, 0)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
expect(payload.size.width).toBe(225)
|
||||
expect(payload.position!.x).toBe(175)
|
||||
})
|
||||
|
||||
it('clamps height to content minimum and fixes bottom edge', () => {
|
||||
startResizeAt(getStartResize(), handle, 'NW')
|
||||
simulateMove(0, 500)
|
||||
|
||||
const payload: ResizeCallbackPayload = callback.mock.calls[0][0]
|
||||
// minContentHeight = 150, so height clamps to 150
|
||||
expect(payload.size.height).toBe(150)
|
||||
// y = startY + startHeight - minContentHeight = 200 + 400 - 150 = 450
|
||||
expect(payload.position!.y).toBe(450)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,21 +1,24 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { CompassCorners } from '@/lib/litegraph/src/interfaces'
|
||||
import type { Point, Size } from '@/renderer/core/layout/types'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useNodeSnap } from '@/renderer/extensions/vueNodes/composables/useNodeSnap'
|
||||
import { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useShiftKeySync'
|
||||
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
|
||||
|
||||
interface ResizeCallbackPayload {
|
||||
export interface ResizeCallbackPayload {
|
||||
size: Size
|
||||
position?: Point
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for node resizing functionality (bottom-right corner only)
|
||||
* Composable for node resizing functionality from any corner.
|
||||
*
|
||||
* Provides resize handle interaction that integrates with the layout system.
|
||||
* Handles pointer capture, coordinate calculations, and size constraints.
|
||||
* Handles pointer capture, coordinate calculations, size constraints,
|
||||
* and position adjustments for non-SE corners.
|
||||
*/
|
||||
export function useNodeResize(
|
||||
resizeCallback: (payload: ResizeCallbackPayload, element: HTMLElement) => void
|
||||
@@ -25,14 +28,16 @@ export function useNodeResize(
|
||||
const isResizing = ref(false)
|
||||
const resizeStartPointer = ref<Point | null>(null)
|
||||
const resizeStartSize = ref<Size | null>(null)
|
||||
const resizeStartPosition = ref<Point | null>(null)
|
||||
const resizeCorner = ref<CompassCorners>('SE')
|
||||
|
||||
// Snap-to-grid functionality
|
||||
const { shouldSnap, applySnapToSize } = useNodeSnap()
|
||||
const { shouldSnap, applySnapToPosition, applySnapToSize } = useNodeSnap()
|
||||
|
||||
// Shift key sync for LiteGraph canvas preview
|
||||
const { trackShiftKey } = useShiftKeySync()
|
||||
|
||||
const startResize = (event: PointerEvent) => {
|
||||
const startResize = (event: PointerEvent, corner: CompassCorners = 'SE') => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -42,6 +47,9 @@ export function useNodeResize(
|
||||
const nodeElement = target.closest('[data-node-id]')
|
||||
if (!(nodeElement instanceof HTMLElement)) return
|
||||
|
||||
const nodeId = nodeElement.dataset.nodeId
|
||||
if (!nodeId) return
|
||||
|
||||
const rect = nodeElement.getBoundingClientRect()
|
||||
const scale = transformState.camera.z
|
||||
|
||||
@@ -50,6 +58,16 @@ export function useNodeResize(
|
||||
height: rect.height / scale
|
||||
}
|
||||
|
||||
const savedNodeHeight = nodeElement.style.getPropertyValue('--node-height')
|
||||
nodeElement.style.setProperty('--node-height', '0px')
|
||||
const minContentHeight = nodeElement.getBoundingClientRect().height / scale
|
||||
nodeElement.style.setProperty('--node-height', savedNodeHeight || '')
|
||||
|
||||
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||
const startPosition: Point = nodeLayout
|
||||
? { ...nodeLayout.position }
|
||||
: { x: 0, y: 0 }
|
||||
|
||||
// Track shift key state and sync to canvas for snap preview
|
||||
const stopShiftSync = trackShiftKey(event)
|
||||
|
||||
@@ -61,12 +79,15 @@ export function useNodeResize(
|
||||
isResizing.value = true
|
||||
resizeStartPointer.value = { x: event.clientX, y: event.clientY }
|
||||
resizeStartSize.value = startSize
|
||||
resizeStartPosition.value = startPosition
|
||||
resizeCorner.value = corner
|
||||
|
||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
||||
if (
|
||||
!isResizing.value ||
|
||||
!resizeStartPointer.value ||
|
||||
!resizeStartSize.value
|
||||
!resizeStartSize.value ||
|
||||
!resizeStartPosition.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -77,19 +98,94 @@ export function useNodeResize(
|
||||
const deltaY =
|
||||
(moveEvent.clientY - resizeStartPointer.value.y) / (scale || 1)
|
||||
|
||||
let newSize: Size = {
|
||||
width: resizeStartSize.value.width + deltaX,
|
||||
height: resizeStartSize.value.height + deltaY
|
||||
const activeCorner = resizeCorner.value
|
||||
let newWidth: number
|
||||
let newHeight: number
|
||||
let newX = resizeStartPosition.value.x
|
||||
let newY = resizeStartPosition.value.y
|
||||
|
||||
switch (activeCorner) {
|
||||
case 'NE':
|
||||
newY = resizeStartPosition.value.y + deltaY
|
||||
newWidth = resizeStartSize.value.width + deltaX
|
||||
newHeight = resizeStartSize.value.height - deltaY
|
||||
break
|
||||
case 'SW':
|
||||
newX = resizeStartPosition.value.x + deltaX
|
||||
newWidth = resizeStartSize.value.width - deltaX
|
||||
newHeight = resizeStartSize.value.height + deltaY
|
||||
break
|
||||
case 'NW':
|
||||
newX = resizeStartPosition.value.x + deltaX
|
||||
newY = resizeStartPosition.value.y + deltaY
|
||||
newWidth = resizeStartSize.value.width - deltaX
|
||||
newHeight = resizeStartSize.value.height - deltaY
|
||||
break
|
||||
default: // SE
|
||||
newWidth = resizeStartSize.value.width + deltaX
|
||||
newHeight = resizeStartSize.value.height + deltaY
|
||||
break
|
||||
}
|
||||
|
||||
// Apply snap if shift is held
|
||||
// Apply snap-to-grid
|
||||
if (shouldSnap(moveEvent)) {
|
||||
newSize = applySnapToSize(newSize)
|
||||
// Snap position first for N/W corners, then compensate size
|
||||
if (activeCorner.includes('N') || activeCorner.includes('W')) {
|
||||
const originalX = newX
|
||||
const originalY = newY
|
||||
const snapped = applySnapToPosition({ x: newX, y: newY })
|
||||
newX = snapped.x
|
||||
newY = snapped.y
|
||||
|
||||
if (activeCorner.includes('N')) {
|
||||
newHeight += originalY - newY
|
||||
}
|
||||
if (activeCorner.includes('W')) {
|
||||
newWidth += originalX - newX
|
||||
}
|
||||
}
|
||||
|
||||
const snappedSize = applySnapToSize({
|
||||
width: newWidth,
|
||||
height: newHeight
|
||||
})
|
||||
newWidth = snappedSize.width
|
||||
newHeight = snappedSize.height
|
||||
}
|
||||
|
||||
const nodeElement = target.closest('[data-node-id]')
|
||||
if (nodeElement instanceof HTMLElement) {
|
||||
resizeCallback({ size: newSize }, nodeElement)
|
||||
// Enforce minimum size with position compensation (matching litegraph)
|
||||
const minWidth =
|
||||
parseFloat(nodeElement.style.getPropertyValue('min-width') || '0') ||
|
||||
225
|
||||
if (newWidth < minWidth) {
|
||||
if (activeCorner.includes('W')) {
|
||||
newX =
|
||||
resizeStartPosition.value.x + resizeStartSize.value.width - minWidth
|
||||
}
|
||||
newWidth = minWidth
|
||||
}
|
||||
if (newHeight < minContentHeight) {
|
||||
if (activeCorner.includes('N')) {
|
||||
newY =
|
||||
resizeStartPosition.value.y +
|
||||
resizeStartSize.value.height -
|
||||
minContentHeight
|
||||
}
|
||||
newHeight = minContentHeight
|
||||
}
|
||||
|
||||
const payload: ResizeCallbackPayload = {
|
||||
size: { width: newWidth, height: newHeight }
|
||||
}
|
||||
|
||||
// Only include position for non-SE corners
|
||||
if (activeCorner !== 'SE') {
|
||||
payload.position = { x: newX, y: newY }
|
||||
}
|
||||
|
||||
const targetNodeElement = target.closest('[data-node-id]')
|
||||
if (targetNodeElement instanceof HTMLElement) {
|
||||
resizeCallback(payload, targetNodeElement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +195,7 @@ export function useNodeResize(
|
||||
layoutStore.isResizingVueNodes.value = false
|
||||
resizeStartPointer.value = null
|
||||
resizeStartSize.value = null
|
||||
resizeStartPosition.value = null
|
||||
|
||||
// Stop tracking shift key state
|
||||
stopShiftSync()
|
||||
|
||||
Reference in New Issue
Block a user