Fix shift-drag vertical movement, PE shape outline and hit-testing
- Fix shift-drag: require 8px movement before locking axis, use strict greater-than to avoid horizontal bias at small deltas - Add PE shape-following outline for selection/hover/collision highlights - Add PE shape-aware hit-testing (arrow + beam + receiver zones with margin) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
67cbf5c6ea
commit
09cafa4577
@ -1,6 +1,6 @@
|
||||
/** Pure hit-testing functions — no module state, no side effects */
|
||||
|
||||
import { isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, getSymbolGroup, EPC_CONFIG, INDUCTION_CONFIG, getCurveGeometry } from '../symbols.js';
|
||||
import { isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, isPhotoeyeType, getSymbolGroup, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG, getCurveGeometry } from '../symbols.js';
|
||||
import { layout } from '../stores/layout.svelte.js';
|
||||
import { THEME } from './render-theme.js';
|
||||
import type { PlacedSymbol } from '../types.js';
|
||||
@ -170,7 +170,7 @@ function pointInEpc(px: number, py: number, sym: PlacedSymbol): boolean {
|
||||
const last = waypoints[waypoints.length - 1];
|
||||
const prev = waypoints[waypoints.length - 2];
|
||||
const plx = ox + last.x, ply = oy + last.y;
|
||||
const angle = Math.atan2(ply - (oy + prev.y), plx - (ox + prev.x)) - Math.PI / 2;
|
||||
const angle = Math.atan2(ply - (oy + prev.y), plx - (ox + prev.x));
|
||||
const cos = Math.cos(-angle), sin = Math.sin(-angle);
|
||||
const dx = px - plx, dy = py - ply;
|
||||
const lx = dx * cos - dy * sin, ly = dx * sin + dy * cos;
|
||||
@ -193,12 +193,28 @@ function pointInEpc(px: number, py: number, sym: PlacedSymbol): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Check if a point is inside the photoeye shape (arrow + beam + receiver) with margin */
|
||||
function pointInPhotoeye(px: number, py: number, sym: PlacedSymbol): boolean {
|
||||
const { leftCap, rightCap } = PHOTOEYE_CONFIG;
|
||||
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
|
||||
const m = 4; // hit margin
|
||||
|
||||
// Arrow (left transmitter)
|
||||
if (px >= x - m && px <= x + leftCap + m && py >= y + h * 0.05 - m && py <= y + h * 0.948 + m) return true;
|
||||
// Beam (middle)
|
||||
if (px >= x + leftCap - m && px <= x + w - rightCap + m && py >= y + h * 0.42 - m && py <= y + h * 0.585 + m) return true;
|
||||
// Receiver (right cap)
|
||||
if (px >= x + w - rightCap - m && px <= x + w + m && py >= y + h * 0.181 - m && py <= y + h * 0.826 + m) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Hit test a single symbol against its actual shape */
|
||||
function pointInSymbol(px: number, py: number, sym: PlacedSymbol): boolean {
|
||||
if (isCurvedType(sym.symbolId)) return pointInArcBand(px, py, sym);
|
||||
if (isSpurType(sym.symbolId)) return pointInTrapezoid(px, py, sym);
|
||||
if (isInductionType(sym.symbolId)) return pointInInduction(px, py, sym);
|
||||
if (isEpcType(sym.symbolId)) return pointInEpc(px, py, sym);
|
||||
if (isPhotoeyeType(sym.symbolId)) return pointInPhotoeye(px, py, sym);
|
||||
return pointInRect(px, py, sym.x, sym.y, sym.w, sym.h);
|
||||
}
|
||||
|
||||
|
||||
@ -510,12 +510,16 @@ 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 — lock once, don't re-evaluate
|
||||
// Shift: constrain to orthogonal axis — lock after enough movement
|
||||
if (e.shiftKey) {
|
||||
if (!dragState.shiftAxis) {
|
||||
dragState.shiftAxis = Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!) ? 'h' : 'v';
|
||||
const dx = Math.abs(pos.x - dragState.startX!);
|
||||
const dy = Math.abs(pos.y - dragState.startY!);
|
||||
if (!dragState.shiftAxis && Math.max(dx, dy) >= 8) {
|
||||
dragState.shiftAxis = dx > dy ? 'h' : 'v';
|
||||
}
|
||||
if (dragState.shiftAxis === 'h') pos.y = dragState.startY!;
|
||||
else if (dragState.shiftAxis === 'v') pos.x = dragState.startX!;
|
||||
else if (dx > dy) pos.y = dragState.startY!;
|
||||
else pos.x = dragState.startX!;
|
||||
} else {
|
||||
dragState.shiftAxis = undefined;
|
||||
@ -538,12 +542,16 @@ 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 — lock once, don't re-evaluate
|
||||
// Shift: constrain to orthogonal axis — lock after enough movement
|
||||
if (e.shiftKey) {
|
||||
if (!dragState.shiftAxis) {
|
||||
dragState.shiftAxis = Math.abs(pos.x - dragState.startX!) >= Math.abs(pos.y - dragState.startY!) ? 'h' : 'v';
|
||||
const dx = Math.abs(pos.x - dragState.startX!);
|
||||
const dy = Math.abs(pos.y - dragState.startY!);
|
||||
if (!dragState.shiftAxis && Math.max(dx, dy) >= 8) {
|
||||
dragState.shiftAxis = dx > dy ? 'h' : 'v';
|
||||
}
|
||||
if (dragState.shiftAxis === 'h') pos.y = dragState.startY!;
|
||||
else if (dragState.shiftAxis === 'v') pos.x = dragState.startX!;
|
||||
else if (dx > dy) pos.y = dragState.startY!;
|
||||
else pos.x = dragState.startX!;
|
||||
} else {
|
||||
dragState.shiftAxis = undefined;
|
||||
|
||||
@ -296,6 +296,28 @@ function traceInductionPath(ctx: CanvasRenderingContext2D, sym: PlacedSymbol, pa
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
/** Trace photoeye outline path */
|
||||
function tracePhotoeyePath(ctx: CanvasRenderingContext2D, sym: PlacedSymbol, pad: number) {
|
||||
const { leftCap, rightCap } = PHOTOEYE_CONFIG;
|
||||
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
|
||||
const p = pad;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + leftCap + p, y + h * 0.42 - p);
|
||||
ctx.lineTo(x + leftCap + p, y + h * 0.248 - p);
|
||||
ctx.lineTo(x - p, y + h * 0.05 - p);
|
||||
ctx.lineTo(x - p, y + h * 0.948 + p);
|
||||
ctx.lineTo(x + leftCap + p, y + h * 0.744 + p);
|
||||
ctx.lineTo(x + leftCap + p, y + h * 0.585 + p);
|
||||
ctx.lineTo(x + w - rightCap - p, y + h * 0.585 + p);
|
||||
ctx.lineTo(x + w - rightCap - p, y + h * 0.826 + p);
|
||||
ctx.lineTo(x + w + p, y + h * 0.826 + p);
|
||||
ctx.lineTo(x + w + p, y + h * 0.181 - p);
|
||||
ctx.lineTo(x + w - rightCap - p, y + h * 0.181 - p);
|
||||
ctx.lineTo(x + w - rightCap - p, y + h * 0.42 - p);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
/** Stroke an outline around a symbol — uses arc path for curved, trapezoid for spur, EPC shape for EPC, induction shape for induction, rect for straight */
|
||||
function strokeOutline(ctx: CanvasRenderingContext2D, sym: PlacedSymbol, pad: number) {
|
||||
if (isCurvedType(sym.symbolId)) {
|
||||
@ -309,6 +331,9 @@ function strokeOutline(ctx: CanvasRenderingContext2D, sym: PlacedSymbol, pad: nu
|
||||
} else if (isInductionType(sym.symbolId)) {
|
||||
traceInductionPath(ctx, sym, pad);
|
||||
ctx.stroke();
|
||||
} else if (isPhotoeyeType(sym.symbolId)) {
|
||||
tracePhotoeyePath(ctx, sym, pad);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.strokeRect(sym.x - pad, sym.y - pad, sym.w + pad * 2, sym.h + pad * 2);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user