tower / cascadia / files / test / geometry.test.ts
Branch cascadia · default branch · all branches
geometry.test.ts
ec7e78a8 2026-09-07 Pavel Safronov | 1 | import { afterEach, beforeEach, describe, expect, it } from 'vitest' |
| 2 | import { CIRCUMFERENCE_M, LENGTH_M, RADIUS_M } from '../src/canon/constants.js' | |
| 3 | import { setSlatCount, slatCount } from '../src/canon/slats.js' | |
| 4 | import { SLAT_COUNT_DEFAULT } from '../src/canon/constants.js' | |
| 5 | import * as G from '../src/canon/geometry.js' | |
| 6 | ||
| 7 | describe('theta wrapping', () => { | |
| 8 | it('normalizes into [0, 2π)', () => { | |
| 9 | expect(G.normalizeTheta(-0.5)).toBeCloseTo(2 * Math.PI - 0.5, 10) | |
| 10 | expect(G.normalizeTheta(7 * Math.PI)).toBeCloseTo(Math.PI, 10) | |
| 11 | }) | |
| 12 | it('takes the short way across the seam', () => { | |
| 13 | expect(G.deltaTheta(0.1, 2 * Math.PI - 0.1)).toBeCloseTo(-0.2, 10) | |
| 14 | expect(G.deltaTheta(2 * Math.PI - 0.1, 0.1)).toBeCloseTo(0.2, 10) | |
| 15 | }) | |
| 16 | }) | |
| 17 | ||
| 18 | describe('distances', () => { | |
| 19 | it('measures surface distance across the seam, not the long way round', () => { | |
| 20 | const a = { theta: 0.05, z: 0 } | |
| 21 | const b = { theta: 2 * Math.PI - 0.05, z: 0 } | |
| 22 | expect(G.surfaceDistance(a, b)).toBeCloseTo(0.1 * RADIUS_M, 6) | |
| 23 | }) | |
| 24 | it('halfway round is half the circumference on the surface', () => { | |
| 25 | const d = G.surfaceDistance({ theta: 0, z: 0 }, { theta: Math.PI, z: 0 }) | |
| 26 | expect(d).toBeCloseTo(CIRCUMFERENCE_M / 2, 6) | |
| 27 | }) | |
| 28 | it('the chord straight overhead is the diameter, and shorter than walking', () => { | |
| 29 | const a = { theta: 0, z: 0 } | |
| 30 | const b = { theta: Math.PI, z: 0 } | |
| 31 | expect(G.directDistance(a, b)).toBeCloseTo(2 * RADIUS_M, 6) | |
| 32 | expect(G.directDistance(a, b)).toBeLessThan(G.surfaceDistance(a, b)) | |
| 33 | }) | |
| 34 | it('rises the full radius to reach the point directly overhead', () => { | |
| 35 | expect(G.chordApexHeight({ theta: 0, z: 0 }, { theta: Math.PI, z: 0 })).toBeCloseTo(RADIUS_M, 6) | |
| 36 | expect(G.chordApexHeight({ theta: 0, z: 0 }, { theta: 0, z: 1000 })).toBeCloseTo(0, 6) | |
| 37 | }) | |
| 38 | }) | |
| 39 | ||
| 40 | describe('slats and land strips', () => { | |
| 41 | // Canon is a sealed hull (D-017); these exercise the exploration mode. | |
| 42 | beforeEach(() => setSlatCount(8)) | |
| 43 | afterEach(() => setSlatCount(SLAT_COUNT_DEFAULT)) | |
| 44 | ||
| 45 | it('puts land strip centres on land and slat centres on slats', () => { | |
| 46 | for (let i = 0; i < slatCount(); i++) { | |
| 47 | expect(G.isSlat(G.landStripCentre(i))).toBe(false) | |
| 48 | expect(G.isSlat(G.slatCentre(i))).toBe(true) | |
| 49 | expect(G.landStripIndex(G.landStripCentre(i))).toBe(i) | |
| 50 | expect(G.landStripIndex(G.slatCentre(i))).toBeNull() | |
| 51 | } | |
| 52 | }) | |
| 53 | it('reports a slat crossing when moving between adjacent strips', () => { | |
| 54 | expect(G.routeCrossesSlat( | |
| 55 | { theta: G.landStripCentre(0), z: 0 }, | |
| 56 | { theta: G.landStripCentre(1), z: 0 }, | |
| 57 | )).toBe(true) | |
| 58 | }) | |
| 59 | it('reports no crossing when moving along one strip', () => { | |
| 60 | expect(G.routeCrossesSlat( | |
| 61 | { theta: G.landStripCentre(0), z: 0 }, | |
| 62 | { theta: G.landStripCentre(0), z: 200_000 }, | |
| 63 | )).toBe(false) | |
| 64 | }) | |
| 65 | }) | |
| 66 | ||
| 67 | describe('map projection', () => { | |
| 68 | it('round-trips surface points exactly — a cylinder unrolls without distortion', () => { | |
| 69 | for (const p of [ | |
| 70 | { theta: 0, z: 0 }, | |
| 71 | { theta: 1.234, z: 271_000 }, | |
| 72 | { theta: 2 * Math.PI - 1e-9, z: LENGTH_M }, | |
| 73 | ]) { | |
| 74 | const back = G.fromMap(G.toMap(p)) | |
| 75 | expect(back.theta).toBeCloseTo(G.normalizeTheta(p.theta), 9) | |
| 76 | expect(back.z).toBeCloseTo(p.z, 6) | |
| 77 | } | |
| 78 | }) | |
| 79 | it('spans the world', () => { | |
| 80 | expect(G.MAP_WIDTH_M).toBe(LENGTH_M) | |
| 81 | expect(G.MAP_HEIGHT_M).toBeCloseTo(CIRCUMFERENCE_M, 9) | |
| 82 | }) | |
| 83 | }) |