Fix spur/induction collision polygons to account for mirrored state
Collision detection now mirrors spur trapezoid and induction arrow vertices when the symbol is mirrored, matching the visual appearance. Previously the collision polygon stayed unmirrored causing false positives/misaligned collision zones. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
31ea4c0908
commit
d09ffd4a22
@ -42,18 +42,29 @@ function obbOverlapWithSpacing(
|
||||
|
||||
// ─── Convex polygon helpers ───
|
||||
|
||||
/** Get the 4 vertices of a spur trapezoid in world space (with rotation) */
|
||||
/** Get the 4 vertices of a spur trapezoid in world space (with rotation and mirror) */
|
||||
function getSpurVertices(
|
||||
x: number, y: number, w: number, h: number, w2: number, rotation: number
|
||||
x: number, y: number, w: number, h: number, w2: number, rotation: number, mirrored?: boolean
|
||||
): [number, number][] {
|
||||
const cx = x + w / 2;
|
||||
const cy = y + h / 2;
|
||||
const local: [number, number][] = [
|
||||
let local: [number, number][];
|
||||
if (mirrored) {
|
||||
// Mirrored: angled edge on left side instead of right
|
||||
local = [
|
||||
[x + w - w2, y], // TL (narrow end, mirrored)
|
||||
[x + w, y], // TR
|
||||
[x + w, y + h], // BR
|
||||
[x, y + h], // BL (wide end, mirrored)
|
||||
];
|
||||
} else {
|
||||
local = [
|
||||
[x, y], // TL
|
||||
[x + w2, y], // TR (top base end)
|
||||
[x + w, y + h], // BR (bottom base end)
|
||||
[x, y + h], // BL
|
||||
];
|
||||
}
|
||||
if (!rotation) return local;
|
||||
const rad = rotation * Math.PI / 180;
|
||||
const cos = Math.cos(rad), sin = Math.sin(rad);
|
||||
@ -65,24 +76,38 @@ function getSpurVertices(
|
||||
|
||||
/** Get the convex hull vertices of an induction shape (arrow + strip) in world space */
|
||||
function getInductionVertices(
|
||||
x: number, y: number, w: number, h: number, rotation: number
|
||||
x: number, y: number, w: number, h: number, rotation: number, mirrored?: boolean
|
||||
): [number, number][] {
|
||||
const hw = INDUCTION_CONFIG.headWidth;
|
||||
const stripTopY = y + h * INDUCTION_CONFIG.stripTopFrac;
|
||||
const stripBottomY = y + h * INDUCTION_CONFIG.stripBottomFrac;
|
||||
const arrowPts = INDUCTION_CONFIG.arrowPoints;
|
||||
// Convex hull: top-right strip, arrow top, arrow left, arrow bottom, bottom-right strip
|
||||
const local: [number, number][] = [
|
||||
[x + w, stripTopY], // strip top-right
|
||||
[x + arrowPts[0][0] * hw, y + arrowPts[0][1] * h], // arrow top junction
|
||||
[x + arrowPts[1][0] * hw, y + arrowPts[1][1] * h], // arrow top
|
||||
[x + arrowPts[2][0] * hw, y + arrowPts[2][1] * h], // arrow left point
|
||||
[x + arrowPts[3][0] * hw, y + arrowPts[3][1] * h], // arrow bottom
|
||||
[x + arrowPts[5][0] * hw, y + arrowPts[5][1] * h], // arrow bottom junction
|
||||
[x + w, stripBottomY], // strip bottom-right
|
||||
const cx = x + w / 2;
|
||||
let local: [number, number][];
|
||||
if (mirrored) {
|
||||
// Mirror arrow x-coords around center
|
||||
local = [
|
||||
[x, stripTopY],
|
||||
[x + w - arrowPts[0][0] * hw, y + arrowPts[0][1] * h],
|
||||
[x + w - arrowPts[1][0] * hw, y + arrowPts[1][1] * h],
|
||||
[x + w - arrowPts[2][0] * hw, y + arrowPts[2][1] * h],
|
||||
[x + w - arrowPts[3][0] * hw, y + arrowPts[3][1] * h],
|
||||
[x + w - arrowPts[5][0] * hw, y + arrowPts[5][1] * h],
|
||||
[x, stripBottomY],
|
||||
];
|
||||
} else {
|
||||
local = [
|
||||
[x + w, stripTopY],
|
||||
[x + arrowPts[0][0] * hw, y + arrowPts[0][1] * h],
|
||||
[x + arrowPts[1][0] * hw, y + arrowPts[1][1] * h],
|
||||
[x + arrowPts[2][0] * hw, y + arrowPts[2][1] * h],
|
||||
[x + arrowPts[3][0] * hw, y + arrowPts[3][1] * h],
|
||||
[x + arrowPts[5][0] * hw, y + arrowPts[5][1] * h],
|
||||
[x, stripBottomY],
|
||||
];
|
||||
}
|
||||
if (!rotation) return local;
|
||||
const cx = x + w / 2, cy = y + h / 2;
|
||||
const cy = y + h / 2;
|
||||
const rad = rotation * Math.PI / 180;
|
||||
const cos = Math.cos(rad), sin = Math.sin(rad);
|
||||
return local.map(([px, py]) => {
|
||||
@ -437,16 +462,16 @@ function anyQuadVsCurved(
|
||||
/** Get collision vertices for any non-curved, non-EPC symbol based on its actual shape */
|
||||
function getShapeVertices(
|
||||
x: number, y: number, w: number, h: number, rotation: number,
|
||||
symbolId: string, w2?: number
|
||||
symbolId: string, w2?: number, mirrored?: boolean
|
||||
): [number, number][] {
|
||||
if (isSpurType(symbolId)) return getSpurVertices(x, y, w, h, w2 ?? w, rotation);
|
||||
if (isInductionType(symbolId)) return getInductionVertices(x, y, w, h, rotation);
|
||||
if (isSpurType(symbolId)) return getSpurVertices(x, y, w, h, w2 ?? w, rotation, mirrored);
|
||||
if (isInductionType(symbolId)) return getInductionVertices(x, y, w, h, rotation, mirrored);
|
||||
return getOBBVertices(x, y, w, h, rotation);
|
||||
}
|
||||
|
||||
export function checkSpacingViolation(
|
||||
id: number, x: number, y: number, w: number, h: number, rotation: number,
|
||||
symbolId?: string, curveAngle?: number, w2?: number
|
||||
symbolId?: string, curveAngle?: number, w2?: number, mirrored?: boolean
|
||||
): boolean {
|
||||
if (symbolId && SPACING_EXEMPT.has(symbolId)) return false;
|
||||
|
||||
@ -497,29 +522,29 @@ export function checkSpacingViolation(
|
||||
|
||||
// ── EPC vs non-curved/non-EPC ──
|
||||
if (isEpc && epcQuads && !symIsCurved && !symIsEpc) {
|
||||
const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2);
|
||||
const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2, sym.mirrored);
|
||||
if (anyQuadVsPolygon(epcQuads, bVerts, spacing)) return true;
|
||||
continue;
|
||||
}
|
||||
if (symIsEpc && symEpcQuads && !isCurved && !isEpc) {
|
||||
const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2);
|
||||
const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2, mirrored);
|
||||
if (anyQuadVsPolygon(symEpcQuads, aVerts, spacing)) return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Non-EPC, non-curved: shape polygon vs shape polygon ──
|
||||
if (!isCurved && !symIsCurved) {
|
||||
const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2);
|
||||
const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2);
|
||||
const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2, mirrored);
|
||||
const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2, sym.mirrored);
|
||||
if (polygonSATWithSpacing(aVerts, bVerts, spacing)) return true;
|
||||
} else if (isCurved && !symIsCurved) {
|
||||
const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2);
|
||||
const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2, sym.mirrored);
|
||||
if (checkCurvedVsSpur(
|
||||
{ x, y, w, h, rotation, symbolId: symbolId!, curveAngle },
|
||||
bVerts, spacing
|
||||
)) return true;
|
||||
} else if (!isCurved && symIsCurved) {
|
||||
const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2);
|
||||
const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2, mirrored);
|
||||
if (checkCurvedVsSpur(sym, aVerts, spacing)) return true;
|
||||
} else {
|
||||
// Both curved: use band AABBs as fallback
|
||||
|
||||
@ -21,11 +21,11 @@ export function snapToGrid(x: number, y: number, w?: number, h?: number, rotatio
|
||||
|
||||
export function findValidPosition(
|
||||
id: number, x: number, y: number, w: number, h: number,
|
||||
symbolId: string, rotation: number, curveAngle?: number, w2?: number
|
||||
symbolId: string, rotation: number, curveAngle?: number, w2?: number, mirrored?: boolean
|
||||
): { x: number; y: number } {
|
||||
const snapped = snapToGrid(x, y, w, h, rotation);
|
||||
let sx = snapped.x, sy = snapped.y;
|
||||
if (!checkSpacingViolation(id, sx, sy, w, h, rotation, symbolId, curveAngle, w2)) return { x: sx, y: sy };
|
||||
if (!checkSpacingViolation(id, sx, sy, w, h, rotation, symbolId, curveAngle, w2, mirrored)) return { x: sx, y: sy };
|
||||
|
||||
const step = layout.snapEnabled ? layout.gridSize : 1;
|
||||
const searchRadius = 20;
|
||||
@ -37,7 +37,7 @@ export function findValidPosition(
|
||||
const cy = sy + dy * step;
|
||||
const bb = getAABB(cx, cy, w, h, rotation);
|
||||
if (bb.x < 0 || bb.y < 0 || bb.x + bb.w > layout.canvasW || bb.y + bb.h > layout.canvasH) continue;
|
||||
if (!checkSpacingViolation(id, cx, cy, w, h, rotation, symbolId, curveAngle, w2)) return { x: cx, y: cy };
|
||||
if (!checkSpacingViolation(id, cx, cy, w, h, rotation, symbolId, curveAngle, w2, mirrored)) return { x: cx, y: cy };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -659,7 +659,7 @@ function onMouseup(e: MouseEvent) {
|
||||
const sym = layout.symbols.find(s => s.id === dragState!.placedId);
|
||||
if (sym) {
|
||||
if (!e.ctrlKey) {
|
||||
const valid = findValidPosition(sym.id, sym.x, sym.y, sym.w, sym.h, sym.symbolId, sym.rotation, sym.curveAngle, sym.w2);
|
||||
const valid = findValidPosition(sym.id, sym.x, sym.y, sym.w, sym.h, sym.symbolId, sym.rotation, sym.curveAngle, sym.w2, sym.mirrored);
|
||||
sym.x = valid.x;
|
||||
sym.y = valid.y;
|
||||
}
|
||||
|
||||
@ -470,7 +470,7 @@ function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
||||
}
|
||||
|
||||
// Collision highlight
|
||||
if (checkSpacingViolation(sym.id, sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.curveAngle, sym.w2)) {
|
||||
if (checkSpacingViolation(sym.id, sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.curveAngle, sym.w2, sym.mirrored)) {
|
||||
ctx.strokeStyle = THEME.collision.strokeColor;
|
||||
ctx.lineWidth = THEME.collision.lineWidth;
|
||||
ctx.shadowColor = THEME.collision.shadowColor;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user