Skip to content

spec 2: camera pivot — body-fixed navigation behind the pose seam - #647

Merged
rulkens merged 227 commits into
mainfrom
camera-pivot
Sep 11, 2026
Merged

rulkens merged 227 commits into
mainfrom
camera-pivot

Conversation

@rulkens

@rulkens rulkens commented Sep 1, 2026 •

Copy link
Copy Markdown
Owner

Spec 2: camera pivot — body-fixed navigation behind the pose seam

Near a body the authoritative camera state stops being the heliocentric Mpc orbit camera and becomes a body-fixed, anchor-relative pose in SI metres. Google-Earth navigation follows from that storage instead of from corrections layered on a world camera: the ground under the cursor stays under the cursor, the horizon stays level at every latitude, the sky is reachable, and a fast sim clock cannot slide the ground. Outside the engage band the incumbent Mpc orbit camera is unchanged; the crossing is one lossless conversion at one site.

Spec 1 (#634) built the BodyPoseProvider seam and moved every body's rendering into its own metre frame. This PR swaps provider B in behind that seam and does not change the renderer. Prep (#648: roll plumbing, orbitControls recognizer split, drainInput) shipped separately.

  • Spec: docs/superpowers/specs/completed/2026-09-01-camera-pivot.md (ruling record: docs/grill-sessions/globe-camera-pivot-2026-08-24.md)
  • Plan: docs/superpowers/plans/completed/2026-09-01-camera-pivot.md (22 tasks, all ticked; ledger beside it)
  • Single-writer prep plan, shipped on this branch: docs/superpowers/plans/completed/2026-09-09-camera-runtime-single-writer.md

What landed

  • Types and conversions. FramedCameraPose = { frame: 'absolute' | { body }, pose }; one arm authoritative, fold-last conversion at the regime edge.
  • Pure primitives. Cursor ray in body metres, surface readout (standpoint / heading / tilt / range with KML LookAt semantics), descent floor in metres, anchored drag rotation, anchored zoom step, regime predicate with arm-dependent hysteresis.
  • Integration. camera.base migration across every reader, provider B behind the pose seam, the fold in runFrame, the gesture latch and remembered tilt.
  • Camera-runtime single writer. cameraRuntime is written by runFrame and wireInput only, pinned by an allow-list test.
  • Feel. Cursor-anchored zoom both directions, north-up convergence priced on the zoom actually spent (a trackpad twitch no longer resets heading), altitude-keyed orientation authority shared by both arms, Cesium-style remembered tilt (display = remembered × bodyUpWeight(h/R)), tilt floor at nadir with a dead stop, Google-Maps drag directions, authored vs displayed pose two-box, body-switch pose reset, eye-preserving followBody capture.
  • Orientation un-braid. The drag-wall tilt ceiling (a third altitude ramp over tilt) is gone; tilt has one cap. The pointer-down/gesture pair is one three-state field. Drag settles have their own cap constant.
  • Camera tuning in state. The four band edges (engage 0.45 / disengage 0.90, tilt blend 0.06 → 0.60), the blend space and north-up are one CameraTuning record on the camera slice, threaded into the math; one clamp, no mutable module records, no import cycle.
  • Frames for keyframes. Frame-tagged clip keyframes and lonLatFocusPose as a body-arm constructor.
  • Debug panel. Camera section: arm / driver / gesture header, heading / tilt / roll rows with current, target, residual, per-frame delta and peak (recorded only while the section is mounted), a log-scale band bar with the four sliders under it, collapsed raw block, copy-all in radians.
  • Golden traces. settleGoldenTrace and driverGoldenTrace pin the settle mechanisms and the driver stack through the frame loop; re-records carry a parse-compared diff in the commit body.

Deferred (backlog, docs/backlog/2026-09-11-*)

Arm entry adopting the arriving tilt; world-arm wheel burst unclamped; deriveBodyStates scene-body-id type; clip per-endpoint authoring validation; f32 half-ulp sightline nudge at the arm flip; bodyLikeFraming ⇄ focusFraming import cycle; three radar residuals. Also docs/backlog/2026-09-10-wheel-notch-route-by-last-winner.md and the smooth-zoom / flick-coast item.

Landmines for reviewers

  • camera.base is a union. Readers narrow on frame; the world-Mpc view is derived (liveWorldPose / authoredWorldPose), never stored twice.
  • Two pose boxes: the authored register versus cameraRuntime displayed pose. Pick, demand, debug and clip/tween seams read displayed; the fold reads authored.
  • Heading and tilt are derived from the basis, never stored; only rememberedTiltRad is state.
  • clampCameraTuning is the one write path for the band edges (hysteresis window ×1.1, tiltZeroHR ≤ disengageHR); the debug sliders dispatch through it.

Verification

  • npm run typecheck clean; npm test 1277 files / 8846 tests green on the merged HEAD.
  • Perf (T21): neutral, 216.4 vs 215.8 ms summed medians, all deltas inside spread; landed on the user's ruling.
  • Feel gate (T22): seven items attested in-app on 2026-09-11, including the debug-panel and slider pass.
  • Whole-branch deletion audit applied (−103); the file-inventory sections in plan and spec are left as written.

rulkens and others added 2 commits September 1, 2026 10:16
…parison

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pose seam

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
skymap 4ce5f95 Commit Preview URL

Branch Preview URL
Sep 11 2026, 02:13 AM

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rulkens and others added 25 commits September 1, 2026 12:24
…he frame loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n, returns-pose deferred

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 data delta from the camera-pivot spec (§3): PoseFrame,
BodyFixedPose, FramedCameraPose types plus the SURFACE_REGIME
hysteresis/tilt constants. No consumers yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 3 of the camera-pivot plan: `toBodyArm` / `toWorldArm`, provider B's
storage seam. Entering re-expresses the orbit pose in the body's fixed axes in
SI metres and captures nothing time-dependent — no epoch, no orientation
snapshot — so an accelerated clock cannot move the engaged pose (spec §5.1).
It derives the world eye and camera basis exactly as `frameContext` does
(`updatePosition`'s decode through `poseBasis`, then `imagePlaneBasis` over
`frameUp(upBasis)`) and hands them to `bodyRelativePose`, the existing Mpc→m
seam, rather than repeating the subtract-then-scale ordering the precision
depends on.

Leaving bakes the rotation back out and re-derives the orbit parameterization:
the target is the nearest sphere hit ahead of the eye (a miss, or a body
behind, falls back to the body centre), the range is measured in body-fixed
metres, and `roll` carries the screen-up residual the eye position alone
cannot express (spec §12-R1).

Round-trips eye, forward and screen-up to ~1 nm — three orders under provider
A's ~14 µm floor — over Earth, over a tilted-pole body with a non-identity
orientation, through a rolled pose, and at a moon's radius.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
poseFrameConversion.ts (task 3) is a second legitimate SCALE_UNITS seam
alongside bodyRelativePose.ts. Extend oneMpcSeam's sweep to
services/engine/camera, services/camera, and utils/camera, exclude both
seams explicitly, and allow-list the three verified radius->Mpc
framing-bridge uses (bodyLikeFraming.ts, cameraDrivers.ts:332,
pivotRadiusMpc.ts) so the gate covers the camera path the same way it
already covers the body-slab render path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Descending smoothstep ramp (SURFACE_REGIME.disengageHR -> tiltFullHR)
so the look-mode tilt ceiling opens to pi near the ground and closes
to 0 at the surface-arm disengage altitude (spec §6, task 5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fixes for the world ↔ body conversions.

`DIR_FLOOR` was 20× looser than its own derivation claimed: tightened to 1e-10
(eye bound over the shortest fixture range, doubled), still 130× above the
measured 7.4e-13.

`toBodyArm` re-composes `updatePosition`'s two steps out of the same sub-utils
rather than calling it, and no round-trip can catch drift there — both halves
would share it. One new test runs `updatePosition` on an equivalent
`OrbitCamera` and asserts the module's world eye equals its output exactly.
The fixture's target sits near the origin deliberately: at heliocentric
magnitude the f64 grid (~25 µm) swallows any composition difference below a
grid step and the equality goes blind — a 1e-15 relative perturbation passed
until the fixture moved.

Spec: the "~14 µm" round-trip bar was really the 2-ulp-at-1-AU figure, ≈50 µm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves a BodyFixedPose's anchor toward the eye, quantized to the ulp grid
of the anchor's own magnitude, so anchor + eyeRel stays bit-identical
while both stored magnitudes shrink (spec §5.3). Not exercised by the
shipped centre anchor yet — built now because deep-zoom anchors want it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…basis

Builds the CSS-pixel pick ray directly from basisLocal's columns and FOV
(spec §6, task 7) rather than inverting the slab's view-projection matrix,
so it cannot drift from what the renderer actually draws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iew round 1)

C1: add two exact-value fixtures to reanchoredPose.test.ts that pin
quantization to ulp(|anchorLocalM|) specifically — verified by substitution
to fail when the grid instead comes from the range or a fixed constant,
closing the gap the prior bit-identity tests left blind to.

I1: tighten reanchoredPose.ts's three comment blocks (module header,
TRIGGER_FRACTION, ulpAt doc) to 14 comment lines / 30 code lines, back
inside the ≤half budget, without losing the ulp-floor or log2-landmine
rationale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 9 of the camera-pivot SDD plan: the metre-space descent floor
reads SURFACE_STANDOFF_RADII from its single existing declaration
(clampDistance.ts) so the Mpc-space distance clamp and the metre-space
surface descent gesture can never disagree about where the ground is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pure geometric h/R read over the SCENE_BODIES ∩ bodyStates roster,
body-blind (Sun and Sgr A* included) and hysteretic purely through the
PoseFrame it is handed back — no stored regime boolean anywhere, which
noStoredRegimeFlag.test.ts gates as an import-graph scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-centre ENU

