// Unit tests for the geometry primitives in src/map/math.ts. // // These functions are the foundation for hit-test and the no-wrap // camera helpers; they run far more often than their callers and any // regression here ripples everywhere. Each test asserts a single // algebraic property; the cases together cover the contract of the // functions described in ui/docs/renderer.md. import { describe, expect, test } from "vitest"; import { clamp, distSqPointToSegment, screenToWorld, torusShortestDelta, worldToScreen, } from "../src/map/math"; describe("clamp", () => { test("returns the value when inside the bounds", () => { expect(clamp(5, 0, 10)).toBe(5); expect(clamp(0, 0, 10)).toBe(0); expect(clamp(10, 0, 10)).toBe(10); }); test("clamps to the lower bound", () => { expect(clamp(-3, 0, 10)).toBe(0); }); test("clamps to the upper bound", () => { expect(clamp(13, 0, 10)).toBe(10); }); }); describe("torusShortestDelta", () => { test("returns zero for equal inputs", () => { expect(torusShortestDelta(50, 50, 100)).toBe(0); }); test("returns the direct delta when no wrap is shorter", () => { expect(torusShortestDelta(10, 30, 100)).toBe(20); expect(torusShortestDelta(30, 10, 100)).toBe(-20); }); test("wraps to the shorter direction near the seam", () => { // from=10, to=90: direct=+80, wrap=-20 — wrap wins. expect(torusShortestDelta(10, 90, 100)).toBe(-20); // from=90, to=10: direct=-80, wrap=+20 — wrap wins. expect(torusShortestDelta(90, 10, 100)).toBe(20); }); test("normalises inputs outside [0, size)", () => { expect(torusShortestDelta(-10, 10, 100)).toBe(20); expect(torusShortestDelta(110, 10, 100)).toBe(-100 + 100); // wraps to 0 }); test("at exactly size/2 picks the positive direction deterministically", () => { // from=0, to=50, size=100 — both directions are equal. // The contract: returns +size/2. expect(torusShortestDelta(0, 50, 100)).toBe(50); }); test("rejects non-positive size", () => { expect(() => torusShortestDelta(0, 0, 0)).toThrow(); expect(() => torusShortestDelta(0, 0, -1)).toThrow(); }); }); describe("distSqPointToSegment", () => { test("zero distance when the point is on the segment", () => { expect(distSqPointToSegment(5, 0, 0, 0, 10, 0)).toBe(0); expect(distSqPointToSegment(0, 0, 0, 0, 10, 0)).toBe(0); expect(distSqPointToSegment(10, 0, 0, 0, 10, 0)).toBe(0); }); test("perpendicular foot inside the segment", () => { // segment along the x-axis from (0,0) to (10,0); point at (5,3). // foot is (5,0), distance is 3, distSq is 9. expect(distSqPointToSegment(5, 3, 0, 0, 10, 0)).toBeCloseTo(9, 12); }); test("foot beyond the start endpoint clamps to start", () => { expect(distSqPointToSegment(-2, 0, 0, 0, 10, 0)).toBeCloseTo(4, 12); }); test("foot beyond the end endpoint clamps to end", () => { expect(distSqPointToSegment(15, 0, 0, 0, 10, 0)).toBeCloseTo(25, 12); }); test("zero-length segment falls back to point distance", () => { expect(distSqPointToSegment(3, 4, 0, 0, 0, 0)).toBeCloseTo(25, 12); }); }); describe("screenToWorld and worldToScreen", () => { const viewport = { widthPx: 800, heightPx: 600 }; const camera = { centerX: 1000, centerY: 500, scale: 2 }; test("centre of viewport maps to camera centre in world space", () => { const w = screenToWorld({ x: 400, y: 300 }, camera, viewport); expect(w.x).toBeCloseTo(1000, 12); expect(w.y).toBeCloseTo(500, 12); }); test("worldToScreen is the inverse of screenToWorld", () => { const screenIn = { x: 123.5, y: 456.25 }; const world = screenToWorld(screenIn, camera, viewport); const screenOut = worldToScreen(world, camera, viewport); expect(screenOut.x).toBeCloseTo(screenIn.x, 9); expect(screenOut.y).toBeCloseTo(screenIn.y, 9); }); test("scale propagates: 2px on screen = 1 world unit at scale=2", () => { const w0 = screenToWorld({ x: 400, y: 300 }, camera, viewport); const w1 = screenToWorld({ x: 402, y: 300 }, camera, viewport); expect(w1.x - w0.x).toBeCloseTo(1, 12); }); });