1# The render seam23**Status: normative for the lane.** This document is written to one standard:4someone who has never seen `render/` should be able to delete it and write it5again from this document, `docs/format.md`, `docs/app.md` and6`docs/analysis.md`. That is what issue #5 §5.5 means by deletability — not a7CI job that removes a folder, but a lane whose contracts are written down well8enough that the code is replaceable.910The implementation is `render/`. Where this document and the code disagree,11take it as a defect in one of them and say so.1213The standard holds for the contracts and not for the numbers: §10.1 is the14list of what a rewriter would still have to read out of `render/`.1516---1718## 1. What the seam is1920The seam **pictures a volume**. It is a typed TypeScript application, built by21esbuild into one JavaScript file, loaded by one `<script type="module">` tag,22and it renders into two custom elements. It is the only client-side code in23the application.2425It stands on **three published contracts and one duty**:2627| | Where it is written | What it gives the seam |28|---|---|---|29| the `/data` plane | `docs/format.md`, `docs/app.md` §2.1 | worlds, packed locations, prose, tiles, icons |30| the scene description | `docs/app.md` §4, and §3 below | what to draw and in what arrangement |31| the analysis API | `docs/analysis.md`, `analysis/` | cell systems: plans, rings, style tokens |32| the diagnostics duty | §8 below | what the seam must publish about itself |3334And on nothing else. It never imports the application, never reads a Go type,35never learns a route beyond the two it posts to, and **nothing imports it**36(`tools/depcheck` and the ESLint boundary rules both say so).3738### 1.1 The rules that keep it that way3940- **Data flows one way.** Server → scene description → seam. Two things flow41 back and no more: the `atlas:pick` DOM event and the debounced camera42 report (§5).43- **Fetch lives in one place.** `render/data/` owns every network call. An44 ESLint rule fails the build otherwise, naming this contract.45- **No bare `console.*`.** The `log` module is the browser end of the one46 event stream (`docs/logging.md`).47- **No UI framework.** The dependency surface is pinned: OpenLayers,48 globe.gl + three, and `@atlas/analysis` (which brings s2js). Growing it is49 a deliberate act, never a side effect of a feature.50- **`strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, ESM,51 no `any`** outside a typed browser-API escape hatch.52- **~3,000 authored lines** is the guideline. `render/tools/lines.mjs` counts53 and warns; it never fails a build. Today: 3,024 code lines, 1,288 of prose,54 and 630 more of tests counted separately — 24 lines past the guideline, which55 the lane gate prints as a warning on every run. Read the number from the tool56 rather than from this sentence.5758### 1.2 Progressive by construction5960Until the bundle loads, `<atlas-viewport>` is an unknown element: it renders61nothing and breaks nothing. The application must serve, and every non-viewport62test must pass, with the seam's assets absent — `/static/app.js` then answers63`404` ([decision 13](decisions/0013-assets-and-static-are-two-mounts.md)).64This is the deletability principle standing up in the build order.6566---6768## 2. The surface6970```html71<atlas-viewport state-src="#atlas-viewport-state" hx-morph-skip-children>72 <atlas-chart></atlas-chart>73 <atlas-globe hidden></atlas-globe>74</atlas-viewport>75<div id="atlas-viewport-state" hidden data-…>…</div>76```7778Three elements, and the state node beside them. `<atlas-viewport>` is the host:79it reads the scene, loads payloads, computes the standing set, and hands both80panes the same object. `<atlas-chart>` is the plane; `<atlas-globe>` is the81sphere, and is present only on a world that declares one.8283**The morph-skip requirement.** The application renders `hx-morph-skip-children`84on `<atlas-viewport>`, on the map pane and on the overview's canvas holder. A85swap may touch their attributes and must never reach their internals: tearing86down a WebGL context mid-gesture is not a re-render. What a swap *does* replace87is the inert state node beside them, whole (`outerMorph`), which is why the88seam re-resolves it by selector after every swap rather than holding the89element.9091Two further pieces of chrome the application renders **for** the seam, and the92seam draws into:9394| Element | Whose | What the seam does |95|---|---|---|96| `#overview-canvas`, `#overview-viewport` | rendered by `overview.tmpl` | draws the world once per lens; writes the camera rectangle in whole pixels |97| `#atlas-overview` | `overview.tmpl` | the shelf: marks it, and hides it when the camera has the whole world |98| `#globe-toggle` | rendered by `topbar.tmpl` | binds the click; flips the panes; writes `aria-pressed` |99| `#labels-hint` | rendered by `shell.tmpl` | writes the held-key hint text |100| `#visible-count` | `shell.tmpl`'s footer | writes the standing count — the seam's half of a number the server also renders |101| `#atlas-camera-*` | `shell.tmpl`'s hidden form | writes the five camera fields the form posts (§5) |102103One more element sits in the same box and is **not** the seam's: `<div104id="map">`, rendered first by `viewport-surface`, morph-skipped, carrying the105page's backdrop, the keyboard focus and the loading scrim. In the reference106implementation it *was* the map. Here the panes are the custom elements and107`#map` is what is left, which is why paint order is load-bearing — rendered108after the panes its backdrop covers them, every count reads correct and the109page shows an empty rectangle.110111Which pane is up is **seam-local state** (issue #5 §4.1): the same world, the112same filters, the same camera, seen from a different distance. The application113renders the toggle because only it knows whether the world declares a sphere;114the seam wires it because flipping moves no discrete state.115116---117118## 3. The scene description119120`#atlas-viewport-state` is an inert node the server renders and the seam121observes. Every field is a string attribute or a `<data>` child, because the122node has to survive a morph and be readable without JavaScript running.123124### 3.1 Attributes125126| Attribute | Meaning |127|---|---|128| `data-volume` | volume slug |129| `data-base` | `/data/v/<slug>/<stamp12>` — the URL prefix of the serving build |130| `data-world` | world slug |131| `data-lens` | the lens's **name**, matched against the payload's `lenses[].name` |132| `data-lens-index` | its ordinal, used when the name does not match |133| `data-surface` | `plane` or `sphere` — what the world declares |134| `data-selected` | the selected feature's id, or empty |135| `data-search` | the search query, or empty |136| `data-grid-system` | the cell system's slug, or empty for "no grid" |137| `data-grid-cell` | the held cell's opaque id, or empty for the root |138| `data-subgrid` | `0` or a positive number: whether the subdivision shows |139| `data-camera` | `x,y,zoom,rotation`, absent until the seam has reported one |140141### 3.2 Children142143```html144<data class="hidden-collection" value="12"></data>145<data class="highlighted-feature" value="3050"></data>146<data class="label-override" value="39191589=quiet"></data>147```148149- **`hidden-collection`** — collection ids the reader hid, of every kind.150- **`highlighted-feature`** — shape features the reader highlighted. They151 conjoin across collections and unite within one (§4.2).152- **`label-override`** — `<collectionId>=<always|quiet>`, the reader's own153 label policy where it differs from the curation.154155### 3.3 How it is read156157`readScene(node)` is a pure function of a node-shaped thing — it takes the158smallest structural interface a real `Element` satisfies, so it is tested over159a synthetic node with no DOM. It is **lenient**: a missing attribute is its160empty value, an unparsable camera is no camera, a malformed override is161dropped, unknown attributes are ignored.162163`sceneChange(was, now)` names what moved — `volume`, `world`, `lens`,164`filters`, `selection`, `grid`, `camera` — and the seam acts on the names165rather than comparing attributes for itself. That is what makes a swap a166**reconcile**: a hidden collection restyles, a lens swap keeps the camera, a167world change rebuilds.168169`SceneWatcher` keeps the two mechanisms a morph needs: a `MutationObserver`170for a node patched in place, and a re-resolve by selector for a node replaced171whole. `rescan()` is what the boot module calls after a swap.172173---174175## 4. What the seam computes176177### 4.1 The world model178179Built once per world, immutable afterwards. Points come from180`worlds/<slug>.bin` as zero-copy typed-array views (`docs/format.md` §7);181shapes come inline from the payload's collections.182183**The projection nobody wrote down.** A payload spells positions as `lat`/`lng`184in the volume's own world space — which is the slippy-tile grid the capture185was cut from, not WGS 84 — and every renderer needs them as world pixels:186187```188worldTiles = 2^grid.sourceZoom189xTile = (lng + 180) / 360 · worldTiles190yTile = (1 − asinh(tan(lat·π/180)) / π) / 2 · worldTiles191x = (xTile − grid.firstTile) · grid.tileSize192y = −(yTile − grid.firstTile) · grid.tileSize193```194195`grid` is the manifest's `tileGrid` with the world payload's own `grid`196override applied (`docs/format.md` §6.2) — every corpus volume overrides it,197so skipping the override puts every feature about 850,000 pixels off the map.198**This arithmetic belongs in `docs/format.md` and is not there.** It is199recorded here, and in `internal/app/world.go`, until it moves.200201The sign flip is the one place the format's y-down pixels meet the y-negative202-down coordinates everything downstream speaks.203204### 4.2 The standing set205206One object per scene answers what stands, and the chart, the sphere, the picks207and the diagnostics all read it. The rules, in order:2082091. **Shard.** A split world offers one layer at a time. A feature on another210 shard is *elsewhere in the world*, not filtered: not drawn, not counted,211 not picked.2122. **Filtered.** Its collection is hidden, or the search does not name it.2133. **Highlights.** Highlighted shapes **conjoin across collections and unite214 within one**: a pin stands only where every collection holding a highlight215 claims it. Containment is boundary-inclusive with a unit of grace; a path216 claims what lies within half its declared ground width.2174. **Cell.** A held grid cell narrows exactly the way a highlight does.218219Rules 3 and 4 share one bypass: **the selected feature and anything the search220names are spared**. A card open about a feature must not lose it, and a221searched-for name is what the reader asked to see.222223**Promotion** is a separate question from standing: selected, hovered,224searched, or inside the held cell. A promoted feature is drawn again in the225layer above everything and is exempt from declutter.226227**Priority** is a pin's place in the crowd: `max(0, 1e6 − min(members,999)·1000)`228plus a stable hash of its id, mod 1000. Rarer collections first, ties broken229the same way on every run and in both panes.230231---232233## 5. What flows back234235**`atlas:pick`** — a `CustomEvent` raised **on the window**, non-bubbling,236dispatched by the pane that resolved the hit:237238```js239window.dispatchEvent(new CustomEvent("atlas:pick", { bubbles: false,240 detail: { feature: "1849", kind: "point" } })) // kind: point | path | area241```242243The seam resolves the hit and submits nothing: **the page turns it into an244ordinary `POST /session/select`**, and the new selection comes back as a new245scene. That is wired: the shell renders a hidden form,246`<form id="atlas-pick" hx-post="/session/select" hx-trigger="atlas:pick247from:window">`, whose one field the seam fills in before it raises the event248(`render/data/report.ts`, `internal/app/templates/shell.tmpl`). A pick off249either canvas therefore opens the card, exactly as a row in the dock does.250251Two details are contract rather than taste. **The window, and no bubbling**:252there is one listener for the whole page and it is on the window, so the event253is raised where it is heard rather than left to travel — a swap can carry the254form across at any moment, and an event that depended on the path from a255canvas to it would depend on a tree the application is free to reshape.256**A miss does not post.** Clicking open water is not a request to close what257the reader is reading, so an empty feature raises nothing at all; the card is258put away by Escape and by its own button. A pick event that fired on a miss259would clear the selection on every stray click260(`render/test/pick.test.ts` holds this decision).261262**The camera report** is `POST /session/view`, form-encoded263(`volume world x y zoom rotation`), debounced 400 ms after the camera settles,264answered `204` — but **the seam does not make the request**. It writes the265five values into the hidden inputs `#atlas-camera-world|-x|-y|-zoom|-rotation`266and dispatches `atlas:camera` on `window` (not bubbling); `shell.tmpl` carries267a hidden form with `hx-post="/session/view" hx-trigger="atlas:camera268from:window"` that does the posting. The seam therefore owns no request at269all, which is the stricter reading of "fetch lives in one place" and the reason270the seam never sees the answer. One report is unconditional at `show()`,271because a `view.fit()` with no animation raises no settle event and the opening272camera would otherwise never be told.273274That is the whole outward surface: two DOM events and a set of hidden inputs.275No request the page did not make, and no write to the scene node, ever.276277**Inbound, there is one.** After an htmx swap the seam must re-resolve the278scene node by selector, re-assert the chrome it wired, and rewrite the standing279count. `boot.ts` listens on `document.body` for `htmx:after:swap`,280`htmx:after:settle`, their htmx-2 camel-case spellings, and `atlas:rescan` —281the event a harness or a host raises to say "look again" without an htmx swap282behind it. All five call the same `rescan()`.283284**Only the pane the reader is looking through reports.** A chart put away285behind the sphere still has a camera, and it is not a view of anything: the286pane has no window, so a fit into it lands wherever a fit into nothing lands,287and saving that would reopen the volume somewhere nobody ever stood. The same288rule governs the footer's count and the corner locator — a pane with no window289answers no question about what it can see. Coming back up is what makes its290camera the reader's again.291292That is the whole outward surface. No other request, no other event, no293writes to the scene node, ever.294295---296297## 6. The chart298299### 6.1 `ATLAS:PIXELS`300301```302origin top-left of the world square303x increases right304y decreases downward: the square is [0, −size] … [size, 0]305resolution world pixels per screen pixel; level z is size / (tileSize · 2^z)306```307308`size` is 8192 and `tileSize` is 256 in every published bundle, so level 0 is309the square in one tile and zoom 0 is resolution 32. The banner string310311```312ATLAS:PIXELS; origin top-left; x increases right; y decreases downward313```314315is part of the diagnostics contract (§8.2). Never reword it.316317**The camera fits `bounds`**, falling back to the whole world square — never318`surface`. A lens may declare both, and they are not interchangeable: `bounds`319is the raster window the pyramid fills, `surface` is the ground that window320pictures, which on a split sheet is smaller because the window was grown to321take in a title drawn beside the map. The reader is shown everything the lens322drew; `surface` is what anything *dividing* the world measures, and that323reading belongs to the analysis lane's `Ground`.324325### 6.2 The twelve layers326327| z | layer | source | why it is there |328|---|---|---|---|329| 0 | `raster` | complete levels | the pyramid up to `fullZoom` |330| 1 | `rasterDetail` | patchy levels | rides on top so the complete pyramid shows through the gaps |331| 5 | `grid` | chosen cells | the held cell and its subdivision, **under** the pins |332| 6 | `zoneScrim` | one cut polygon | the dimming is seen *through*, not painted over |333| 10 | `zones` | paths and areas | the ground |334| 20 | `zoneTitles` | their names | at the curated policy |335| 40 | `pins` | markers | |336| 42 | `zonePins` | markers a highlight claimed | over the ground that claimed them |337| 44 | `zoneTitleDetail` | a revealed name | over the pins |338| 45 | `pinLabels` | names beside markers | decluttered |339| 48 | `gridContext` | dimmed neighbours | **over** the pins they dim |340| 50 | `priority` | promoted features | over everything, decluttered by nothing |341342### 6.3 The raster, overzoom, and coverage343344Two layers, because a pyramid is two different things. The base carries345`minZoom … fullZoom` and is complete. The detail carries everything above it346and renders **only below the complete level's resolution** — above that347threshold the base is whole, and asking the patchy pyramid for the same ground348doubles every request a fresh view makes.349350**Coverage is consulted before every request** (`docs/format.md` §6.3.1): a351level with no bitset is fully covered *inside the lens's window*, and outside352that window there is nothing. A denied tile returns no URL, and the parent is353drawn larger instead. `render/test/pyramid.test.ts` reproduces every corpus354tile inventory from bounds, coverage and formats alone.355356**Two overzoom levels** sit past the deepest tiles: the view's `maxZoom` is357`lens.maxZoom + 2`, and there the base layer's own tiles are drawn larger.358`lens.interpolate` decides how — smooth for a photograph, nearest-neighbour359for pixel art, because a hand-drawn map magnified with bilinear smoothing360stops being a drawing. *Every lens in the committed corpus declares `true`,361so the nearest-neighbour branch is pinned by no fixture.*362363### 6.4 Scrims, labels, declutter364365**Zone scrims** are one polygon over the world square with the highlighted366areas cut out of it, even-odd. Ring winding in a payload is nobody's promise,367so the outer ring and the holes are wound against each other before the fill368rule sees them.369370**Label ladders** are decided by the application and drawn here. An area's371name speaks when its collection's policy is `always` — `atlas.label.policy`,372absent meaning `always` for areas — and the reader's override in the scene373wins over the curation. Holding **Z** reveals what is *optional* and never374revives what someone chose to quiet; that asymmetry is why the override is375consulted first.376377**Declutter** runs on the label layer, ordered by priority: a name that has to378give way gives way to a rarer collection's, never to whichever was built379first. The promoted layer is not decluttered at all, which is the380selected/searched bypass.381382### 6.5 Shard-crossing view carry383384Swapping to a lens that draws another layer of the same split world **keeps385the camera exactly where it was**. The reader stepped between floors of one386building, not into another world, and a refit would throw away the place they387had found. A world change refits (or restores the reported camera); a lens388change does not.389390### 6.6 The corner overview391392The application renders the shelf and the two surfaces. The seam composites393the world **once per lens**, from the shallowest pyramid level whose picture394of the lens's own window is at least 168 pixels across — a lens filling a395quarter of the square goes two levels deeper for the same number of pixels —396and afterwards only the rectangle moves. The rectangle is written in whole397pixels, because sub-pixel drift between two runs of one tour means nothing.398399While the sphere is up the chart has not moved, so the globe hands the locator400its own extent instead: the locator is the one place the globe's camera is401ever written down.402403---404405## 7. The globe406407Offered only where the world declares `atlas.geometry.surface: sphere` **and**408the flattening inverts. Equirectangular is the one projection that drapes a409sphere without resampling, which is why the globe asks for it by name.410411**Nothing is built until the sphere is entered.** A WebGL context, a texture412and two thousand meshes are not what a reader looking at a chart asked for,413and the `__atlasGlobe` global's absence until then is the observable (§8.3).414415**Four budgets**, each a decision rather than a limit:416417- **The base skin** — one 4096 × 2048 canvas texture, composited once per lens418 from the shallowest complete level worth showing.419- **The detail** — composited into that same texture, under the camera only,420 and only when the camera asks for more than the skin already has. A sphere421 seen whole shows the base skin and nothing else, which is what makes "past422 the base skin's depth, tiles actually arrive" checkable. A put-away globe423 keeps no pyramid tiles; an in-flight composite is cancelled rather than424 allowed to finish into a neighbourhood nobody is over.425- **Labels** — raised only while Z is held, at most 180, in priority order.426- **Sprites** — one per pin, built once, afterwards only shown or hidden, so a427 filter costs a boolean per pin.428429**Zones ride the sphere as the chart draws them.** Every shape the legend430lets through draws its ground and its rings in the same per-feature accent431its chart drawing and its index row wear: the fill faint at rest and just432over a quarter opaque when highlighted or selected, the outlines — outer433rings and holes alike, paths open — thicker and opaque when emphasized, and434an emphasized shape with a title wearing a name card at its centre, the435sphere's spelling of the chart's promoted area title. Ground is triangulated436part by part against its holes and every wide triangle is cut down before437its vertices are lifted to the sphere; segments are subdivided by span438exactly as the grid's boundaries are — both for the one reason, that a chord439between far corners passes through the planet. The layer sits above the440grid's fills and below its boundaries (`ZONE_FILL_RADIUS`, `ZONE_RADIUS`).441Geometry rebuilds only when the world or the filter moves; a highlight or a442selection restyles in place, and selecting a shape turns the planet to show443it whole at the altitude its span asks for — the sphere's spelling of the444chart's fit. The scrim stays the chart's (its even-odd hole-punching is a445flat sheet's trick), and the sphere's hit test remains points-only (§10).446447**The camera round trip** is the pane's one contract with the chart. Position448is invertible arithmetic through the declared mapping; the distance is a449**calibrated power law, not a field-of-view calculation**:450451```452lat, lng = mapping.toLatLng(camera.x, −camera.y) and toWorld back, y negated453altitude(zoom) = clamp(2.5 / 2^(zoom − 2), 0.08, 4)454zoom(altitude) = clamp(2 + log2(2.5 / max(altitude, 0.04)), 0, lens.maxZoom + 2)455rotation = 0, always456```457458Deriving the altitude from the viewport height, the resolution and the459equirect pixel span would be defensible arithmetic and is not what this460pairing is; the power law and its clamps are the contract (§10). The clamps461are load-bearing rather than hygiene.462463A flip to the sphere and straight back hands the chart the camera it was given464— the **identical object**, when latitude, longitude and altitude have all465moved less than 1e-9, rather than numbers that round-trip to within a float of466them — and otherwise inverts honestly.467468---469470## 8. The diagnostics duty471472The seam publishes what only a renderer can know, under stable names473(`render/diagnostics.ts` is their home). A snapshot is two halves: the474application's (the session island, `docs/app.md` §6) and the seam's. Neither475half guesses at the other's keys.476477### 8.1 The globals478479| Global | Shape | Who reads it |480|---|---|---|481| `__atlasSeam.snapshot()` | §8.2 | the canonical entry point |482| `__atlasDebug.snapshot()` | the same object | an alias for drivers that call this name |483| `render_game_to_text()` | JSON of `__atlasAppDiagnostics()` merged under the seam's snapshot | a headless driver |484| `advanceTime()` | forces a synchronous frame | a headless driver's settle loop |485| `__atlasGlobe` | §8.3 — **absent until the sphere is built** | the sphere's own diagnostics |486487`__atlasAppDiagnostics` is the hook the application half plugs into. Nothing488in the seam defines it; if it is absent, `render_game_to_text()` is the seam's489half alone.490491### 8.2 `__atlasSeam.snapshot()`492493`coordinateSystem`, `world`, `lens`, `zoom`, `center`, `resolution`,494`nativeMaxZoom`, `maxZoom`, `interpolate`,495`tileStats{requested,loaded,errors,peakPending}`, `pins`,496`eligibleLocations`, `domNodes`, `canvases`, `rasterCacheSize` (64),497`labelsHeld`, `hoveredPin`, `selectedPin`, `fitZoom`,498`zones{visible,count,focused,highlighted,focusedPins}`,499`sync{drawn,listable}`,500`grid{enabled,system,prefix,maximumDepth,extent,cells,priorityPins}`.501502`tileStats` counts tiles **since the lens was chosen**; `requested` and503`loaded` measure the route two runs took rather than the destination and are504not equality-checked. `grid.cells` is one object per plan cell —505`{hash, extent, role, count, contextDistance}` — read back out of the two506vector sources, chosen path first and dimmed context after, in a stable507order. Every planned cell appears whether or not it painted:508what the grid *holds* and what it *paints* are two questions.509510### 8.3 `__atlasGlobe`511512```ts513{ detail: { lens: string, tiles: Map<"z/x/y", true> },514 grid: { group: Object3D | null, cell: string | null, fitKey: string },515 labels: { key: string, group: Object3D | null },516 sprites: Map<featureId, { visible: boolean }> }517```518519Counts, never handles. `labels.key` is the sphere's own rounding of its520camera — `lat:lng:altitude:system:cell` — and is non-empty only while Z is521down.522523---524525## 9. Building and running526527```sh528make seam # esbuild → render/dist/app.js529make static # → dist/static/app.js, the tree a -static mount wants530make serve-static # the application with the seam mounted531make render-lane # boundary rules + tsc + tests + line budget532npm run --silent watch --workspace @atlas/render # atlas dev -seam-watch533```534535The stylesheet is deliberately not in the static tree: the CSS is the536application's own asset, served from `/assets`, so deleting this lane costs a537page one script tag rather than its chrome.538539---540541## 10. What is not proven yet542543Recorded here rather than papered over.544545- **No browser test drives the panes.** The lane's own suite546 (`render/test/`) judges the models — projection, pyramids, visibility,547 picks, grid extents, the globe's decisions — over stated fixtures and the548 corpus; `tests/e2e` asserts arrangement, never pixels. What a real pointer549 over a real canvas does is exercised by hand, not by a gate.550- **The reconcile path is verified by unit tests and by hand, not by a551 browser tour.** Patching the state node in place and replacing it whole552 both land: a search narrows the standing set, a `<data>` child hides a553 collection, a replaced node plus one `atlas:rescan` opens the grid on the554 held cell, and the selected feature survives the cell's cull. What is555 still missing is a browser test that walks it every time.556- **The globe's altitude pairing is calibrated.** It is a power law anchored at557 one point, not a field-of-view calculation (§7):558559 ```560 one zoom press = altitude ÷ 2^±1, read off the camera, eased over 180 ms561 ```562563 The whole disc at altitude 2.5 reads as the whole chart at zoom 2, and each564 halving of altitude reads as one more zoom. The clamps are load-bearing565 rather than hygiene: the farthest distance is what the whole-disc reading566 stands on.567568 The locator's mark is calibrated with it, and it is a **dot, not a569 rectangle**: a fixed 22 × 22 box centred on the point the camera faces,570 written in the canvas's own pixels — the world square is 8,192 across, the571 locator composites it at 256 pixels, and a camera over the middle lands at572 `0.5 × 256 − 11 = 117`. The box never changes size however close the camera573 comes, because half a sphere is out of sight whatever the camera does and a574 true-to-scale rectangle would be saying something false.575- **Nearest-neighbour resampling is pinned by no fixture.** Every lens in the576 committed corpus declares `interpolate: true`.577- **Canvas hover is implemented and unverified.** A pointer that moves578 without pressing, and the styling that answers it, is held by no test;579 picks are (`render/test/pick.test.ts`).580- **The seam's bundle is 2.3 MB.** globe.gl brings three, d3 and turf. It is a581 desktop application served from its own binary, so this is a note rather582 than a problem — but it is the obvious thing to measure first if it ever583 becomes one.584585## 10.1 What this document does not give you586587§§1–9 are enough to build a seam that is *correct* — right contracts, right588flows, right arithmetic, right budgets. They are **not** enough to build one589that draws and reports exactly what this one does, because the code is full of590numbers this document does not carry. The gap is named here rather than left591for the next person to discover step by step.592593What a rewriter would still have to take from `render/` itself:594595- **Every drawn dimension.** Pin radii by state, rim stroke widths, icon pixel596 size and anchor, label fonts and halo widths, area fill opacities, sprite597 geometry and colours on the sphere. None of it is here, and almost nothing598 checks pixels, so a rewrite could differ in all of it and pass.599- **`atlas.icon.outset`.** §2 does not mention it; the seam reads the world's600 declaration and uses it for the pin rim. *It also uses it wrong* — the601 `light`/`dark` vocabulary is passed to the renderer as a literal colour602 string, and the colour table beside it is dead code. A rewriter following the603 semantics would draw different rims from the ones shipping today, and would604 be right to.605- **`atlas.render.as`.** The seam reads `pin` and `text` and nothing else, and606 today `text` only forces a collection's labels always-on — the "name and no607 marker" branch exists and is never reached. The registry's meaning and the608 seam's behaviour do not agree; the registry is the one to build to.609- **Search semantics.** Case-insensitive substring over a feature's `title`610 alone; a non-empty query is a hard filter on points and no filter at all on611 shapes, which nevertheless drop out of the *listable* count.612- **Hit testing.** Four CSS pixels of tolerance, resolved top-down through the613 z-order of §6.2 so a promoted pin beats a plain pin beats ground; a hidden614 feature does not swallow the hit. On the sphere it is nearest standing point615 within roughly two degrees. Hover shares the chart's rule and changes only616 the pin's radius — it deliberately does *not* rebuild the standing set.617- **Declutter is OpenLayers' own**, enabled on exactly one layer (`pinLabels`),618 ordered by the label style's `zIndex`. "Ordered by priority" in §6.4 is that619 zIndex, not a hand-written pass.620- **`members` in the priority formula** (§4.2) is the number of packed rows621 sharing a point's `owner` column — counted over the whole `.bin`, across622 every shard, before any filter. Not the standing count, not the legend's.623- **The overview's 168** (§6.6) is a *level-selection* threshold, not a canvas624 size: the canvas is the lens's window at the chosen level, which for a625 full-world lens on the standard grid is 256 px — the number §10's locator626 arithmetic quietly depends on.627- **Diagnostics that are not what their names suggest.** `fitZoom` is captured628 once, when a lens is opened fresh, and never recomputed. `zones.focused` is629 deliberately *not* emitted by the seam — the application's half supplies it630 and a seam that emitted `null` would clobber it. `grid.maximumDepth` reports631 the default system's depth even with the grid off. `sync.listable` counts632 only shapes that have a title.633- **The chart `View`'s explicit resolution ladder.** Enumerating634 `size/tileSize / 2^z` per level rather than deriving from a max and a factor635 is what makes two implementations' cameras *equal* rather than close.636- **The rest of the constants**: focus zoom 4 and the 220 ms flight, the637 "steered" thresholds that decide whether a closing card gives the camera638 back, the grid fit's 52 px padding, the 46 × 23 px test that decides whether639 a cell wears its label, the raster caches at 64, the renderer buffers, and640 the globe's texture size, detail budget and horizon arithmetic.641642Two of those are contract-level rather than cosmetic and are the ones to write643down next, in this document: the outset vocabulary and `atlas.render.as`, both644of which are `format/semconv` keys whose *meaning* is normative even where the645current seam's reading of them is not.646647## 11. Where each named behaviour lives648649| Behaviour (issue #5 §5.5) | Module | Verified by |650|---|---|---|651| `ATLAS:PIXELS` projection | `chart/projection.ts` | `test/projection.test.ts` |652| 12-layer z-order | `chart/element.ts` | reading |653| overzoom + parent upsampling | `chart/raster.ts` | `test/raster.test.ts` |654| coverage bitsets honoured | `data/pyramid.ts` | `test/pyramid.test.ts` (the corpus inventories) |655| nearest vs smooth | `chart/raster.ts` | `test/raster.test.ts`; payload-faithful, no fixture pins the nearest branch |656| priority declutter + bypass | `chart/styles.ts`, `world/visibility.ts` | `test/visibility.test.ts` |657| zone scrims, even-odd | `chart/element.ts` | `test/element.test.ts` |658| shard-crossing view carry | `chart/element.ts` | `test/visibility.test.ts` (the shard half) |659| the corner overview | `chart/overview.ts` | reading |660| globe equirect compositing | `globe/texture.ts` | `test/texture.test.ts` |661| detail-tile + label budgets | `globe/element.ts` | `test/globe.test.ts` (the label budget); reading for the detail tiles |662| camera round trip | `globe/element.ts` | `test/globe.test.ts` |663| grid token vocabulary | `chart/grid.ts` | `test/grid.test.ts`, hand-derived extents |