diff --git a/svelte-app/src/lib/canvas/hit-testing.ts b/svelte-app/src/lib/canvas/hit-testing.ts index 9efcb97..2dba00c 100644 --- a/svelte-app/src/lib/canvas/hit-testing.ts +++ b/svelte-app/src/lib/canvas/hit-testing.ts @@ -146,11 +146,59 @@ function pointInInduction(px: number, py: number, sym: PlacedSymbol): boolean { 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 */ 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); return pointInRect(px, py, sym.x, sym.y, sym.w, sym.h); }