Derives standpoint/heading/tilt/range/altitude from a BodyFixedPose (spec
§3, §14): altitudeM stays eye-based (FW-A) and never reads the sightline;
tiltRad is measured from local nadir per the type's datum; heading falls
back to the basis's up column within ~0.08° of vertical, and the East/North
construction stays finite through the pole singularity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding: the fallback (eye's nadir footprint) does NOT reduce
continuously to the exact-hit case as tilt crosses the tangency angle
(arccos(R/(R+h)) from nadir) — standpoint snaps discontinuously there.
The fallback choice stands (this readout is display currency; engaged-path
gesture/enforcement code derives its own eye-anchored ENU) but the comment
must not claim continuity it doesn't have. No behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 1:1 drag of spec §6a (FW-I): intersect the previous and current cursor
rays with the frozen pick sphere and rotate the pose — position and basis
together — by the quaternion carrying the current pick back onto the grabbed
point, so that point re-projects onto the current pixel exactly. Null on a
miss, on a hit behind the eye, or at grazing incidence, where a rotation
would be a teleport; the caller degrades the gesture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stateless per-tick zoom toward an anchor (spec §6b): approaching with a
cursor hit anchors on the cursor; zoom-out and any cursor miss always fall
back to the body centre (FW-H), so the cursor can never become an
accumulating pivot. Per-notch factor is clamped on both signs and the
result is floored via surfaceFloorM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…honormalise handedness trap

The pose rotates with its rays, so it turns by the INVERSE of the quaternion
carrying the grabbed pick to the current one; the spec sentence read the other
way and would have been "fixed" back. `reorthonormalise` rebuilds column 2 as
c0 × c1, so an image-plane (right, up, forward) triple comes back mirrored —
that now lives in its header rather than only at the one call site.

Also exports MIN_INCIDENCE_COS as the controller's single home for the grazing
threshold, drops an authority-order claim that does not hold, and documents the
unit-`dir` precondition. No behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-level const

Two tests asserted call-vs-call equality only, which a latched-anchor or
cursor-ignoring implementation would also satisfy; pin them to hand-derived
closed-form eye positions instead. Delete the module-level CENTRE const
(dead weight in the one module whose contract is statelessness) and inline
the literal at its single use site. Trim the header comment to the algebra
one-liner for the tangent-plane invariant and note the caller's contract
(factor derives from the centre-measured range, never |eye - anchor|).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…task 13)

