// Geometry primitives used by the map renderer. // // All distances are in world units (TS numbers, float64). Functions // in this file are pure and side-effect-free; tests exercise them // directly. import type { Camera, Viewport } from "./world"; // clamp returns v constrained to [lo, hi]. If lo > hi the function // returns lo (callers are expected to keep the bounds well-formed). export function clamp(v: number, lo: number, hi: number): number { if (v < lo) return lo; if (v > hi) return hi; return v; } // torusShortestDelta returns the signed delta from a to b on a circle // of circumference `size`, picking the direction with the smaller // absolute distance. Result lies in (-size/2, size/2]. // // At exactly size/2 the function returns +size/2 (positive direction); // the lower bound is exclusive so a delta of -size/2 wraps to +size/2. // This deterministic tie-break keeps the function self-consistent // regardless of input order. The `+0` at the end normalises -0 (which // JavaScript produces for some modulo cases) to +0. export function torusShortestDelta(a: number, b: number, size: number): number { if (!(size > 0)) { throw new Error(`torusShortestDelta: size must be positive, got ${size}`); } let d = (b - a) % size; if (d > size / 2) d -= size; else if (d <= -size / 2) d += size; return d + 0; } // distSqPointToSegment returns the squared distance from point (px,py) // to the segment (ax,ay)–(bx,by). For zero-length segments it falls // back to point-to-point distance. export function distSqPointToSegment( px: number, py: number, ax: number, ay: number, bx: number, by: number, ): number { const dx = bx - ax; const dy = by - ay; const lenSq = dx * dx + dy * dy; if (lenSq === 0) { const ex = px - ax; const ey = py - ay; return ex * ex + ey * ey; } let t = ((px - ax) * dx + (py - ay) * dy) / lenSq; if (t < 0) t = 0; else if (t > 1) t = 1; const fx = ax + t * dx; const fy = ay + t * dy; const ex = px - fx; const ey = py - fy; return ex * ex + ey * ey; } // screenToWorld converts cursor pixel coordinates (relative to the // viewport top-left) to world coordinates under the given camera. export function screenToWorld( cursorPx: { x: number; y: number }, camera: Camera, viewport: Viewport, ): { x: number; y: number } { const offX = cursorPx.x - viewport.widthPx / 2; const offY = cursorPx.y - viewport.heightPx / 2; return { x: camera.centerX + offX / camera.scale, y: camera.centerY + offY / camera.scale, }; } // worldToScreen converts a world-space point to viewport pixel // coordinates under the given camera. export function worldToScreen( world: { x: number; y: number }, camera: Camera, viewport: Viewport, ): { x: number; y: number } { return { x: viewport.widthPx / 2 + (world.x - camera.centerX) * camera.scale, y: viewport.heightPx / 2 + (world.y - camera.centerY) * camera.scale, }; }