Note 06 — through-the-lens test harness + local solver (2026-06-17) (Image Pose Uxui)

**Note** from Bead: Image Pose Uxui · [canonical source](https://redfish.acequia.io/guerin/.agents/74c30681-2b0c-48b2-b2a8-9e21190a4926/2026-06-16/notes/06-ttl-test-harness-and-solver.md) · session 2026-06-16 · discussion: Talk: Image Pose Uxui

Controlled rig to develop the through-the-lens **algorithm + UX** without Google tiles, so we have a KNOWN ground-truth pose to check against. File: `repo/through-the-lens-test.html` (deployed under `repo/`).

## The corrected interaction (Stephen, this session) Earlier I anchored the snapshot **in the world** where it was shot (a textured quad at the snap frustum). Wrong. Stephen: *"the image should fly with me. once i like the displacement — i lock the new position with misaligned image in front of me. and then clicking-dragging puts constraints and moves the model (which is really moving the camera pose)."* So the image is a **screen-pinned overlay** (a HUD `<img>`, `object-fit:fill`, driven by `canvas.toDataURL`) that **flies with the camera**. Flow: 1. **📸 snapshot** — pin the current render to the screen. 2. **fly** (orbit/pan/dolly/WASD) until the live model sits behind the image at a displacement you like. 3. **L lock** — freeze navigation (green ring; left-drag is now reserved for picking). 4. **drag** a model point onto its place in the image → camera re-solves so the model registers. This is the Naimark/Debevec campanile composite (note 05), now with the image flying with the viewer + an explicit lock step, instead of a world-anchored cap.

## Picking — we own the geometry, so no engine `pickFeature` The scene is 6 axis-aligned boxes + 3 vertical cylinders + ground, so picking is analytic: - ray from cursor: `dir = normalize(F + R·ndcx·tanX + U·ndcy·tanY)`, basis `F=camForward`, `R=norm(F×up)`, `U=R×F` (the SAME basis the pan control uses → screen math matches the engine). - box = ray-AABB slab; cylinder = ray vs infinite vertical cylinder + cap planes; ground = `y=0` plane. - nearest positive hit → world point `X`. (For the real `calibrate-3d` app over Google tiles, this `X` instead comes from `pickFeature` on the tile DSM — same correspondence, different surface. The test just removes that dependency.)

## Solver — `solveLocal(cons, p0)` (minimal-change / Gleicher–Witkin) Params `[x,y,z,yaw,pitch]` (fov fixed here). Residual = reprojection of each `(X→uv)` in NDC **plus a small prior** `PR·(p−p0)` anchored to the *current* pose. Numeric-Jacobian Gauss-Newton, ~10 iters, re-anchored each tick. The prior regularizes the under-determined DOF when <4 pins, so a single dragged point nudges the camera smoothly instead of blowing up = the differential through-the-lens feel. ≥4 well-spread pins fully determine the pose; the prior vanishes at the true solution (reprojection→0). Every constraint soft (feedback_every-constraint-is-soft).

## Verification (CDP on the real GPU, :9333) - Overlay flies with the camera: full-screen through a camera orbit; lock blocks orbit (Δyaw 0.00°). ✓ - **Solver:** from a camera displaced 6 m / 20° off, 15 ground-truth correspondences recovered snapPose **exactly** — Δpos 0.000 m, Δyaw 0.00°, Δpitch 0.00°. ✓ - **Projection matches the engine:** screenshot after the solve shows the live model **coinciding** with the real overlay render (no double-image), pins glued to model corners. ✓ - WebGPU capture: `canvas.toDataURL('image/png')` works where `drawImage`/`createImageBitmap`/GPU-copy of the swapchain return blank — reused as the overlay `src`.

## Next - Replace the scripted ground-truth pins with a real human drag pass (the handlers are wired; the automated test injected pins to isolate the solver). - Port the analytic picker → `pickFeature` over Google tiles in `calibrate-3d.html` (Stage 2). - Add focal length (fovy) as a 6th solved DOF once ≥5 pins (campanile: spread lights up intrinsics last).