Behaviour-identical: no body arm is constructible until the fold lands, so
every gate added here reads `frame === 'absolute'` and is trivially true.

- `camera.base`, `cameraRuntime.lastPose` and `CameraDriver.pose` carry the
  arm; `commitCameraPose` takes a `FramedCameraPose` and world-arm producers
  wrap with the one `absoluteArm` constructor.
- `resting` stays arm-agnostic; `orbitDrag` / `autoRotate` / `followBody` gate
  `isActive` on the absolute arm (spec §7). `applyWheelZoom` and
  `applyFocusedBodyPivot` return early on a body arm rather than leaving the
  rule to a caller.
- `runFrame` resolves the world arm exactly once, below every pose writer and
  above step 4 — the slot the regime fold takes next. `liveWorldPose` is the
  single off-frame resolution site (pick, demand, gesture seed, getLivePose,
  the follow driver's `from` capture).
- `eyeMpcOf` extracts `updatePosition`'s eye derivation, allocation-free, so
  the regime predicate and the camera read one eye.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The header claimed the helper serves callers OUTSIDE the frame loop and always
reflects the last RENDERED epoch. `followBody.pose` calls it from inside the
produce step, after `runFrame` has advanced `lastRenderedSimDays`, so at that
site the field is THIS frame's epoch — which is what the approach ease wants.
Name both call-site classes and the epoch each actually gets.

Comment-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds poseFromBodyArm (the anchor fold: eyeRelBodyM = anchorLocalM +
eyeRelAnchorM, basisM = basisLocal) and wires it into
deriveFrameContext's bodyPose closure: the engaged body's pose comes
from provider B, every other body keeps provider A (spec §5.2, ruled
S1). deriveFrameContext grows an `arm: FramedCameraPose` parameter
threaded in beside the already-resolved world pose by both callers
(runFrame, pickFrameContext) rather than re-resolving it.

Tested and inert: nothing constructs a body arm until Task 15 flips
the fold, so no rendered pixel moves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rulkens and others added 25 commits September 10, 2026 14:43
…log-zoom

The settle was `clamp(0.25·residual, ±0.1)` spent once per input STEP, and
`inputAggregator` folds a frame's wheel events into one step — so a deltaY-1
trackpad event (factor ≈ 1.001) cost the same 25 % of the heading as a
deltaY-100 mouse notch (factor ≈ 1.105). Continuous trackpad scrolling norths
the view in ~0.2 s with essentially no altitude change (diagnosis §F1).

Every zoom-driven settle amount is now a function of the notch's log-zoom
magnitude `u = |ln factor|`: the residual multiplier is `e^(−k·u)` (so N
notches decay exactly as much as the one notch whose factor they multiply to)
and the cap is `capRadPerLogZoom·u` (so a burst of small events cannot
re-flatten the rate). Calibrated on the deltaY-100 notch — `u = 100 ·
WHEEL_ZOOM_K = 0.1` exactly — to the legacy 25 % / 0.1 rad, giving
`perLogZoom = −ln(0.75)/0.1 = 2.8768…` and `capRadPerLogZoom = 1`.

`u` threads from `surfaceZoomStep`'s factor through `settledZoomPose` into
`orientStepRad` (dive heading), `riddenOrientStepRad` (recession heading and
the tilt-deviation decay) and `levelledPose`'s cap, and from `replayInput`'s
zoom step into `frameAlignedRoll`. Drag-driven settles are out of scope and
spend `ORIENT_DECAY.notchLogZoom` — the reference notch, i.e. what they always
spent. `rideBoundRad` is unchanged: the reference's own move already scales
with the notch.

Two consequences worth naming. `u = 0` is inert, so a factor-1 notch settles
nothing and a park no longer converges — several tests that used factor-1 (or
deltaY-0.0001) notches to "converge in place" now dither in and out at one
altitude instead. And a large folded notch buys a large settle: a factor-0.5
tick spends ~0.69 rad, which is what seven mouse notches spend.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ruled behaviour change (see the previous commit): re-recorded with
`SETTLE_GOLDEN_RECORD=1`. `driverGoldenTrace` did NOT move — it passes
unmodified, so `DRIVER_GOLDEN_RECORD` was not run.

Parse-compare, old vs new, both cases (`northUp`, `northUpOff`):

- keys identical (`northUp`, `northUpOff`); step count 26 → 26 in both;
  every `label` identical in order; per-step field keys unchanged
  (`label`, `arm`, `hr`, `displayed`, `register`, `memory`); no `arm` changed.
- first differing step: 3 (`dive 39`), in both cases.
- 23 of 26 steps carry some changed value, in both cases.

The script's deltaY-100 legs move only in the last digits — that is the
calibration point, and it holds. The movement is the deltaY-300 legs, where a
notch is now 3 e-folds' worth: `dive brisk 0` moves `register[5]` by 446 m out
of 254 km (1.8e-3 relative, the largest single-value change), `hr` by 2.3e-5
relative, and the displayed roll by at most 4.4e-2 rad (`recede brisk 0`,
−2.879 → −2.836). `northUpOff` moves too — the tilt-deviation decay is not
behind that gate — but only by 5e-3 relative at most. `memory` is untouched
beyond 4e-7 relative.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The body arm read `u` off the folded notch, not the one `anchoredZoomStep` is
allowed to spend: a 20-event trackpad flick folds to factor e², moves the eye
by the clamped x2, and spent u = 2.0 — up to 2.0 rad of heading in one frame,
worse than the per-step defect the previous commit removed. The [0.5, 2] clamp
moves into `spentZoomFactor` so both users read one home, and the settle prices
off it: one frame now spends at most u = ln 2.

The world arm keeps its threaded `logZoom`. Deriving it from the pre/post
distances is not possible there: `zoomedDistance` scales the ALTITUDE above the
pivot, so a distance ratio under-prices a focused notch ~20x, while the
altitude ratio over-prices when the pivot has no radius — which of the two
holds is `pivot.radiusMpc`, and only the caller has it. Its header now says so,
and states that nothing above `rideBoundRad + capRad·u` bounds one notch there.

`ORIENT_DECAY` derives from `WHEEL_ZOOM_K` instead of restating it as a
17-digit literal (bit-identical: -ln(0.75)/0.1 === 2.8768207245178088), so the
calibration cannot silently decouple from the aggregator. Golden traces do not
move — the settle trace's notches are u <= 0.3, inside the clamp.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A folded burst is bounded by `spentZoomFactor`, so one frame spends at most
u = ln 2 whatever arrives in it. Asserted through the real driver on the
diagnosis's 20-event flick: the turn is at most capRadPerLogZoom*ln 2 and the
heading retained at least exp(-perLogZoom*ln 2) of what it started with.
Mutation-checked — pricing off the raw factor fails it at 2.0 rad vs 0.693.

`rememberedTilt`'s "zoom must not move a just-set tilt" device was a factor-1
notch, which the per-log-zoom settle makes inert: it could no longer fail on
the un-map bug it names. Now a +-notch dither, tolerance set above the anchored
pair's own walk (1.0e-3) and below the erosion `remembered = display` spends
(1.25e-2), both measured.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e comments

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wall was a third altitude ramp over the same scalar the tilt blend
band already owns (F1 of the orientation-unbraid design), reconciled
against it by a `Math.max` whose branch is unreachable at the shipped
band. Ruling 12 made display tilt a pure function of the memory, so the
memory needs one constant cap, not a returning curve: `TILT_BAND.maxRad
= Math.PI` at the one `surfaceStep` memory clamp. `tilt = 0` at the arm
flip is now carried by `TILT_BAND.zeroHR <= SURFACE_REGIME.disengageHR`
alone.

Deletes `walledTiltPose.ts`, `maxTiltRad.ts` and their tests, drops
`SURFACE_REGIME.tiltMaxRad` / `tiltFullHR` and the `ceilingRad` debug
row, and amends spec §6's ceiling paragraph and the Q4 identity in the
DoD to the ruled shape.

User rulings B1/B2/B3. B4/B5/B6 (the trial knobs) are deferred.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`SurfaceMemory.{gesture, pointerDown}` stored one three-state lifecycle
(idle → pressed-unlatched → latched) as two independent fields, so it
could spell "latched with the pointer up" and three readers each had to
re-establish the invariant against it (F4 of the orientation-unbraid
design). One field, `SurfaceGesture | 'down' | null`, makes the
impossible state unrepresentable and collapses both guards to a
narrowing. `surfaceGestureEdge` stays the only writer of the boundary.

Also drops `noteBody`'s redundant conjunct (T-D/F7): the early return
already establishes `bodyId !== prev.memoryBodyId`.

No behaviour change — both golden traces byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`levelledPose` derived nothing from `logZoom` but the cap, so a drag —
which carries no zoom — had to claim it spent a mouse notch, and
`capRadPerLogZoom` was a false single home for two independent knobs
(F5 of the orientation-unbraid design). The function now takes `capRad`;
zoom callers price it per log-zoom spent, the drag passes its own
`ORIENT_DECAY.dragLevelCapRad`. `notchLogZoom` keeps only its
calibration role.

Same numbers on both paths — both golden traces byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tale wall prose

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…bug snapshot

The panel polls at 4 Hz, which averages ~15 frames into one reading — the
shape that let F1 (a north-up decay keyed per frame instead of per zoom unit)
run live without the readout showing it. `runFrame` now records each DOF's
frame-to-frame motion, wrapped to ±π, plus the largest step since the last
clear, and the snapshot carries it.

The three DOFs move to one derivation home (`cameraDofAnglesOf`) as
current/target/residual rows, so a Δ is always a Δ of the number on the row:
heading against north of the band-blended reference, tilt against ruling 12's
`remembered × w(h/R)`, roll against the band ride target. The two endpoint
roll numbers (spin-axis roll and its residual) go with it — grill ruling Q4.

Delta state is module-level, not `cameraRuntime`: that has one writer by gate,
and this is not camera mechanism.

(re-fitted onto camera-pivot after the wall deletion)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… a band bar

Five parallel text groups read as a system dump, not an answer. The section is
now organised by the questions it answers, in priority order: one header line
for authorship (arm · driver · gesture, with a badge only on an arm or epoch
mismatch), three DOF rows carrying current / target / residual / Δ / peak with
one clear button, and the regime band as a drawn log ruler with its four edge
sliders directly underneath, so dragging an edge moves its own tick.

Degrees at one decimal on screen, h/R at three; `copy all` still dumps
everything off the same model at full-precision radians. Anchor, cursor hit,
eye-to-anchor, sim days and distance-Mpc move behind a collapsed `raw`.

`OrientationTuning` grows the ruler and loses nothing; the band group is one
visual object rather than a slider block and a text block to cross-reference.

(re-fitted onto camera-pivot after the wall deletion — the tilt-ceiling
readout goes with it)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mounted

The Δ/peak derivation walked the body roster every frame (20–40 µs) for every
user, including those who never open the DebugPanel and every pre-boot frame.
The panel's mount is now the gate: `watchOrientDeltas()` from the section's poll
effect, one boolean read in `runFrame` otherwise. `prevRad` resets on subscribe,
so the first frame back does not report the whole closed period as one Δ.

Rides along: the band readouts come off the one panel model like the DOF rows
(they were formatted inline off the snapshot), and the record's header no longer
claims Δ is body-rotation-free — heading and tilt are measured body-fixed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…op the mirrors

The Q8 test asserted the heading CURRENT cell (30.0°, which its zero target makes
the residual too), so blanking both target columns left it green. It now asserts
the roll row, whose target is independent of its current.

The snapshot test's heading assertions restated the literal 0 the source passes
and re-derived the residual from that same 0; the roll residual was the same
mirror shape. None could fail on a real bug.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The four band edges and two feel toggles as ONE value on the camera slice:
`CameraTuning`, its defaults/limits in `data/camera/cameraTuning.ts`, and
`clampCameraTuning` — the single producer, folding `setSurfaceBand`'s and
`setTiltBand`'s two settles (and their mutual re-settle) into one ordered
pass over one record. Nothing reads it yet; the three module records stay
until the threading lands.

The two record tests are replaced by the clamp's cross-edge policy test,
which also asserts the fixed point `bodyUpWeight`'s load-time `throw` stood
for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ete the three mutable records

`camera.tuning` reaches the math as an argument: one read per frame in
`stepCameraRuntime` (off the pre-replay snapshot — no action the replay emits
writes it), then down the two existing bags (`replayInput` ctx,
`projectFramePose` args, `SurfaceStepCtx`) to the eleven readers. Eleven
functions that advertised purity become pure.

`surfaceRegime.ts`, `tiltBand.ts` and `orientTuning.ts` are deleted, so their
import cycle is unrepresentable rather than merely broken, and the load-time
`throw` it forced into a third file (`bodyUpWeight`) is gone — that invariant
is now a property of `clampCameraTuning`, asserted in its test.
`TILT_BAND.maxRad` becomes `MAX_REMEMBERED_TILT_RAD`; `SurfaceBandKnob` and
`TiltBandKnob` die with the setters' return channel, so the panel's hysteresis
readout keeps `AT FLOOR` without the "(x yielded)" clause.

Both golden traces are byte-identical: same defaults, same clamp order, same
values. Ten `*_AT_LOAD` save/restore rituals go with the records — the
frame-level fixtures seed via `setCameraTuning`, the direct-call ones pass a
tuning literal.

The DebugPanel is wired minimally to compile and work against the new API
(`CameraStateSection` owns the one selector read and passes the value down);
its own task takes the readout ruling and the visual pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The spec and plan still named `SURFACE_REGIME` / `TILT_BAND` / `ORIENT_TUNING`
as the live home of the band edges; they are `CameraTuning`, clamped by
`clampCameraTuning` and written by `setCameraTuning`. Two comments in code
carried the same dead names.

No test-tuning helper: the seven per-case literals are one-line spreads, so
`tuningWith(patch)` would save nothing and cost a file plus six imports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e plan's ceiling lines

Applies the tuning-review's F1-F4 fix wave: engageFlipPop drops its local
tuning literal in favour of the store it already seeds, makeSurfaceDriver's
tuning param is now required with defaults only on each suite's own wrapper
(closing the raiseTiltTo silent-'log'-default hazard), the two clamp
expectations are hand-computed rather than restating the source's own
division, and CameraState.tuning's readonly modifier now names its reason.
Also restates the plan's five stale maxTiltRad/SURFACE_REGIME mentions
against the live bodyUpWeight mechanism, present tense.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

# Conflicts:
#	src/services/engine/camera/bodyHomePose.ts
#	src/services/engine/camera/cameraFraming.ts
#	src/services/engine/engine.ts
#	src/services/engine/frame/runFrame.ts
#	src/services/engine/helpers/shouldKeepTicking.ts
#	src/services/engine/phases/wireInput.ts
#	src/state/selection/watchGoHomeSaga.ts
…g lines

All 43 checkboxes (Tasks 1-22 + the Definition-of-Done inventory and spec
§11 acceptance criteria) are backed by the SDD ledger: per-task 'complete'
lines, T21 NEUTRAL with the user's LAND ruling, T22 attested 7/7 on
2026-09-11, and the debug-panel/tuning visual pass.

Task 5's subject (maxTiltRad) was deleted by the ruled un-braid; the three
sites that still described the ceiling as live now say the ceiling is gone
and display tilt = remembered x bodyUpWeight(h/R, tuning).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DoD audit READY after the controller's three rulings: main merged, all 43
checkboxes ticked from the ledger, Task 5's retired-ceiling prose restated.

Plan + spec → completed/; the 2026-09-09 single-writer plan (shipped on this
branch, ledger already archived) rides along as the housekeeping straggler —
it shares this feature's spec, so there is no separate spec to move. SDD
ledger copied verbatim to plans/completed/2026-09-01-camera-pivot.ledger.md.

Backlog: nothing this plan shipped was left behind; the seven 2026-09-11
adjacent findings and the wheel-notch item are deferred work and stay. Their
index lines are trimmed to the convention — title, tag, one clause, details
link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rulkens
rulkens marked this pull request as ready for review September 11, 2026 02:10
@rulkens
rulkens merged commit 62e3ac1 into main Sep 11, 2026
1 of 2 checks passed
@rulkens
rulkens deleted the camera-pivot branch September 11, 2026 02:12
rulkens added a commit that referenced this pull request Sep 11, 2026
…as-mesh-bodies

engine.ts: kept the new meshBodyRenderer / meshBodies asset-slot entries alongside main's comment-audit deletions around ringRenderer and bodyTextureAtlas.
frameContext.ts: kept both import sides — the attached-mesh-bodies map helpers and #647's poseFromBodyArm, both live below.
slabs.ts: kept the attached-mesh-body near-plane widening (hostNear + attachedBodiesByHostId) on top of main's tightened near-plane comments.

Also fixed a semantic break the textual merge could not see: #682 routes orbit trails through sceneOccluderSpheres -> sceneBodyPartition, which reads this branch's state.data.bodies.meshBodies, so two of main's fixtures needed the field (and one branch test an explicit canvasSize).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant