From 67cbf5c6eacda0e0f2d32746323a704373de91d0 Mon Sep 17 00:00:00 2001 From: igurielidze Date: Mon, 30 Mar 2026 18:03:04 +0400 Subject: [PATCH] Fix shift-drag jitter: lock axis once instead of re-evaluating each frame The axis constraint was recalculated every mouse move, causing the symbol to jerk between horizontal and vertical near the 45-degree diagonal. Now the axis locks on first movement and stays locked until shift is released. Co-Authored-By: Claude Opus 4.6 (1M context) --- svelte-app/src/lib/canvas/interactions.ts | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/svelte-app/src/lib/canvas/interactions.ts b/svelte-app/src/lib/canvas/interactions.ts index 9d5ab85..b2c1fe1 100644 --- a/svelte-app/src/lib/canvas/interactions.ts +++ b/svelte-app/src/lib/canvas/interactions.ts @@ -38,6 +38,7 @@ interface DragState { ghost?: HTMLDivElement; multiOffsets?: { id: number; offsetX: number; offsetY: number }[]; dragActivated?: boolean; // true once mouse moves past threshold + shiftAxis?: 'h' | 'v'; // locked axis for shift-constrained drag waypointIndex?: number; // which EPC waypoint is being dragged deviceLabel?: string; // label to assign when dropping from device dock } @@ -360,8 +361,8 @@ function recalcEpcBounds(sym: typeof layout.symbols[0]) { // Right box corners (oriented along last segment) const last = wps[wps.length - 1]; const prev = wps[wps.length - 2]; - const rbDx = last.x - prev.x, rbDy = last.y - prev.y; - const rbCorners = orientedBoxCorners(last.x, last.y, rbDy, -rbDx, EPC_CONFIG.rightBox.w, EPC_CONFIG.rightBox.h, 'right'); + const rbDir = { x: last.x - prev.x, y: last.y - prev.y }; + const rbCorners = orientedBoxCorners(last.x, last.y, rbDir.x, rbDir.y, EPC_CONFIG.rightBox.w, EPC_CONFIG.rightBox.h, 'right'); for (const [bx, by] of rbCorners) { minX = Math.min(minX, bx); minY = Math.min(minY, by); maxX = Math.max(maxX, bx); maxY = Math.max(maxY, by); @@ -509,13 +510,15 @@ function onMousemove(e: MouseEvent) { if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return; dragState.dragActivated = true; } - // Shift: constrain to orthogonal axis (horizontal or vertical) + // Shift: constrain to orthogonal axis — lock once, don't re-evaluate if (e.shiftKey) { - if (Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!)) { - pos.y = dragState.startY!; - } else { - pos.x = dragState.startX!; + if (!dragState.shiftAxis) { + dragState.shiftAxis = Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!) ? 'h' : 'v'; } + if (dragState.shiftAxis === 'h') pos.y = dragState.startY!; + else pos.x = dragState.startX!; + } else { + dragState.shiftAxis = undefined; } const sym = layout.symbols.find(s => s.id === dragState!.placedId); if (!sym) return; @@ -535,13 +538,15 @@ function onMousemove(e: MouseEvent) { if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return; dragState.dragActivated = true; } - // Shift: constrain to orthogonal axis + // Shift: constrain to orthogonal axis — lock once, don't re-evaluate if (e.shiftKey) { - if (Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!)) { - pos.y = dragState.startY!; - } else { - pos.x = dragState.startX!; + if (!dragState.shiftAxis) { + dragState.shiftAxis = Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!) ? 'h' : 'v'; } + if (dragState.shiftAxis === 'h') pos.y = dragState.startY!; + else pos.x = dragState.startX!; + } else { + dragState.shiftAxis = undefined; } for (const { id, offsetX, offsetY } of dragState.multiOffsets) { const sym = layout.symbols.find(s => s.id === id);