Grid layout transitions — v15 sketch (cleanup / simplification pass) (Firewatch Camera Grid)

**Note** from Bead: Firewatch Camera Grid · [canonical source](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/notes/09-grid-layout-v15-sketch.md) · session 2026-06-24 · discussion: Talk: Firewatch Camera Grid

**Rendered:** https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v15.html Sketched by **Debbie** (senior UX/UI). v15 is a **refinement** pass on [v14](./08-grid-layout-v14-sketch.md), per Stephen's review: the mechanism has converged, so this is the Tufte lens — **remove extraneous ink**, fix two real bugs, and say the one open question in plain language. **No new concepts were added.**

## What was removed (and why — reduce extraneous ink, Tufte) Each of these was clutter or non-functional; deleted from both markup and logic, not just hidden: - **The "bug the team caught" panel** — a historical war-story that no longer earns its space; the demonstration (NAIVE↔POSITION, crossings→0) already carries the point. - **The "this panel IS the processor" dashboard blurb** — implementation trivia that distracted from reading the team's votes. The dashboard itself stays. - **The "spatial reading retained" gauge** — Stephen found it added confusion, not clarity. Removed the gauge UI **and** its logic (`spatialRetention()`, `updateRetGauge()`, the per-frame call). - **The fullscreen button** — it never worked. Removed the button, its handler, and the `fullscreen` state. - **The TIME cursor strip** — non-functional here and cluttering. Removed the slider + clock and all wiring; the camera azimuths that were wobbled by time are now **static**. - **The 3D / oblique map mode** — wasn't adding anything. The segmented toggle is now just **GRID | MAP** (2D top-down). Removed the oblique `project()` tilt / horizon, the `is3D` branches, and the `3D` button. The morph is simply **map (2D) ⇄ grid**.

## The two bug fixes ### 1. Viewport containment (the implicit selection leaked off-screen cameras) The map SVG renders with `preserveAspectRatio="xMidYMid slice"`, so it is scaled to **cover** the stage and the longer axis is **cropped**. v14's `inViewBox()` tested each camera's point against the raw **600×400 viewBox**, which is *not* the visible rectangle under slice — so cropped, off-screen cameras still passed the test and inflated the `auto: N in view` count. **Fix:** the visible map rectangle is exactly the stage's own client rect. v15 tests the camera's dot in **stage-pixel coordinates** (`mapDotPx()`, which already folds in the slice scaling) against `[0, clientWidth] × [0, clientHeight]`. A camera panned off-screen now correctly drops out, and the count matches what's drawn. *Sanity check (harness mirroring the math, stage 1000×560, default view):* C8 lands at stage-x ≈ 1055 (> 1000) → out; C7 at y ≈ −28 (< 0) → out; C10 at y ≈ 625 (> 560) → out; C3 (near scene) → in. Pan right (cx 470→950): C8 comes in, C3 stays, the left cameras stay out. Matches expectation; the raw-viewBox test would have wrongly kept the cropped ones. ### 2. Panel mode keeps the map LIVE (+ the card→map arc) In panel mode (grid + map side by side) the map is no longer a frozen, ghost-dimmed snapshot. A single gate `mapInteractive()` (= `panelMode || t < 0.5`) now drives pan, wheel-zoom, and the zoom buttons; in panel mode the basemap stays at full opacity and the zoom controls stay live. `pressInMapRegion()` restricts the gesture to the **left (map) half**, so the right (grid) half stays free for card drag/re-arrange and the two never collide. Panning the live map re-filters the in-view set, so the cameras drawn on it stay current. The **card→map arc** (the curved leader-line drawn on hover/brush to a camera's dot) is retained — it draws for `t ≥ 0.55`, which panel mode (t≈1) satisfies, so hovering a grid card still throws its arc to the map.

## The grid-shape fork — re-ordered A/B/C, plain language (no "aspect") Stephen didn't understand "aspect." The underlying question is just: *should the grid's rows and columns copy how the cameras are spread out in the real world?* Re-ordered to **A, B, C** (was A, C, B) and reworded — visible copy never says "aspect": - **A — Match the camera layout.** Rows and columns mirror how the cameras are spread: side-to-side cameras → one wide row; stacked cameras → a tall column. Strongest spatial match, but can produce awkward shapes (one long row of tiny tiles). - **B — Tidy grid.** Always pack into a compact grid (up to ~3 across), read left-to-right, top-to-bottom, ignoring the world layout. Predictable and space-efficient, but a card's spot no longer tells you where its camera is. - **C — Match, but keep it readable (recommended).** Mirror the layout like A, but cap how wide or tall it can get so the tiles stay a usable size. It stays a real three-way fork answerable in the existing `feedback/` ballot box (`sketch:'grid-layout-v15'`). The choice **keys are unchanged** (`aspect-follows` / `fixed-cols` / `aspect-floored`) so the dashboard still tallies these and folds in prior votes via the aliases; only the user-facing labels/order changed. The dashboard tally rows are now ordered A/B/C with plain labels.

## Kept intact (not regressed) Single-scene `lerp(mapPos,gridPos,t)` morph with the MAP◀▶GRID scrubber; position-preserving placement (left-stays-left, crossings→0); dim-ghost basemap; viewport-implicit selection (now fixed); 2D drag-pan + wheel-zoom; per-card un-pin / re-flow; OrientedMap glyphs + tie-point ray; identity color; the 5s-polling results dashboard; onboarding overlay + Buxton link; "Redfish Team" label; the whole feedback ballot-box auth (own → ballot-token → last-resort modal). 180° axis stays gone.

## Verified - `node --check` on the extracted inline script: **PASS**. - Viewport-containment math sanity-checked in a standalone harness (numbers above). - Read-back of the file: no fullscreen button/handler, no TIME strip, no `3D` button or `is3D` oblique branches, no gauge, no "bug the team caught" / "panel is the processor" copy; segmented toggle is GRID | MAP only; fork order is A → B → C in plain language (no "aspect" in any visible copy); top bar reads v15 / build 2026-06-26. - *Not run* (no deploy this step; parent publishes): the live incognito UI end-to-end.

## Status **refinement** — cleanup + two bug fixes + clearer copy on the converged v14 design. Recommended fork **C**. Awaiting the team's vote in the [ballot box](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/feedback/).