fix resizing from top

This commit is contained in:
bymyself
2025-11-29 16:52:58 -08:00
parent a055241e2e
commit baa2e3dc81
4 changed files with 93 additions and 14 deletions

View File

@@ -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
// Apply size directly to DOM element - ResizeObserver will pick this up
// 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
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
}
})

View File

@@ -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) {

View File

@@ -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<string>()
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

View File

@@ -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()