mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 22:59:14 +00:00
## Summary
- Backport of #7063 to cloud/1.33
- Simplifies Vue node resize to bottom-right corner only
Cherry-picked from d76c59cb14
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7068-backport-cloud-1-33-Simplify-Vue-node-resize-to-bottom-right-corner-only-7063-2bc6d73d36508149bd85c3ae534387b6)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 150 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 142 KiB |
@@ -117,17 +117,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Resize handles -->
|
||||
<template v-if="!isCollapsed">
|
||||
<div
|
||||
v-for="handle in cornerResizeHandles"
|
||||
:key="handle.id"
|
||||
role="button"
|
||||
:aria-label="handle.ariaLabel"
|
||||
:class="cn(baseResizeHandleClasses, handle.classes)"
|
||||
@pointerdown.stop="handleResizePointerDown(handle.direction)($event)"
|
||||
/>
|
||||
</template>
|
||||
<!-- Resize handle (bottom-right only) -->
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
role="button"
|
||||
:aria-label="t('g.resizeFromBottomRight')"
|
||||
:class="cn(baseResizeHandleClasses, 'right-0 bottom-0 cursor-se-resize')"
|
||||
@pointerdown.stop="handleResizePointerDown"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -171,7 +168,6 @@ import {
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import type { ResizeHandleDirection } from '../interactions/resize/resizeMath'
|
||||
import { useNodeResize } from '../interactions/resize/useNodeResize'
|
||||
import LivePreview from './LivePreview.vue'
|
||||
import NodeContent from './NodeContent.vue'
|
||||
@@ -263,7 +259,7 @@ onErrorCaptured((error) => {
|
||||
return false // Prevent error propagation
|
||||
})
|
||||
|
||||
const { position, size, zIndex, moveNodeTo } = useNodeLayout(() => nodeData.id)
|
||||
const { position, size, zIndex } = useNodeLayout(() => nodeData.id)
|
||||
const { pointerHandlers } = useNodePointerInteractions(() => nodeData.id)
|
||||
const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers
|
||||
const { startDrag } = useNodeDrag()
|
||||
@@ -314,41 +310,6 @@ onMounted(() => {
|
||||
|
||||
const baseResizeHandleClasses =
|
||||
'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
|
||||
const POSITION_EPSILON = 0.01
|
||||
|
||||
type CornerResizeHandle = {
|
||||
id: string
|
||||
direction: ResizeHandleDirection
|
||||
classes: string
|
||||
ariaLabel: string
|
||||
}
|
||||
|
||||
const cornerResizeHandles: CornerResizeHandle[] = [
|
||||
{
|
||||
id: 'se',
|
||||
direction: { horizontal: 'right', vertical: 'bottom' },
|
||||
classes: 'right-0 bottom-0 cursor-se-resize',
|
||||
ariaLabel: t('g.resizeFromBottomRight')
|
||||
},
|
||||
{
|
||||
id: 'ne',
|
||||
direction: { horizontal: 'right', vertical: 'top' },
|
||||
classes: 'right-0 top-0 cursor-ne-resize',
|
||||
ariaLabel: t('g.resizeFromTopRight')
|
||||
},
|
||||
{
|
||||
id: 'sw',
|
||||
direction: { horizontal: 'left', vertical: 'bottom' },
|
||||
classes: 'left-0 bottom-0 cursor-sw-resize',
|
||||
ariaLabel: t('g.resizeFromBottomLeft')
|
||||
},
|
||||
{
|
||||
id: 'nw',
|
||||
direction: { horizontal: 'left', vertical: 'top' },
|
||||
classes: 'left-0 top-0 cursor-nw-resize',
|
||||
ariaLabel: t('g.resizeFromTopLeft')
|
||||
}
|
||||
]
|
||||
|
||||
const MIN_NODE_WIDTH = 225
|
||||
|
||||
@@ -361,22 +322,11 @@ 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`)
|
||||
|
||||
const currentPosition = position.value
|
||||
const deltaX = Math.abs(result.position.x - currentPosition.x)
|
||||
const deltaY = Math.abs(result.position.y - currentPosition.y)
|
||||
|
||||
if (deltaX > POSITION_EPSILON || deltaY > POSITION_EPSILON) {
|
||||
moveNodeTo(result.position)
|
||||
}
|
||||
})
|
||||
|
||||
const handleResizePointerDown = (direction: ResizeHandleDirection) => {
|
||||
return (event: PointerEvent) => {
|
||||
if (nodeData.flags?.pinned) return
|
||||
|
||||
startResize(event, direction, { ...position.value })
|
||||
}
|
||||
const handleResizePointerDown = (event: PointerEvent) => {
|
||||
if (nodeData.flags?.pinned) return
|
||||
startResize(event)
|
||||
}
|
||||
|
||||
watch(isCollapsed, (collapsed) => {
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import type { Point, Size } from '@/renderer/core/layout/types'
|
||||
|
||||
export type ResizeHandleDirection = {
|
||||
horizontal: 'left' | 'right'
|
||||
vertical: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
function applyHandleDelta(
|
||||
startSize: Size,
|
||||
delta: Point,
|
||||
handle: ResizeHandleDirection
|
||||
): Size {
|
||||
const horizontalMultiplier = handle.horizontal === 'right' ? 1 : -1
|
||||
const verticalMultiplier = handle.vertical === 'bottom' ? 1 : -1
|
||||
|
||||
return {
|
||||
width: startSize.width + delta.x * horizontalMultiplier,
|
||||
height: startSize.height + delta.y * verticalMultiplier
|
||||
}
|
||||
}
|
||||
|
||||
function computeAdjustedPosition(
|
||||
startPosition: Point,
|
||||
startSize: Size,
|
||||
nextSize: Size,
|
||||
handle: ResizeHandleDirection
|
||||
): Point {
|
||||
const widthDelta = startSize.width - nextSize.width
|
||||
const heightDelta = startSize.height - nextSize.height
|
||||
|
||||
return {
|
||||
x:
|
||||
handle.horizontal === 'left'
|
||||
? startPosition.x + widthDelta
|
||||
: startPosition.x,
|
||||
y:
|
||||
handle.vertical === 'top'
|
||||
? startPosition.y + heightDelta
|
||||
: startPosition.y
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the resulting size and position of a node given pointer movement
|
||||
* and handle orientation.
|
||||
*/
|
||||
export function computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta,
|
||||
handle,
|
||||
snapFn
|
||||
}: {
|
||||
startSize: Size
|
||||
startPosition: Point
|
||||
delta: Point
|
||||
handle: ResizeHandleDirection
|
||||
snapFn?: (size: Size) => Size
|
||||
}): { size: Size; position: Point } {
|
||||
const resized = applyHandleDelta(startSize, delta, handle)
|
||||
const snapped = snapFn?.(resized) ?? resized
|
||||
const position = computeAdjustedPosition(
|
||||
startPosition,
|
||||
startSize,
|
||||
snapped,
|
||||
handle
|
||||
)
|
||||
|
||||
return {
|
||||
size: snapped,
|
||||
position
|
||||
}
|
||||
}
|
||||
|
||||
export function createResizeSession(config: {
|
||||
startSize: Size
|
||||
startPosition: Point
|
||||
handle: ResizeHandleDirection
|
||||
}) {
|
||||
const startSize = { ...config.startSize }
|
||||
const startPosition = { ...config.startPosition }
|
||||
const handle = config.handle
|
||||
|
||||
return (delta: Point, snapFn?: (size: Size) => Size) =>
|
||||
computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
handle,
|
||||
delta,
|
||||
snapFn
|
||||
})
|
||||
}
|
||||
|
||||
export function toCanvasDelta(
|
||||
startPointer: Point,
|
||||
currentPointer: Point,
|
||||
scale: number
|
||||
): Point {
|
||||
const safeScale = scale === 0 ? 1 : scale
|
||||
return {
|
||||
x: (currentPointer.x - startPointer.x) / safeScale,
|
||||
y: (currentPointer.y - startPointer.y) / safeScale
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,14 @@ import { ref } from 'vue'
|
||||
import type { Point, Size } from '@/renderer/core/layout/types'
|
||||
import { useNodeSnap } from '@/renderer/extensions/vueNodes/composables/useNodeSnap'
|
||||
import { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useShiftKeySync'
|
||||
|
||||
import type { ResizeHandleDirection } from './resizeMath'
|
||||
import { createResizeSession, toCanvasDelta } from './resizeMath'
|
||||
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
|
||||
|
||||
interface ResizeCallbackPayload {
|
||||
size: Size
|
||||
position: Point
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for node resizing functionality
|
||||
* Composable for node resizing functionality (bottom-right corner only)
|
||||
*
|
||||
* Provides resize handle interaction that integrates with the layout system.
|
||||
* Handles pointer capture, coordinate calculations, and size constraints.
|
||||
@@ -27,16 +23,7 @@ export function useNodeResize(
|
||||
|
||||
const isResizing = ref(false)
|
||||
const resizeStartPointer = ref<Point | null>(null)
|
||||
const resizeSession = ref<
|
||||
| ((
|
||||
delta: Point,
|
||||
snapFn?: (size: Size) => Size
|
||||
) => {
|
||||
size: Size
|
||||
position: Point
|
||||
})
|
||||
| null
|
||||
>(null)
|
||||
const resizeStartSize = ref<Size | null>(null)
|
||||
|
||||
// Snap-to-grid functionality
|
||||
const { shouldSnap, applySnapToSize } = useNodeSnap()
|
||||
@@ -44,11 +31,7 @@ export function useNodeResize(
|
||||
// Shift key sync for LiteGraph canvas preview
|
||||
const { trackShiftKey } = useShiftKeySync()
|
||||
|
||||
const startResize = (
|
||||
event: PointerEvent,
|
||||
handle: ResizeHandleDirection,
|
||||
startPosition: Point
|
||||
) => {
|
||||
const startResize = (event: PointerEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -74,37 +57,36 @@ export function useNodeResize(
|
||||
|
||||
isResizing.value = true
|
||||
resizeStartPointer.value = { x: event.clientX, y: event.clientY }
|
||||
resizeSession.value = createResizeSession({
|
||||
startSize,
|
||||
startPosition: { ...startPosition },
|
||||
handle
|
||||
})
|
||||
resizeStartSize.value = startSize
|
||||
|
||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
||||
if (
|
||||
!isResizing.value ||
|
||||
!resizeStartPointer.value ||
|
||||
!resizeSession.value
|
||||
)
|
||||
!resizeStartSize.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const startPointer = resizeStartPointer.value
|
||||
const session = resizeSession.value
|
||||
const scale = transformState.camera.z
|
||||
const deltaX =
|
||||
(moveEvent.clientX - resizeStartPointer.value.x) / (scale || 1)
|
||||
const deltaY =
|
||||
(moveEvent.clientY - resizeStartPointer.value.y) / (scale || 1)
|
||||
|
||||
const delta = toCanvasDelta(
|
||||
startPointer,
|
||||
{ x: moveEvent.clientX, y: moveEvent.clientY },
|
||||
transformState.camera.z
|
||||
)
|
||||
let newSize: Size = {
|
||||
width: resizeStartSize.value.width + deltaX,
|
||||
height: resizeStartSize.value.height + deltaY
|
||||
}
|
||||
|
||||
// Apply snap if shift is held
|
||||
if (shouldSnap(moveEvent)) {
|
||||
newSize = applySnapToSize(newSize)
|
||||
}
|
||||
|
||||
const nodeElement = target.closest('[data-node-id]')
|
||||
if (nodeElement instanceof HTMLElement) {
|
||||
const outcome = session(
|
||||
delta,
|
||||
shouldSnap(moveEvent) ? applySnapToSize : undefined
|
||||
)
|
||||
|
||||
resizeCallback(outcome, nodeElement)
|
||||
resizeCallback({ size: newSize }, nodeElement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +94,7 @@ export function useNodeResize(
|
||||
if (isResizing.value) {
|
||||
isResizing.value = false
|
||||
resizeStartPointer.value = null
|
||||
resizeSession.value = null
|
||||
resizeStartSize.value = null
|
||||
|
||||
// Stop tracking shift key state
|
||||
stopShiftSync()
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
computeResizeOutcome,
|
||||
createResizeSession,
|
||||
toCanvasDelta
|
||||
} from '@/renderer/extensions/vueNodes/interactions/resize/resizeMath'
|
||||
|
||||
describe('nodeResizeMath', () => {
|
||||
const startSize = { width: 200, height: 120 }
|
||||
const startPosition = { x: 80, y: 160 }
|
||||
|
||||
it('computes resize from bottom-right corner without moving position', () => {
|
||||
const outcome = computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta: { x: 40, y: 20 },
|
||||
handle: { horizontal: 'right', vertical: 'bottom' }
|
||||
})
|
||||
|
||||
expect(outcome.size).toEqual({ width: 240, height: 140 })
|
||||
expect(outcome.position).toEqual(startPosition)
|
||||
})
|
||||
|
||||
it('computes resize from top-left corner adjusting position', () => {
|
||||
const outcome = computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta: { x: -30, y: -20 },
|
||||
handle: { horizontal: 'left', vertical: 'top' }
|
||||
})
|
||||
|
||||
expect(outcome.size).toEqual({ width: 230, height: 140 })
|
||||
expect(outcome.position).toEqual({ x: 50, y: 140 })
|
||||
})
|
||||
|
||||
it('supports reusable resize sessions with snapping', () => {
|
||||
const session = createResizeSession({
|
||||
startSize,
|
||||
startPosition,
|
||||
handle: { horizontal: 'right', vertical: 'bottom' }
|
||||
})
|
||||
|
||||
const snapFn = vi.fn((size: typeof startSize) => ({
|
||||
width: Math.round(size.width / 25) * 25,
|
||||
height: Math.round(size.height / 25) * 25
|
||||
}))
|
||||
|
||||
const applied = session({ x: 13, y: 17 }, snapFn)
|
||||
|
||||
expect(applied.size).toEqual({ width: 225, height: 125 })
|
||||
expect(applied.position).toEqual(startPosition)
|
||||
expect(snapFn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('converts screen delta to canvas delta using scale', () => {
|
||||
const delta = toCanvasDelta({ x: 50, y: 75 }, { x: 150, y: 135 }, 2)
|
||||
|
||||
expect(delta).toEqual({ x: 50, y: 30 })
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles zero scale by using fallback scale of 1', () => {
|
||||
const delta = toCanvasDelta({ x: 50, y: 75 }, { x: 150, y: 135 }, 0)
|
||||
|
||||
expect(delta).toEqual({ x: 100, y: 60 })
|
||||
})
|
||||
|
||||
it('handles negative deltas when resizing from right/bottom', () => {
|
||||
const outcome = computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta: { x: -50, y: -30 },
|
||||
handle: { horizontal: 'right', vertical: 'bottom' }
|
||||
})
|
||||
|
||||
expect(outcome.size).toEqual({ width: 150, height: 90 })
|
||||
expect(outcome.position).toEqual(startPosition)
|
||||
})
|
||||
|
||||
it('handles very large deltas without overflow', () => {
|
||||
const outcome = computeResizeOutcome({
|
||||
startSize,
|
||||
startPosition,
|
||||
delta: { x: 10000, y: 10000 },
|
||||
handle: { horizontal: 'right', vertical: 'bottom' }
|
||||
})
|
||||
|
||||
expect(outcome.size.width).toBe(10200)
|
||||
expect(outcome.size.height).toBe(10120)
|
||||
expect(outcome.position).toEqual(startPosition)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user