/** * The aeroplane glyph has a floor AND a ceiling, and the ceiling is the newer half. * * The floor is a screen-space rule: never smaller than legible, because position * and heading are what a reader wants off a map and neither survives half a * pixel. It scales by the distance to *that aircraft*, which is the same thing * as "how far the camera has zoomed" only when everything in frame is equally * far away. On a whole-board pose it is. Beside a landmark it is not — at the * Golden Gate the bridge is a couple of units from the camera and the traffic * over the Pacific is a couple of thousand, so the floor fired hard on the * aeroplane and not at all on the bridge, and an airliner was drawn about two * and a half times the length of the main span. * * These tests pin both ends: that the board still gets a symbol it can read, and * that no camera anywhere can produce a state-sized aeroplane again. */ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { glyphScale } from "../../engine/flights.ts"; /** The field of view the city scenes actually use, near enough for a ratio. */ const FOV = 50; describe("glyph scale", () => { it("never shrinks an aeroplane below the size it was drawn at", () => { for (const d of [0.5, 5, 20, 34]) { assert.ok(glyphScale(d, FOV) >= 1, `close camera at ${d} must not shrink the glyph`); } }); it("still grows the glyph across the whole-board range, where the floor is the point", () => { const near = glyphScale(200, FOV); const far = glyphScale(400, FOV); assert.ok(near > 1, "a board-distance aeroplane must be enlarged to stay readable"); assert.ok(far > near, "the floor must keep tracking distance until the ceiling binds"); }); it("stops growing at all, however far the aircraft is", () => { const capped = glyphScale(2280, FOV); assert.equal( glyphScale(4000, FOV), capped, "past the ceiling the scale must be flat, not merely slower", ); assert.equal(glyphScale(1e9, FOV), capped, "and flat all the way out"); }); /* * Deliberately asserts a REDUCTION and not an absolute size, because the * ceiling is a mitigation and calling it a cure in a test name would be the * test lying about the product. * * 2,280 units is the measured Golden Gate case, where the uncapped glyph * reached about 81x — roughly two and a half times the bridge's 1,280 m main * span at ~94 m to the unit. The ceiling takes that to about one and a half. * It is still bigger than the bridge. The complete fix is to clamp against the * camera's focus distance rather than the aircraft's, which is a signature * change and is written up in `flights.ts`. */ it("cuts the worst case by a third without touching any board distance", () => { /** The floor alone, with no ceiling — what the scale used to be. */ const uncapped = (d: number, fov: number): number => { const frustum = 2 * d * Math.tan((fov * Math.PI) / 360); return Math.max(1, (0.016 * frustum) / 0.42); }; // 1,160 units is the far end of the orbit over the California corridor — a // pose people actually use. It must be untouched at every field of view the // scenes run at, because below 0.012 of the frame the wings stop resolving // and a ceiling of 26 put it at 0.0123. for (const fov of [42, 50, 60]) { assert.equal( glyphScale(1160, fov), uncapped(1160, fov), `the ceiling must not bind at 1160 units and ${fov} degrees`, ); } // And the chapter-zoom case must actually come down. assert.ok( glyphScale(2280, 50) < uncapped(2280, 50) * 0.7, "the ceiling must remove at least 30% of the worst case", ); }); it("is monotonic, so an aeroplane never grows as it approaches", () => { let previous = 0; for (const d of [1, 10, 50, 100, 300, 700, 900, 2000, 5000]) { const s = glyphScale(d, FOV); assert.ok(s >= previous, `scale fell between distances at ${d}`); previous = s; } }); it("returns 1 for degenerate inputs rather than emptying the sky", () => { for (const [d, fov] of [[0, FOV], [-1, FOV], [10, 0], [10, 180], [NaN, FOV], [10, NaN]]) { assert.equal(glyphScale(d as number, fov as number), 1); } }); }); /* * The focus-distance clamp — the complete fix the ceiling above was a mitigation * for. * * Every assertion in the suite above is left exactly as it was, and that is the * point of the first test here: the third parameter is optional and omitting it * has to reproduce the previous answer to the bit, not to a rounding. A ceiling * that moved by a hair under this change would be silent visual drift in the one * function whose entire job is to prevent silent visual drift. */ describe("glyph scale, clamped against what the camera is looking at", () => { it("reproduces the two-argument answer exactly when no focus distance is given", () => { for (const fov of [42, 50, 60]) { for (const d of [0.5, 5, 34, 200, 400, 1160, 2280, 4000, 1e9]) { assert.equal( glyphScale(d, fov, undefined), glyphScale(d, fov), `omitting the focus distance changed the answer at ${d} units and ${fov} degrees`, ); } } }); it("ignores a focus distance that is not a usable number", () => { for (const bad of [0, -1, NaN, Infinity]) { assert.equal( glyphScale(2280, 50, bad), glyphScale(2280, 50), `a focus distance of ${bad} must fall back to the flat ceiling`, ); } }); it("leaves the whole-board pose alone, where the floor is right about everything", () => { /* * At a standoff the camera's target and the aircraft are the same distance * away to within a third, so the clamp must not bind. 1,160 units is the far * end of the California orbit; the aircraft on that board run from about 900 * to about 1,400. */ for (const fov of [42, 50, 60]) { for (const aircraft of [900, 1160, 1400]) { assert.equal( glyphScale(aircraft, fov, 1160), glyphScale(aircraft, fov), `the clamp bound at a whole-board pose (${aircraft} units, ${fov} degrees)`, ); } } }); it("collapses the Golden Gate case, which the flat ceiling only softened", () => { // The measured chapter: the bridge fifteen units off the camera, the traffic // over the Pacific two thousand two hundred and eighty away. const flat = glyphScale(2280, 50); const focused = glyphScale(2280, 50, 15); assert.ok(focused < flat / 5, `focus clamp barely moved the worst case: ${focused} vs ${flat}`); assert.ok(focused >= 1, "and it must never shrink the glyph below its authored size"); }); it("is monotonic in the focus distance: looking further away never shrinks the glyph", () => { let previous = 0; for (const focus of [1, 5, 15, 34, 100, 300, 1160, 5000]) { const s = glyphScale(2280, 50, focus); assert.ok(s >= previous, `scale fell as the camera focused further out, at ${focus}`); previous = s; } assert.equal( previous, glyphScale(2280, 50), "and at a focus distance past the backstop it must equal the unclamped answer", ); }); it("never exceeds the absolute backstop, whatever focus distance it is handed", () => { for (const focus of [1e6, 1e9]) { assert.ok( glyphScale(1e9, 50, focus) <= glyphScale(1e9, 50), "the focus clamp must be a ceiling on top of the old one, never a lift", ); } }); });