Chat: Step0 Build (Taos Engine)

**Chat log** from Bead: Taos Engine · [canonical source](https://redfish.acequia.io/guerin/.agents/c66cbd1d-453c-41f8-8440-179502f25de4/2026-06-09/chats/2026-06-10-step0-build.md) · session 2026-06-09 · discussion: Talk: Taos Engine *Verbatim chat log: immutable provenance. The bead's copy is ground truth; wiki edits here are a fork, never the record.*

Bead `#taos-engine` (c66cbd1d-453c-41f8-8440-179502f25de4). Fresh bearer-writer agent, no prior context except the build brief. BUILDING (step-0), not planning.

## Prompt (Stephen, via brief) > You are a fresh "bearer writer" agent operating bead #taos-engine. No prior context > except this brief. Today is 2026-06-10. You are BUILDING now (step-0), not just planning. > > **Bead:** #taos-engine, GUID c66cbd1d-…, local origin …/c66cbd1d-…/. Write only inside > this bead's subdirs; absolute https:// URLs; keep a chat log incrementally. > > **Orient:** beads skill; the build plan + the projector-intrinsics note (use the > intrinsics mapping verbatim); the REAL TaosEngine source cloned at > `c:/Users/steph/Downloads/TaosEngine-src` (read `samples/geo_osm_projection.ts`, > `geo_osm_terrain.ts`, `geo_osm.ts`, `projector_test.ts`, `src/engine/components/projector.ts`, > `src/geo/*`, `src/index.ts`); the MISB data — pick ONE clip from > `apps/viewer-3d/incidents.json`, fetch its telemetry, parse ONE sample (sensor > lat/lon/alt(MSL), frame_center lat/lon/elev, HFOV+VFOV, slant_range). > > **Build step-0:** ONE Projector at a single MISB sample's pose + intrinsics, casting a > texture onto raster-DEM terrain so the image lands on the ground at the correct > footprint (near frame-center). Texture content secondary (still frame / test-grid). > Implement as a NEW sample in the cloned repo: `samples/misb_step0.ts` + `.html` (copy > `geo_osm_projection.html`). Token-free raster-DEM terrain + imagery. Projector: > position = sensor ECEF; forward(−Z) = normalize(frameCenterECEF − sensorECEF) (port the > sample-local `lookRotation`); intrinsics verbatim: focalLength=null, fovY=VFOV, > aspect=tan(HFOV/2)/tan(VFOV/2), near/far bracketing slant_range; shape:'perspective'; > still texture source. Place the fly camera behind/above the sensor looking at > frame-center. Log sensorECEF, direction, fovY, aspect, footprint to console. > > **Run + verify:** `npm install`, `npm run dev` in background, open the page. WebGPU may > not render headless — if so, don't burn the run; verify (a) page loads with no > console/Vite/TS errors, (b) logged pose/intrinsics numerically sane. Stop the dev > server when done. > > **Record:** copy the sample into artifacts/step-0/ with a README; chat log; about.md line.

## What I did 1. **Oriented.** Read the build plan (`notes/taos-misb-build-plan.md` incl. the 2026-06-10 API-confirmation pass) and the intrinsics note (`notes/projector-intrinsics-and-misb-fov.md`). Read the **real** cloned source — the actual sample/API differs from the dist reverse-engineering: classes are `Engine`, `GameObject` (not `Node`), `Camera.createPerspective`, **`CameraController.create`** (not `FlyControls`), `deferredPreset({…projectors:true…})(engine)` (not `renderPreset`), and `Projector` is re-exported from `src/engine/index.ts`. Confirmed `Projector.verticalFov()`: `focalLength` (mm) overrides, else `(fovY*π/180)` — so `fovY` is a **degrees** field. Confirmed `GeoFrame.atLonLat(lonDeg,latDeg,h)`, `worldFromEcefPoint`, and that `geodeticToEcef` takes **radians** (`lonLatToEcef` takes degrees). 2. **Picked the clip + sample.** From `incidents.json`: 2025 Palisades · Air Intel · **N57B 22:30:16Z** (`N57B_223016`). Fetched its telemetry from `guerin.acequia.io` (CORS `*`). ST 0601 splits fields across packets sharing one `precision_time_stamp`, so I field-merged the first ~12 packets (`ts=1736288946876095`) into one complete sample: sensor 34.0558,−118.5041,4737 m MSL; frame-center 34.0764,−118.5434,0.03 m; HFOV 41.913°, VFOV 23.577°, slant 6392.48 m. 3. **Sanity-checked the geometry** before coding: `|frameCenterECEF − sensorECEF| = 6392.1 m` vs `slant_range 6392.5 m` (0.3 m), footprint `2·slant·tan(HFOV/2) = 4897 m` = telemetry `target_width`, aspect 1.835 ≈ 16:9. Self-consistent → mapping is sound. 4. **Wrote the sample.** `samples/misb_step0.ts` + `.html`. Anchored the GeoScene at the sensor lon/lat with origin at the frame-center ground elevation; token-free `addRasterDemTerrain(AWS_TERRARIUM,{imagery:ESRI_WORLD_IMAGERY})`. One `Projector` at sensor ECEF, `rotation = lookRotation(frameCenterEcef − sensorEcef)` (ported the 6-line helper + `textureFromImage` from `geo_osm_projection.ts`), `shape:'perspective'`, `focalLength=null`, `fovY=VFOV`, `aspect=tan(HFOV/2)/tan(VFOV/2)`, near=slant·0.25, far=slant·1.75, `blend:'alpha'`. Source = a generated labelled test-grid with the source aspect (live video is step-1). Camera seeded behind+above the sensor aimed at the frame-center. Console-logs the full pose/intrinsics/footprint. 5. **Ran + verified.** `npm install` clean (498 pkgs, sharp ok, no native failures). `npx tsc --noEmit` over the whole repo (incl. the new sample) → **0 errors**. `npm run dev` (Vite v8, port 5173) in background; `misb_step0.html` → 200; the sample module + every imported engine/geo module transform 200 with no Vite error overlay. Headless Chrome (puppeteer) has **no WebGPU adapter** → engine throws `No WebGPU adapter found` from `RenderContext.create`; the stack trace shows the page reaches `main()` → `Engine.create()` cleanly (no TS/module/reference error in our code). Per the brief I did **not** burn the run forcing WebGPU. Instead verified pose/intrinsics numerically with the verbatim `GeoFrame.atLonLat` ENU math: sensor world-Y = 4737 m (above terrain), frame-center world-Y = −1.4 m (on ground), look-dist vs slant 0.34 m, footprint 4897×2668 m (= target_width), frame-center 4290 m NW of sensor — all PASS. Stopped the Vite process. 6. **Recorded.** Copied `misb_step0.ts`/`.html` + the headless capture (renamed honestly: `…_headless-no-webgpu-adapter.png` + console txt) into `2026-06-09/artifacts/step-0/`, with a `README.md` (what it does, exact clip+values, how to run, verified-vs-not, step-1). Wrote this chat. Added an about.md Session Log line.

## Blockers / next - No WebGPU in headless here → could not capture a true render. Open `http://localhost:5173/samples/misb_step0.html` in desktop WebGPU Chrome to see it. - **step-1:** swap the test-grid for live video — `projector.source = {kind:'video', video, backend:'copy'}` (engine re-uploads per frame). Same pose/intrinsics; only the source changes.