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 ───
|
// ─── 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(
|
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][] {
|
): [number, number][] {
|
||||||
const cx = x + w / 2;
|
const cx = x + w / 2;
|
||||||
const cy = y + h / 2;
|
const cy = y + h / 2;
|
||||||
const local: [number, number][] = [
|
let local: [number, number][];
|
||||||
[x, y], // TL
|
if (mirrored) {
|
||||||
[x + w2, y], // TR (top base end)
|
// Mirrored: angled edge on left side instead of right
|
||||||
[x + w, y + h], // BR (bottom base end)
|
local = [
|
||||||
[x, y + h], // BL
|
[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;
|
if (!rotation) return local;
|
||||||
const rad = rotation * Math.PI / 180;
|
const rad = rotation * Math.PI / 180;
|
||||||
const cos = Math.cos(rad), sin = Math.sin(rad);
|
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 */
|
/** Get the convex hull vertices of an induction shape (arrow + strip) in world space */
|
||||||
function getInductionVertices(
|
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][] {
|
): [number, number][] {
|
||||||
const hw = INDUCTION_CONFIG.headWidth;
|
const hw = INDUCTION_CONFIG.headWidth;
|
||||||
const stripTopY = y + h * INDUCTION_CONFIG.stripTopFrac;
|
const stripTopY = y + h * INDUCTION_CONFIG.stripTopFrac;
|
||||||
const stripBottomY = y + h * INDUCTION_CONFIG.stripBottomFrac;
|
const stripBottomY = y + h * INDUCTION_CONFIG.stripBottomFrac;
|
||||||
const arrowPts = INDUCTION_CONFIG.arrowPoints;
|
const arrowPts = INDUCTION_CONFIG.arrowPoints;
|
||||||
// Convex hull: top-right strip, arrow top, arrow left, arrow bottom, bottom-right strip
|
const cx = x + w / 2;
|
||||||
const local: [number, number][] = [
|
let local: [number, number][];
|
||||||
[x + w, stripTopY], // strip top-right
|
if (mirrored) {
|
||||||
[x + arrowPts[0][0] * hw, y + arrowPts[0][1] * h], // arrow top junction
|
// Mirror arrow x-coords around center
|
||||||
[x + arrowPts[1][0] * hw, y + arrowPts[1][1] * h], // arrow top
|
local = [
|
||||||
[x + arrowPts[2][0] * hw, y + arrowPts[2][1] * h], // arrow left point
|
[x, stripTopY],
|
||||||
[x + arrowPts[3][0] * hw, y + arrowPts[3][1] * h], // arrow bottom
|
[x + w - arrowPts[0][0] * hw, y + arrowPts[0][1] * h],
|
||||||
[x + arrowPts[5][0] * hw, y + arrowPts[5][1] * h], // arrow bottom junction
|
[x + w - arrowPts[1][0] * hw, y + arrowPts[1][1] * h],
|
||||||
[x + w, stripBottomY], // strip bottom-right
|
[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;
|
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 rad = rotation * Math.PI / 180;
|
||||||
const cos = Math.cos(rad), sin = Math.sin(rad);
|
const cos = Math.cos(rad), sin = Math.sin(rad);
|
||||||
return local.map(([px, py]) => {
|
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 */
|
/** Get collision vertices for any non-curved, non-EPC symbol based on its actual shape */
|
||||||
function getShapeVertices(
|
function getShapeVertices(
|
||||||
x: number, y: number, w: number, h: number, rotation: number,
|
x: number, y: number, w: number, h: number, rotation: number,
|
||||||
symbolId: string, w2?: number
|
symbolId: string, w2?: number, mirrored?: boolean
|
||||||
): [number, number][] {
|
): [number, number][] {
|
||||||
if (isSpurType(symbolId)) return getSpurVertices(x, y, w, h, w2 ?? w, rotation);
|
if (isSpurType(symbolId)) return getSpurVertices(x, y, w, h, w2 ?? w, rotation, mirrored);
|
||||||
if (isInductionType(symbolId)) return getInductionVertices(x, y, w, h, rotation);
|
if (isInductionType(symbolId)) return getInductionVertices(x, y, w, h, rotation, mirrored);
|
||||||
return getOBBVertices(x, y, w, h, rotation);
|
return getOBBVertices(x, y, w, h, rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkSpacingViolation(
|
export function checkSpacingViolation(
|
||||||
id: number, x: number, y: number, w: number, h: number, rotation: number,
|
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 {
|
): boolean {
|
||||||
if (symbolId && SPACING_EXEMPT.has(symbolId)) return false;
|
if (symbolId && SPACING_EXEMPT.has(symbolId)) return false;
|
||||||
|
|
||||||
@ -497,29 +522,29 @@ export function checkSpacingViolation(
|
|||||||
|
|
||||||
// ── EPC vs non-curved/non-EPC ──
|
// ── EPC vs non-curved/non-EPC ──
|
||||||
if (isEpc && epcQuads && !symIsCurved && !symIsEpc) {
|
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;
|
if (anyQuadVsPolygon(epcQuads, bVerts, spacing)) return true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (symIsEpc && symEpcQuads && !isCurved && !isEpc) {
|
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;
|
if (anyQuadVsPolygon(symEpcQuads, aVerts, spacing)) return true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Non-EPC, non-curved: shape polygon vs shape polygon ──
|
// ── Non-EPC, non-curved: shape polygon vs shape polygon ──
|
||||||
if (!isCurved && !symIsCurved) {
|
if (!isCurved && !symIsCurved) {
|
||||||
const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, 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);
|
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;
|
if (polygonSATWithSpacing(aVerts, bVerts, spacing)) return true;
|
||||||
} else if (isCurved && !symIsCurved) {
|
} 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(
|
if (checkCurvedVsSpur(
|
||||||
{ x, y, w, h, rotation, symbolId: symbolId!, curveAngle },
|
{ x, y, w, h, rotation, symbolId: symbolId!, curveAngle },
|
||||||
bVerts, spacing
|
bVerts, spacing
|
||||||
)) return true;
|
)) return true;
|
||||||
} else if (!isCurved && symIsCurved) {
|
} 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;
|
if (checkCurvedVsSpur(sym, aVerts, spacing)) return true;
|
||||||
} else {
|
} else {
|
||||||
// Both curved: use band AABBs as fallback
|
// 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(
|
export function findValidPosition(
|
||||||
id: number, x: number, y: number, w: number, h: number,
|
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 } {
|
): { x: number; y: number } {
|
||||||
const snapped = snapToGrid(x, y, w, h, rotation);
|
const snapped = snapToGrid(x, y, w, h, rotation);
|
||||||
let sx = snapped.x, sy = snapped.y;
|
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 step = layout.snapEnabled ? layout.gridSize : 1;
|
||||||
const searchRadius = 20;
|
const searchRadius = 20;
|
||||||
@ -37,7 +37,7 @@ export function findValidPosition(
|
|||||||
const cy = sy + dy * step;
|
const cy = sy + dy * step;
|
||||||
const bb = getAABB(cx, cy, w, h, rotation);
|
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 (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);
|
const sym = layout.symbols.find(s => s.id === dragState!.placedId);
|
||||||
if (sym) {
|
if (sym) {
|
||||||
if (!e.ctrlKey) {
|
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.x = valid.x;
|
||||||
sym.y = valid.y;
|
sym.y = valid.y;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -470,7 +470,7 @@ function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collision highlight
|
// 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.strokeStyle = THEME.collision.strokeColor;
|
||||||
ctx.lineWidth = THEME.collision.lineWidth;
|
ctx.lineWidth = THEME.collision.lineWidth;
|
||||||
ctx.shadowColor = THEME.collision.shadowColor;
|
ctx.shadowColor = THEME.collision.shadowColor;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user