- 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>
78 lines
2.6 KiB
TypeScript
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 };
|
|
}
|