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;
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);