Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions src/renderer/extensions/vueNodes/components/LGraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -60,6 +61,10 @@ export function useNodePointerInteractions(
startDrag(event, nodeId)
}

function clearStartPosition() {
startPosition.value = null
}

function onPointermove(event: PointerEvent) {
if (forwardMiddlePointerIfNeeded(event)) return

Expand All @@ -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)
Expand Down Expand Up @@ -122,6 +134,7 @@ export function useNodePointerInteractions(

if (wasDragging) {
safeDragEnd(event)
clearStartPosition()
return
}
const multiSelect = isMultiSelectKey(event)
Expand All @@ -130,6 +143,8 @@ export function useNodePointerInteractions(
if (nodeId) {
toggleNodeSelectionAfterPointerUp(nodeId, multiSelect)
}

clearStartPosition()
}

function onPointercancel(event: PointerEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -110,25 +113,38 @@ 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)

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand All @@ -74,6 +80,7 @@ export function useNodeResize(

isResizing.value = true
resizeStartPointer.value = { x: event.clientX, y: event.clientY }

resizeSession.value = createResizeSession({
startSize,
startPosition: { ...startPosition },
Expand All @@ -85,8 +92,9 @@ export function useNodeResize(
!isResizing.value ||
!resizeStartPointer.value ||
!resizeSession.value
)
) {
return
}

const startPointer = resizeStartPointer.value
const session = resizeSession.value
Expand Down Expand Up @@ -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()
Expand Down
Loading