Improve EPC clickability: shape-accurate hit testing with generous margin

Replace bounding-box hit test with proper shape-aware check that tests
proximity to line segments and oriented end boxes with 8px hit margin,
making the thin EPC much easier to select and drag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-30 15:27:30 +04:00
parent 82bb1b46c8
commit d721f47757

View File

@ -146,11 +146,59 @@ function pointInInduction(px: number, py: number, sym: PlacedSymbol): boolean {
return false; return false;
} }
/** Check if a point is near the EPC shape (line segments + end boxes) */
function pointInEpc(px: number, py: number, sym: PlacedSymbol): boolean {
const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints;
const hitMargin = 8; // generous click target around thin lines
const ox = sym.x, oy = sym.y;
// Check left box
const lb = EPC_CONFIG.leftBox;
if (waypoints.length >= 2) {
const p0x = ox + waypoints[0].x, p0y = oy + waypoints[0].y;
const p1x = ox + waypoints[1].x, p1y = oy + waypoints[1].y;
const angle = Math.atan2(p1y - p0y, p1x - p0x);
const cos = Math.cos(-angle), sin = Math.sin(-angle);
const dx = px - p0x, dy = py - p0y;
const lx = dx * cos - dy * sin, ly = dx * sin + dy * cos;
if (lx >= -lb.w - hitMargin && lx <= hitMargin && ly >= -lb.h / 2 - hitMargin && ly <= lb.h / 2 + hitMargin) return true;
}
// Check right box
const rb = EPC_CONFIG.rightBox;
if (waypoints.length >= 2) {
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));
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;
if (lx >= -rb.w - hitMargin && lx <= hitMargin && ly >= -rb.h / 2 - hitMargin && ly <= rb.h / 2 + hitMargin) return true;
}
// Check line segments
for (let i = 0; i < waypoints.length - 1; i++) {
const ax = ox + waypoints[i].x, ay = oy + waypoints[i].y;
const bx = ox + waypoints[i + 1].x, by = oy + waypoints[i + 1].y;
const sdx = bx - ax, sdy = by - ay;
const len2 = sdx * sdx + sdy * sdy;
if (len2 === 0) continue;
const t = Math.max(0, Math.min(1, ((px - ax) * sdx + (py - ay) * sdy) / len2));
const nx = ax + t * sdx, ny = ay + t * sdy;
const dist = Math.sqrt((px - nx) ** 2 + (py - ny) ** 2);
if (dist <= hitMargin) return true;
}
return false;
}
/** Hit test a single symbol against its actual shape */ /** Hit test a single symbol against its actual shape */
function pointInSymbol(px: number, py: number, sym: PlacedSymbol): boolean { function pointInSymbol(px: number, py: number, sym: PlacedSymbol): boolean {
if (isCurvedType(sym.symbolId)) return pointInArcBand(px, py, sym); if (isCurvedType(sym.symbolId)) return pointInArcBand(px, py, sym);
if (isSpurType(sym.symbolId)) return pointInTrapezoid(px, py, sym); if (isSpurType(sym.symbolId)) return pointInTrapezoid(px, py, sym);
if (isInductionType(sym.symbolId)) return pointInInduction(px, py, sym); if (isInductionType(sym.symbolId)) return pointInInduction(px, py, sym);
if (isEpcType(sym.symbolId)) return pointInEpc(px, py, sym);
return pointInRect(px, py, sym.x, sym.y, sym.w, sym.h); return pointInRect(px, py, sym.x, sym.y, sym.w, sym.h);
} }