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) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-30 18:03:04 +04:00
parent b68622a63a
commit 67cbf5c6ea

View File

@ -38,6 +38,7 @@ interface DragState {
ghost?: HTMLDivElement; ghost?: HTMLDivElement;
multiOffsets?: { id: number; offsetX: number; offsetY: number }[]; multiOffsets?: { id: number; offsetX: number; offsetY: number }[];
dragActivated?: boolean; // true once mouse moves past threshold 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 waypointIndex?: number; // which EPC waypoint is being dragged
deviceLabel?: string; // label to assign when dropping from device dock 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) // Right box corners (oriented along last segment)
const last = wps[wps.length - 1]; const last = wps[wps.length - 1];
const prev = wps[wps.length - 2]; const prev = wps[wps.length - 2];
const rbDx = last.x - prev.x, rbDy = last.y - prev.y; const rbDir = { x: last.x - prev.x, y: last.y - prev.y };
const rbCorners = orientedBoxCorners(last.x, last.y, rbDy, -rbDx, EPC_CONFIG.rightBox.w, EPC_CONFIG.rightBox.h, 'right'); 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) { for (const [bx, by] of rbCorners) {
minX = Math.min(minX, bx); minY = Math.min(minY, by); minX = Math.min(minX, bx); minY = Math.min(minY, by);
maxX = Math.max(maxX, bx); maxY = Math.max(maxY, 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; if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return;
dragState.dragActivated = true; 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 (e.shiftKey) {
if (Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!)) { if (!dragState.shiftAxis) {
pos.y = dragState.startY!; dragState.shiftAxis = Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!) ? 'h' : 'v';
} else {
pos.x = dragState.startX!;
} }
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); const sym = layout.symbols.find(s => s.id === dragState!.placedId);
if (!sym) return; if (!sym) return;
@ -535,13 +538,15 @@ function onMousemove(e: MouseEvent) {
if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return; if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return;
dragState.dragActivated = true; dragState.dragActivated = true;
} }
// Shift: constrain to orthogonal axis // Shift: constrain to orthogonal axis — lock once, don't re-evaluate
if (e.shiftKey) { if (e.shiftKey) {
if (Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!)) { if (!dragState.shiftAxis) {
pos.y = dragState.startY!; dragState.shiftAxis = Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!) ? 'h' : 'v';
} else {
pos.x = dragState.startX!;
} }
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) { for (const { id, offsetX, offsetY } of dragState.multiOffsets) {
const sym = layout.symbols.find(s => s.id === id); const sym = layout.symbols.find(s => s.id === id);