igurielidze 6f0ac836fb Refactor collision/distance modules, fix curved geometry, add mirror support
- Split collision.ts (707→549): extract distance.ts (pure math) and grid-snap.ts
- Fix curved conveyor/chute outline to match SVG viewBox geometry
- Draw curves programmatically with fixed 30px band width (no SVG stretching)
- Single resize handle for curves (was 2)
- Add .gitattributes for consistent line endings
- Make Vec2 type module-private
- Add mirror transform support in renderer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:21:04 +04:00

78 lines
2.6 KiB
TypeScript

/** Shared geometry helpers used by both collision.ts and interactions.ts */
type Vec2 = [number, number];
/** Compute 4 corners of an oriented box anchored at a point along a direction.
* anchorSide='right': box extends backward from anchor (left box, right-center at anchor).
* anchorSide='left': box extends forward from anchor (right box, left-center at anchor). */
export function orientedBoxCorners(
anchorX: number, anchorY: number,
dirX: number, dirY: number,
boxW: number, boxH: number,
anchorSide: 'left' | 'right'
): Vec2[] {
const len = Math.sqrt(dirX * dirX + dirY * dirY);
if (len === 0) return [[anchorX, anchorY]];
const ux = dirX / len, uy = dirY / len;
const nx = -uy, ny = ux;
const hh = boxH / 2;
if (anchorSide === 'right') {
return [
[anchorX - ux * boxW - nx * hh, anchorY - uy * boxW - ny * hh],
[anchorX - nx * hh, anchorY - ny * hh],
[anchorX + nx * hh, anchorY + ny * hh],
[anchorX - ux * boxW + nx * hh, anchorY - uy * boxW + ny * hh],
];
} else {
return [
[anchorX - nx * hh, anchorY - ny * hh],
[anchorX + ux * boxW - nx * hh, anchorY + uy * boxW - ny * hh],
[anchorX + ux * boxW + nx * hh, anchorY + uy * boxW + ny * hh],
[anchorX + nx * hh, anchorY + ny * hh],
];
}
}
/** Check if mouse has moved past a drag threshold */
export function pastDragThreshold(
posX: number, posY: number,
startX: number, startY: number,
threshold: number
): boolean {
const dx = posX - startX;
const dy = posY - startY;
return dx * dx + dy * dy >= threshold * threshold;
}
/** Create a world-transform pair (toLocal / toWorld) for a rotated curve symbol.
* Arc center is at (acx, acy), symbol center at (scx, scy). */
export function createCurveTransforms(
acx: number, acy: number,
scx: number, scy: number,
curveRotRad: number
) {
const invCos = Math.cos(-curveRotRad), invSin = Math.sin(-curveRotRad);
const fwdCos = Math.cos(curveRotRad), fwdSin = Math.sin(curveRotRad);
/** World → arc-local (Y up) */
function toLocal(wx: number, wy: number): Vec2 {
let lx = wx, ly = wy;
if (curveRotRad) {
const dx = wx - scx, dy = wy - scy;
lx = scx + dx * invCos - dy * invSin;
ly = scy + dx * invSin + dy * invCos;
}
return [lx - acx, acy - ly];
}
/** Unrotated-local → world */
function toWorld(localX: number, localY: number): Vec2 {
if (!curveRotRad) return [localX, localY];
const dx = localX - scx, dy = localY - scy;
return [scx + dx * fwdCos - dy * fwdSin, scy + dx * fwdSin + dy * fwdCos];
}
return { toLocal, toWorld };
}