From baa2e3dc814b05315bf8087aff0561c0531d51c5 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 29 Nov 2025 16:52:58 -0800 Subject: [PATCH] fix resizing from top --- .../vueNodes/components/LGraphNode.vue | 47 ++++++++++++++++--- .../composables/useNodePointerInteractions.ts | 17 ++++++- .../composables/useVueNodeResizeTracking.ts | 28 ++++++++--- .../interactions/resize/useNodeResize.ts | 15 +++++- 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 72f1e6c495a..d3376031c8d 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -352,22 +352,57 @@ const cornerResizeHandles: CornerResizeHandle[] = [ const MIN_NODE_WIDTH = 225 -const { startResize } = useNodeResize((result, element) => { +// Track the actual DOM size to detect when we've hit min size constraints +let lastActualHeight: number | null = null +let lastActualWidth: number | null = null + +const { startResize, isResizing } = useNodeResize((result, element) => { if (isCollapsed.value) return // Clamp width to minimum to avoid conflicts with CSS min-width const clampedWidth = Math.max(result.size.width, MIN_NODE_WIDTH) + const requestedHeight = result.size.height + + // Capture current actual size before applying (uses cached offsetWidth/Height, no layout thrash) + const prevActualWidth = element.offsetWidth + const prevActualHeight = element.offsetHeight - // Apply size directly to DOM element - ResizeObserver will pick this up + // Apply size directly to DOM element element.style.setProperty('--node-width', `${clampedWidth}px`) - element.style.setProperty('--node-height', `${result.size.height}px`) + element.style.setProperty('--node-height', `${requestedHeight}px`) + + // Check if actual size changed from last frame (not this frame - avoid layout thrash) + // If actual size stopped changing while we're still trying to shrink, we've hit the floor + const widthHitFloor = + lastActualWidth !== null && + Math.abs(prevActualWidth - lastActualWidth) < POSITION_EPSILON && + clampedWidth < prevActualWidth + + const heightHitFloor = + lastActualHeight !== null && + Math.abs(prevActualHeight - lastActualHeight) < POSITION_EPSILON && + requestedHeight < prevActualHeight + + lastActualWidth = prevActualWidth + lastActualHeight = prevActualHeight const currentPosition = position.value - const deltaX = Math.abs(result.position.x - currentPosition.x) - const deltaY = Math.abs(result.position.y - currentPosition.y) + const newX = widthHitFloor ? currentPosition.x : result.position.x + const newY = heightHitFloor ? currentPosition.y : result.position.y + + const deltaX = Math.abs(newX - currentPosition.x) + const deltaY = Math.abs(newY - currentPosition.y) if (deltaX > POSITION_EPSILON || deltaY > POSITION_EPSILON) { - moveNodeTo(result.position) + moveNodeTo({ x: newX, y: newY }) + } +}) + +// Reset tracking when resize ends +watch(isResizing, (resizing) => { + if (!resizing) { + lastActualWidth = null + lastActualHeight = null } }) diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts index 33369bc0643..ff953072453 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts @@ -26,7 +26,8 @@ export function useNodePointerInteractions( return true } - const startPosition = ref({ x: 0, y: 0 }) + // null means pointerdown hasn't happened yet on this node + const startPosition = ref<{ x: number; y: number } | null>(null) const DRAG_THRESHOLD = 3 // pixels @@ -60,6 +61,10 @@ export function useNodePointerInteractions( startDrag(event, nodeId) } + function clearStartPosition() { + startPosition.value = null + } + function onPointermove(event: PointerEvent) { if (forwardMiddlePointerIfNeeded(event)) return @@ -72,6 +77,13 @@ export function useNodePointerInteractions( const multiSelect = isMultiSelectKey(event) const lmbDown = event.buttons & 1 + + // If we don't have a start position, pointerdown was handled elsewhere (e.g., resize handle) + // Don't start dragging in this case + if (!startPosition.value) { + return + } + if (lmbDown && multiSelect && !layoutStore.isDraggingVueNodes.value) { layoutStore.isDraggingVueNodes.value = true handleNodeSelect(event, nodeId) @@ -122,6 +134,7 @@ export function useNodePointerInteractions( if (wasDragging) { safeDragEnd(event) + clearStartPosition() return } const multiSelect = isMultiSelectKey(event) @@ -130,6 +143,8 @@ export function useNodePointerInteractions( if (nodeId) { toggleNodeSelectionAfterPointerUp(nodeId, multiSelect) } + + clearStartPosition() } function onPointercancel(event: PointerEvent) { diff --git a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts index 4d4e274d3ca..348f14d3604 100644 --- a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts @@ -18,6 +18,9 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import type { Bounds, NodeId } from '@/renderer/core/layout/types' import { LayoutSource } from '@/renderer/core/layout/types' +// Set of node IDs currently being resized via handles (not ResizeObserver) +export const nodesBeingResized = new Set() + import { syncNodeSlotLayoutsFromDOM } from './useSlotElementTracking' /** @@ -110,17 +113,28 @@ const resizeObserver = new ResizeObserver((entries) => { height: Math.max(0, height) } + // If this entry is a node, mark it for slot layout resync (even during resize) + if (elementType === 'node' && elementId) { + nodesNeedingSlotResync.add(elementId) + } + + // For nodes being actively resized via handles, only update size (not position) + // The position is managed by the resize callback to avoid stale DOM reads overwriting it + if (elementType === 'node' && nodesBeingResized.has(elementId)) { + const currentLayout = layoutStore.getNodeLayoutRef(elementId).value + if (currentLayout) { + // Keep current position, only update size + bounds.x = currentLayout.position.x + bounds.y = currentLayout.position.y + } + } + let updates = updatesByType.get(elementType) if (!updates) { updates = [] updatesByType.set(elementType, updates) } updates.push({ id: elementId, bounds }) - - // If this entry is a node, mark it for slot layout resync - if (elementType === 'node' && elementId) { - nodesNeedingSlotResync.add(elementId) - } } layoutStore.setSource(LayoutSource.DOM) @@ -128,7 +142,9 @@ const resizeObserver = new ResizeObserver((entries) => { // Flush per-type for (const [type, updates] of updatesByType) { const config = trackingConfigs.get(type) - if (config && updates.length) config.updateHandler(updates) + if (config && updates.length) { + config.updateHandler(updates) + } } // After node bounds are updated, refresh slot cached offsets and layouts diff --git a/src/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts b/src/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts index 4d63e00ab2c..95a0c36099e 100644 --- a/src/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts +++ b/src/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts @@ -4,6 +4,7 @@ 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 { nodesBeingResized } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking' import type { ResizeHandleDirection } from './resizeMath' import { createResizeSession, toCanvasDelta } from './resizeMath' @@ -58,6 +59,11 @@ export function useNodeResize( const nodeElement = target.closest('[data-node-id]') if (!(nodeElement instanceof HTMLElement)) return + const nodeId = nodeElement.dataset.nodeId + if (nodeId) { + nodesBeingResized.add(nodeId) + } + const rect = nodeElement.getBoundingClientRect() const scale = transformState.camera.z @@ -74,6 +80,7 @@ export function useNodeResize( isResizing.value = true resizeStartPointer.value = { x: event.clientX, y: event.clientY } + resizeSession.value = createResizeSession({ startSize, startPosition: { ...startPosition }, @@ -85,8 +92,9 @@ export function useNodeResize( !isResizing.value || !resizeStartPointer.value || !resizeSession.value - ) + ) { return + } const startPointer = resizeStartPointer.value const session = resizeSession.value @@ -117,6 +125,11 @@ export function useNodeResize( // Stop tracking shift key state stopShiftSync() + // Allow ResizeObserver to update this node again + if (nodeId) { + nodesBeingResized.delete(nodeId) + } + target.releasePointerCapture(upEvent.pointerId) stopMoveListen() stopUpListen()