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. `schema.json` describes every180packed column under `data/*.pack`; those columns reconstruct Volume → World →181FeatureSets/Features, RasterPyramids, Presentation and Assets. Presentation is182then applied to the semantic data without a document or GeoJSON intermediary.183184Geometry is already expressed in the world's declared CoordinateSpace. The185standard `atlas:tile-plane` is y-down pixel space; the renderer only flips y186once at its OpenLayers boundary. Projection from source longitude/latitude is187a producer responsibility and therefore part of the stamped data, not an188implicit reader convention.189190### 4.2 The standing set191192One object per scene answers what stands, and the chart, the sphere, the picks193and the diagnostics all read it. The rules, in order:1941951. **Shard.** A split world offers one layer at a time. A feature on another196 shard is *elsewhere in the world*, not filtered: not drawn, not counted,197 not picked.1982. **Filtered.** Its collection is hidden, or the search does not name it.1993. **Highlights.** Highlighted shapes **conjoin across collections and unite200 within one**: a pin stands only where every collection holding a highlight201 claims it. Containment is boundary-inclusive with a unit of grace; a path202 claims what lies within half its declared ground width.2034. **Cell.** A held grid cell narrows exactly the way a highlight does.204205Rules 3 and 4 share one bypass: **the selected feature and anything the search206names are spared**. A card open about a feature must not lose it, and a207searched-for name is what the reader asked to see.208209**Promotion** is a separate question from standing: selected, hovered,210searched, or inside the held cell. A promoted feature is drawn again in the211layer above everything and is exempt from declutter.212213**Priority** is a pin's place in the crowd: `max(0, 1e6 − min(members,999)·1000)`214plus a stable hash of its id, mod 1000. Rarer collections first, ties broken215the same way on every run and in both panes.216217---218219## 5. What flows back220221**`atlas:pick`** — a `CustomEvent` raised **on the window**, non-bubbling,222dispatched by the pane that resolved the hit:223224```js225window.dispatchEvent(new CustomEvent("atlas:pick", { bubbles: false,226 detail: { feature: "1849", kind: "point" } })) // kind: point | path | area227```228229The seam resolves the hit and submits nothing: **the page turns it into an230ordinary `POST /session/select`**, and the new selection comes back as a new231scene. That is wired: the shell renders a hidden form,232`<form id="atlas-pick" hx-post="/session/select" hx-trigger="atlas:pick233from:window">`, whose one field the seam fills in before it raises the event234(`render/data/report.ts`, `internal/app/templates/shell.tmpl`). A pick off235either canvas therefore opens the card, exactly as a row in the dock does.236237Two details are contract rather than taste. **The window, and no bubbling**:238there is one listener for the whole page and it is on the window, so the event239is raised where it is heard rather than left to travel — a swap can carry the240form across at any moment, and an event that depended on the path from a241canvas to it would depend on a tree the application is free to reshape.242**A miss does not post.** Clicking open water is not a request to close what243the reader is reading, so an empty feature raises nothing at all; the card is244put away by Escape and by its own button. A pick event that fired on a miss245would clear the selection on every stray click246(`render/test/pick.test.ts` holds this decision).247248**The camera report** is `POST /session/view`, form-encoded249(`volume world x y zoom rotation`), debounced 400 ms after the camera settles,250answered `204` — but **the seam does not make the request**. It writes the251five values into the hidden inputs `#atlas-camera-world|-x|-y|-zoom|-rotation`252and dispatches `atlas:camera` on `window` (not bubbling); `shell.tmpl` carries253a hidden form with `hx-post="/session/view" hx-trigger="atlas:camera254from:window"` that does the posting. The seam therefore owns no request at255all, which is the stricter reading of "fetch lives in one place" and the reason256the seam never sees the answer. One report is unconditional at `show()`,257because a `view.fit()` with no animation raises no settle event and the opening258camera would otherwise never be told.259260That is the whole outward surface: two DOM events and a set of hidden inputs.261No request the page did not make, and no write to the scene node, ever.262263**Inbound, there is one.** After an htmx swap the seam must re-resolve the264scene node by selector, re-assert the chrome it wired, and rewrite the standing265count. `boot.ts` listens on `document.body` for `htmx:after:swap`,266`htmx:after:settle`, their htmx-2 camel-case spellings, and `atlas:rescan` —267the event a harness or a host raises to say "look again" without an htmx swap268behind it. All five call the same `rescan()`.269270**Only the pane the reader is looking through reports.** A chart put away271behind the sphere still has a camera, and it is not a view of anything: the272pane has no window, so a fit into it lands wherever a fit into nothing lands,273and saving that would reopen the volume somewhere nobody ever stood. The same274rule governs the footer's count and the corner locator — a pane with no window275answers no question about what it can see. Coming back up is what makes its276camera the reader's again.277278That is the whole outward surface. No other request, no other event, no279writes to the scene node, ever.280281---282283## 6. The chart284285### 6.1 `ATLAS:PIXELS`286287```288origin top-left of the world square289x increases right290y decreases downward: the square is [0, −size] … [size, 0]291resolution world pixels per screen pixel; level z is size / (tileSize · 2^z)292```293294`size` is 8192 and `tileSize` is 256 in every published bundle, so level 0 is295the square in one tile and zoom 0 is resolution 32. The banner string296297```298ATLAS:PIXELS; origin top-left; x increases right; y decreases downward299```300301is part of the diagnostics contract (§8.2). Never reword it.302303**The camera fits `bounds`**, falling back to the whole world square — never304`surface`. A lens may declare both, and they are not interchangeable: `bounds`305is the raster window the pyramid fills, `surface` is the ground that window306pictures, which on a split sheet is smaller because the window was grown to307take in a title drawn beside the map. The reader is shown everything the lens308drew; `surface` is what anything *dividing* the world measures, and that309reading belongs to the analysis lane's `Ground`.310311### 6.2 The twelve layers312313| z | layer | source | why it is there |314|---|---|---|---|315| 0 | `raster` | complete levels | the pyramid up to `fullZoom` |316| 1 | `rasterDetail` | patchy levels | rides on top so the complete pyramid shows through the gaps |317| 5 | `grid` | chosen cells | the held cell and its subdivision, **under** the pins |318| 6 | `zoneScrim` | one cut polygon | the dimming is seen *through*, not painted over |319| 10 | `zones` | paths and areas | the ground |320| 20 | `zoneTitles` | their names | at the curated policy |321| 40 | `pins` | markers | |322| 42 | `zonePins` | markers a highlight claimed | over the ground that claimed them |323| 44 | `zoneTitleDetail` | a revealed name | over the pins |324| 45 | `pinLabels` | names beside markers | decluttered |325| 48 | `gridContext` | dimmed neighbours | **over** the pins they dim |326| 50 | `priority` | promoted features | over everything, decluttered by nothing |327328### 6.3 The raster, overzoom, and coverage329330Two layers, because a pyramid is two different things. The base carries331`minZoom … fullZoom` and is complete. The detail carries everything above it332and renders **only below the complete level's resolution** — above that333threshold the base is whole, and asking the patchy pyramid for the same ground334doubles every request a fresh view makes.335336**Coverage is consulted before every request** (`docs/format.md` §6.3.1): a337level with no bitset is fully covered *inside the lens's window*, and outside338that window there is nothing. A denied tile returns no URL, and the parent is339drawn larger instead. `render/test/pyramid.test.ts` reproduces every corpus340tile inventory from bounds, coverage and formats alone.341342**Two overzoom levels** sit past the deepest tiles: the view's `maxZoom` is343`lens.maxZoom + 2`, and there the base layer's own tiles are drawn larger.344`lens.interpolate` decides how — smooth for a photograph, nearest-neighbour345for pixel art, because a hand-drawn map magnified with bilinear smoothing346stops being a drawing. *Every lens in the committed corpus declares `true`,347so the nearest-neighbour branch is pinned by no fixture.*348349### 6.4 Scrims, labels, declutter350351**Zone scrims** are one polygon over the world square with the highlighted352areas cut out of it, even-odd. Ring winding in a payload is nobody's promise,353so the outer ring and the holes are wound against each other before the fill354rule sees them.355356**Label ladders** are decided by the application and drawn here. An area's357name speaks when its collection's policy is `always` — `atlas.label.policy`,358absent meaning `always` for areas — and the reader's override in the scene359wins over the curation. Holding **Z** reveals what is *optional* and never360revives what someone chose to quiet; that asymmetry is why the override is361consulted first.362363**Declutter** runs on the label layer, ordered by priority: a name that has to364give way gives way to a rarer collection's, never to whichever was built365first. The promoted layer is not decluttered at all, which is the366selected/searched bypass.367368### 6.5 Shard-crossing view carry369370Swapping to a lens that draws another layer of the same split world **keeps371the camera exactly where it was**. The reader stepped between floors of one372building, not into another world, and a refit would throw away the place they373had found. A world change refits (or restores the reported camera); a lens374change does not.375376### 6.6 The corner overview377378The application renders the shelf and the two surfaces. The seam composites379the world **once per lens**, from the shallowest pyramid level whose picture380of the lens's own window is at least 168 pixels across — a lens filling a381quarter of the square goes two levels deeper for the same number of pixels —382and afterwards only the rectangle moves. The rectangle is written in whole383pixels, because sub-pixel drift between two runs of one tour means nothing.384385While the sphere is up the chart has not moved, so the globe hands the locator386its own extent instead: the locator is the one place the globe's camera is387ever written down.388389---390391## 7. The globe392393Offered only where the world declares `atlas.geometry.surface: sphere` **and**394the flattening inverts. Equirectangular is the one projection that drapes a395sphere without resampling, which is why the globe asks for it by name.396397**Nothing is built until the sphere is entered.** A WebGL context, a texture398and two thousand meshes are not what a reader looking at a chart asked for,399and the `__atlasGlobe` global's absence until then is the observable (§8.3).400401**Four budgets**, each a decision rather than a limit:402403- **The base skin** — one 4096 × 2048 canvas texture, composited once per lens404 from the shallowest complete level worth showing.405- **The detail** — composited into that same texture, under the camera only,406 and only when the camera asks for more than the skin already has. A sphere407 seen whole shows the base skin and nothing else, which is what makes "past408 the base skin's depth, tiles actually arrive" checkable. A put-away globe409 keeps no pyramid tiles; an in-flight composite is cancelled rather than410 allowed to finish into a neighbourhood nobody is over.411- **Labels** — raised only while Z is held, at most 180, in priority order.412- **Sprites** — one per pin, built once, afterwards only shown or hidden, so a413 filter costs a boolean per pin.414415**Zones ride the sphere as the chart draws them.** Every shape the legend416lets through draws its ground and its rings in the same per-feature accent417its chart drawing and its index row wear: the fill faint at rest and just418over a quarter opaque when highlighted or selected, the outlines — outer419rings and holes alike, paths open — thicker and opaque when emphasized, and420an emphasized shape with a title wearing a name card at its centre, the421sphere's spelling of the chart's promoted area title. Ground is triangulated422part by part against its holes and every wide triangle is cut down before423its vertices are lifted to the sphere; segments are subdivided by span424exactly as the grid's boundaries are — both for the one reason, that a chord425between far corners passes through the planet. The layer sits above the426grid's fills and below its boundaries (`ZONE_FILL_RADIUS`, `ZONE_RADIUS`).427Geometry rebuilds only when the world or the filter moves; a highlight or a428selection restyles in place, and selecting a shape turns the planet to show429it whole at the altitude its span asks for — the sphere's spelling of the430chart's fit. The scrim stays the chart's (its even-odd hole-punching is a431flat sheet's trick), and the sphere's hit test remains points-only (§10).432433**The camera round trip** is the pane's one contract with the chart. Position434is invertible arithmetic through the declared mapping; the distance is a435**calibrated power law, not a field-of-view calculation**:436437```438lat, lng = mapping.toLatLng(camera.x, −camera.y) and toWorld back, y negated439altitude(zoom) = clamp(2.5 / 2^(zoom − 2), 0.08, 4)440zoom(altitude) = clamp(2 + log2(2.5 / max(altitude, 0.04)), 0, lens.maxZoom + 2)441rotation = 0, always442```443444Deriving the altitude from the viewport height, the resolution and the445equirect pixel span would be defensible arithmetic and is not what this446pairing is; the power law and its clamps are the contract (§10). The clamps447are load-bearing rather than hygiene.448449A flip to the sphere and straight back hands the chart the camera it was given450— the **identical object**, when latitude, longitude and altitude have all451moved less than 1e-9, rather than numbers that round-trip to within a float of452them — and otherwise inverts honestly.453454---455456## 8. The diagnostics duty457458The seam publishes what only a renderer can know, under stable names459(`render/diagnostics.ts` is their home). A snapshot is two halves: the460application's (the session island, `docs/app.md` §6) and the seam's. Neither461half guesses at the other's keys.462463### 8.1 The globals464465| Global | Shape | Who reads it |466|---|---|---|467| `__atlasSeam.snapshot()` | §8.2 | the canonical entry point |468| `__atlasDebug.snapshot()` | the same object | an alias for drivers that call this name |469| `render_game_to_text()` | JSON of `__atlasAppDiagnostics()` merged under the seam's snapshot | a headless driver |470| `advanceTime()` | forces a synchronous frame | a headless driver's settle loop |471| `__atlasGlobe` | §8.3 — **absent until the sphere is built** | the sphere's own diagnostics |472473`__atlasAppDiagnostics` is the hook the application half plugs into. Nothing474in the seam defines it; if it is absent, `render_game_to_text()` is the seam's475half alone.476477### 8.2 `__atlasSeam.snapshot()`478479`coordinateSystem`, `world`, `lens`, `zoom`, `center`, `resolution`,480`nativeMaxZoom`, `maxZoom`, `interpolate`,481`tileStats{requested,loaded,errors,peakPending}`, `pins`,482`eligibleLocations`, `domNodes`, `canvases`, `rasterCacheSize` (64),483`labelsHeld`, `hoveredPin`, `selectedPin`, `fitZoom`,484`zones{visible,count,focused,highlighted,focusedPins}`,485`sync{drawn,listable}`,486`grid{enabled,system,prefix,maximumDepth,extent,cells,priorityPins}`.487488`tileStats` counts tiles **since the lens was chosen**; `requested` and489`loaded` measure the route two runs took rather than the destination and are490not equality-checked. `grid.cells` is one object per plan cell —491`{hash, extent, role, count, contextDistance}` — read back out of the two492vector sources, chosen path first and dimmed context after, in a stable493order. Every planned cell appears whether or not it painted:494what the grid *holds* and what it *paints* are two questions.495496### 8.3 `__atlasGlobe`497498```ts499{ detail: { lens: string, tiles: Map<"z/x/y", true> },500 grid: { group: Object3D | null, cell: string | null, fitKey: string },501 labels: { key: string, group: Object3D | null },502 sprites: Map<featureId, { visible: boolean }> }503```504505Counts, never handles. `labels.key` is the sphere's own rounding of its506camera — `lat:lng:altitude:system:cell` — and is non-empty only while Z is507down.508509---510511## 9. Building and running512513```sh514make seam # esbuild → render/dist/app.js515make static # → dist/static/app.js, the tree a -static mount wants516make serve-static # the application with the seam mounted517make render-lane # boundary rules + tsc + tests + line budget518npm run --silent watch --workspace @atlas/render # atlas dev -seam-watch519```520521The stylesheet is deliberately not in the static tree: the CSS is the522application's own asset, served from `/assets`, so deleting this lane costs a523page one script tag rather than its chrome.524525---526527## 10. What is not proven yet528529Recorded here rather than papered over.530531- **No browser test drives the panes.** The lane's own suite532 (`render/test/`) judges the models — projection, pyramids, visibility,533 picks, grid extents, the globe's decisions — over stated fixtures and the534 corpus; `tests/e2e` asserts arrangement, never pixels. What a real pointer535 over a real canvas does is exercised by hand, not by a gate.536- **The reconcile path is verified by unit tests and by hand, not by a537 browser tour.** Patching the state node in place and replacing it whole538 both land: a search narrows the standing set, a `<data>` child hides a539 collection, a replaced node plus one `atlas:rescan` opens the grid on the540 held cell, and the selected feature survives the cell's cull. What is541 still missing is a browser test that walks it every time.542- **The globe's altitude pairing is calibrated.** It is a power law anchored at543 one point, not a field-of-view calculation (§7):544545 ```546 one zoom press = altitude ÷ 2^±1, read off the camera, eased over 180 ms547 ```548549 The whole disc at altitude 2.5 reads as the whole chart at zoom 2, and each550 halving of altitude reads as one more zoom. The clamps are load-bearing551 rather than hygiene: the farthest distance is what the whole-disc reading552 stands on.553554 The locator's mark is calibrated with it, and it is a **dot, not a555 rectangle**: a fixed 22 × 22 box centred on the point the camera faces,556 written in the canvas's own pixels — the world square is 8,192 across, the557 locator composites it at 256 pixels, and a camera over the middle lands at558 `0.5 × 256 − 11 = 117`. The box never changes size however close the camera559 comes, because half a sphere is out of sight whatever the camera does and a560 true-to-scale rectangle would be saying something false.561- **Nearest-neighbour resampling is pinned by no fixture.** Every lens in the562 committed corpus declares `interpolate: true`.563- **Canvas hover is implemented and unverified.** A pointer that moves564 without pressing, and the styling that answers it, is held by no test;565 picks are (`render/test/pick.test.ts`).566- **The seam's bundle is 2.3 MB.** globe.gl brings three, d3 and turf. It is a567 desktop application served from its own binary, so this is a note rather568 than a problem — but it is the obvious thing to measure first if it ever569 becomes one.570571## 10.1 What this document does not give you572573§§1–9 are enough to build a seam that is *correct* — right contracts, right574flows, right arithmetic, right budgets. They are **not** enough to build one575that draws and reports exactly what this one does, because the code is full of576numbers this document does not carry. The gap is named here rather than left577for the next person to discover step by step.578579What a rewriter would still have to take from `render/` itself:580581- **Every drawn dimension.** Pin radii by state, rim stroke widths, icon pixel582 size and anchor, label fonts and halo widths, area fill opacities, sprite583 geometry and colours on the sphere. None of it is here, and almost nothing584 checks pixels, so a rewrite could differ in all of it and pass.585- **`atlas.icon.outset`.** §2 does not mention it; the seam reads the world's586 declaration and uses it for the pin rim. *It also uses it wrong* — the587 `light`/`dark` vocabulary is passed to the renderer as a literal colour588 string, and the colour table beside it is dead code. A rewriter following the589 semantics would draw different rims from the ones shipping today, and would590 be right to.591- **`atlas.render.as`.** The seam reads `pin` and `text` and nothing else, and592 today `text` only forces a collection's labels always-on — the "name and no593 marker" branch exists and is never reached. The registry's meaning and the594 seam's behaviour do not agree; the registry is the one to build to.595- **Search semantics.** Case-insensitive substring over a feature's `title`596 alone; a non-empty query is a hard filter on points and no filter at all on597 shapes, which nevertheless drop out of the *listable* count.598- **Hit testing.** Four CSS pixels of tolerance, resolved top-down through the599 z-order of §6.2 so a promoted pin beats a plain pin beats ground; a hidden600 feature does not swallow the hit. On the sphere it is nearest standing point601 within roughly two degrees. Hover shares the chart's rule and changes only602 the pin's radius — it deliberately does *not* rebuild the standing set.603- **Declutter is OpenLayers' own**, enabled on exactly one layer (`pinLabels`),604 ordered by the label style's `zIndex`. "Ordered by priority" in §6.4 is that605 zIndex, not a hand-written pass.606- **`members` in the priority formula** (§4.2) is the number of packed rows607 sharing a point's `owner` column — counted over the whole `.bin`, across608 every shard, before any filter. Not the standing count, not the legend's.609- **The overview's 168** (§6.6) is a *level-selection* threshold, not a canvas610 size: the canvas is the lens's window at the chosen level, which for a611 full-world lens on the standard grid is 256 px — the number §10's locator612 arithmetic quietly depends on.613- **Diagnostics that are not what their names suggest.** `fitZoom` is captured614 once, when a lens is opened fresh, and never recomputed. `zones.focused` is615 deliberately *not* emitted by the seam — the application's half supplies it616 and a seam that emitted `null` would clobber it. `grid.maximumDepth` reports617 the default system's depth even with the grid off. `sync.listable` counts618 only shapes that have a title.619- **The chart `View`'s explicit resolution ladder.** Enumerating620 `size/tileSize / 2^z` per level rather than deriving from a max and a factor621 is what makes two implementations' cameras *equal* rather than close.622- **The rest of the constants**: focus zoom 4 and the 220 ms flight, the623 "steered" thresholds that decide whether a closing card gives the camera624 back, the grid fit's 52 px padding, the 46 × 23 px test that decides whether625 a cell wears its label, the raster caches at 64, the renderer buffers, and626 the globe's texture size, detail budget and horizon arithmetic.627628Two of those are contract-level rather than cosmetic and are the ones to write629down next, in this document: the outset vocabulary and `atlas.render.as`, both630of which are `format/semconv` keys whose *meaning* is normative even where the631current seam's reading of them is not.632633## 11. Where each named behaviour lives634635| Behaviour (issue #5 §5.5) | Module | Verified by |636|---|---|---|637| `ATLAS:PIXELS` projection | `chart/projection.ts` | `test/projection.test.ts` |638| 12-layer z-order | `chart/element.ts` | reading |639| overzoom + parent upsampling | `chart/raster.ts` | `test/raster.test.ts` |640| coverage bitsets honoured | `data/pyramid.ts` | `test/pyramid.test.ts` (the corpus inventories) |641| nearest vs smooth | `chart/raster.ts` | `test/raster.test.ts`; payload-faithful, no fixture pins the nearest branch |642| priority declutter + bypass | `chart/styles.ts`, `world/visibility.ts` | `test/visibility.test.ts` |643| zone scrims, even-odd | `chart/element.ts` | `test/element.test.ts` |644| shard-crossing view carry | `chart/element.ts` | `test/visibility.test.ts` (the shard half) |645| the corner overview | `chart/overview.ts` | reading |646| globe equirect compositing | `globe/texture.ts` | `test/texture.test.ts` |647| detail-tile + label budgets | `globe/element.ts` | `test/globe.test.ts` (the label budget); reading for the detail tiles |648| camera round trip | `globe/element.ts` | `test/globe.test.ts` |649| grid token vocabulary | `chart/grid.ts` | `test/grid.test.ts`, hand-derived extents |