clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / analysis / cellsystems / ground.ts
ground.ts 3.7K
1// The Ground descriptor: what a cell system is handed instead of reaching2// into a running application (issue #5 §5.4).3//4// A cell system needs three facts about the world it is dividing — the5// volume's tile grid, the open lens, the current world — and a system that6// reaches for them is a system that cannot be run without a browser holding an7// application. A Ground is exactly those three reads, written down and handed8// in, and nothing else. `analysis/testdata/cells/grounds.json` records the9// same shape, and both lanes' conformance suites hold this module to its10// numbers.11//12// What is deliberately NOT here: the active system, the held cell, whether13// the subdivision is showing. Those are session state (§4.1) and they arrive14// as arguments — `cellPlan(ground, system, cellID)` — which is what lets one15// ground be divided two ways at once by two callers who never meet.1617import type { World } from "../semconv/geometry.ts";1819/** A rectangle in **y-down world pixels**, the space a lens declares itself in. */20export interface Rect {21 readonly x: number;22 readonly y: number;23 readonly width: number;24 readonly height: number;25}2627/**28 * One raster picture of a ground. `surface` is the ground the picture covers29 * when the lens declares one; `bounds` is the raster window it actually fills.30 * Both are independently absent, and the fallback ladder below is why.31 */32export interface Lens {33 readonly surface?: Rect | null;34 readonly bounds?: Rect | null;35}3637/**38 * Everything a cell system may know about where it is dividing.39 *40 * `tileGridSize` is the volume's world square — the last fallback, and null41 * when no volume is open at all. `lens` is null when no lens is open.42 */43export interface Ground {44 readonly tileGridSize: number | null;45 readonly lens: Lens | null;46 readonly world: World;47}4849/** `[minX, minY, maxX, maxY]` in OL world coordinates: x east, y negative-down. */50export type Extent = readonly [number, number, number, number];5152/** `[x, y]` in OL world coordinates: x east, y negative-down. */53export type Coordinate = readonly [number, number];5455/**56 * The declared surface, resolved as one extent. S2, the ring clip and the57 * ground questions divide this; geohash makes its own ground choice on58 * planes (`geohashExtent` in geohash.ts), so a game map's grid can cover the59 * full map whatever window a lens declares.60 *61 * Three branches, in order: the lens's declared surface; failing that its62 * raster window; failing that the whole world square. The sign flip is the63 * conversion from the y-down pixels a bundle declares to the y-negative-down64 * coordinates every other function here speaks — the top edge of a lens65 * anchored at y = 0 comes out as -0, which is a real double and a deliberate66 * output: the intent tests assert it, and the shared vectors normalize it67 * away at the JSON boundary (analysis/test/vectors.test.ts).68 */69export function surfaceExtent(ground: Ground): Extent {70 const surface = ground.lens?.surface;71 if (surface) {72 return [73 surface.x,74 -(surface.y + surface.height),75 surface.x + surface.width,76 -surface.y,77 ];78 }79 const size = ground.tileGridSize ?? 0;80 const bounds = ground.lens?.bounds ?? { x: 0, y: 0, width: size, height: size };81 return [bounds.x, -(bounds.y + bounds.height), bounds.x + bounds.width, -bounds.y];82}8384/** The width of the ground: one whole world of x, and the seam's period. */85export function surfaceWidth(ground: Ground): number {86 const [minimumX, , maximumX] = surfaceExtent(ground);87 return maximumX - minimumX;88}8990/** The area of an extent, used to compare precision across systems. */91export function extentArea(extent: Extent): number {92 const [minimumX, minimumY, maximumX, maximumY] = extent;93 return (maximumX - minimumX) * (maximumY - minimumY);94}