Skip to content

fix(camera): frame the city on initial load to match R (#62)#68

Merged
thalida merged 3 commits into
mainfrom
worktree-fix+issue-62-initial-frame
Jun 16, 2026
Merged

fix(camera): frame the city on initial load to match R (#62)#68
thalida merged 3 commits into
mainfrom
worktree-fix+issue-62-initial-frame

Conversation

@thalida

@thalida thalida commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Closes #62.

Problem

The first auto-frame on world load was off (city not centered/fit); pressing r re-framed it correctly. Root cause: the initial frame was captured from a transient early state, while r re-captures later once everything has settled.

Three framing inputs differed between the auto-frame and r (each confirmed via instrumentation — rootW 32→96, tallestH null→1024, labelHalfW 128→512). The first two are the empty boot / placeholder-height skeleton vs the settled real city; the third is the web font loading.

Fix

Every camera-framing input now comes from layout data / settings / pure math, and the camera re-snaps through the load's applies:

  1. Tallest building heightbuildings.getTallest() looped the building cell meshes, which build asynchronously, so it was null at frame time and only valid by the time r ran. Replaced with a cityState.tallestBuilding computed derived from layout.buildings (already includes the worker's media-silhouette sizing). It tracks layout (reassigned every apply), so the height is fresh on the skeleton→final reuse apply where placeholder heights become real without bumping structureRevision. The rig reads it from cityState; the now-dead buildings.getTallest is removed.

  2. The final manifest applies as a REUSE (same tree_signaturestructureRevision and bbox frozen), so the old bbox-tracking reframe never re-fired and the camera stayed on the early framing (empty boot, then skeleton placeholder heights). The composer now follows the framing on every apply (cityRevision) until the user first takes control (OrbitControls start), so it tracks the world settling but never yanks a user-set view. reset() also recomputes from current state instead of a cached pose, removing an effect-ordering hazard.

  3. Repo-label width — the height-fit sampled the label's left/right corners, so the label's world width (from the text-texture aspect, which only settles once the Orbitron web font loads) pulled the camera back and shifted the frame after load. The label is centered on the gem and already sits inside the width-fit frame, so the fit now uses only the label's top-edge height, not its width — removing the last rendered/font dependency from framing.

Tests

  • cityState.tallestBuilding unit tests, incl. the reuse-apply-without-structureRevision case (the crux).
  • initialFraming integration tests: re-snaps on a post-load layout rebuild before the user interacts; stops following once the user takes control.
  • cameraRig test locking in that the repo-label width does not affect start framing (only its top-edge height).
  • Full suite green (2304), typecheck / eslint / prettier clean.

🤖 Generated with Claude Code

@thalida thalida linked an issue Jun 16, 2026 that may be closed by this pull request
Comment thread app/src/city/state/index.ts Outdated
Comment thread app/src/city/index.ts Outdated
Comment thread app/src/city/index.ts Outdated
@thalida thalida force-pushed the worktree-fix+issue-62-initial-frame branch 2 times, most recently from f6abc07 to 655e76f Compare June 16, 2026 02:16
The first auto-frame on load differed from pressing R because it was captured
from a transient early state, while R re-captures after everything settles.
All framing inputs now come from layout data, and the camera re-snaps through the
load's applies:

1. Tallest building height — getTallest() looped the building cell MESHES, which
   build asynchronously, so it was null when the camera framed and only valid by
   the time R ran. Replaced with a `cityState.tallestBuilding` computed derived
   from `layout.buildings` (which already includes the worker's media-silhouette
   sizing). It tracks `layout` (reassigned every apply), so the height is fresh
   on the skeleton→final reuse apply, where placeholder heights become real
   without bumping structureRevision. The rig reads it from cityState like its
   other framing inputs; the now-dead buildings.getTallest is removed.

2. The final manifest applies as a REUSE (same tree_signature → structureRevision
   and bbox frozen), so the old bbox-tracking reframe never re-fired and the
   camera stayed on the early framing (empty boot, then skeleton placeholder
   heights). The composer now follows the framing on every apply (cityRevision)
   until the user first takes control of the camera (OrbitControls 'start'), so
   it tracks the world settling but never yanks a view the user has set. reset()
   also recomputes the framing from current state instead of a cached pose,
   removing an effect-ordering hazard.

3. Repo-label width — the height-fit sampled the label's left/right corners, so
   the label's world width (from the text-texture aspect, which only settles once
   the Orbitron web font loads) pulled the camera back and shifted the frame
   after load. The label is centered on the gem and already sits inside the
   width-fit frame, so the fit now uses only the label's top-edge HEIGHT, not its
   width — removing the last rendered/font dependency from framing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thalida thalida force-pushed the worktree-fix+issue-62-initial-frame branch from 655e76f to 0e65025 Compare June 16, 2026 02:19
thalida and others added 2 commits June 15, 2026 22:29
…action

The previous commit reframed on every apply until the user's first interaction
("follow"). That silently reframed the camera on config-saves and live-updates
when the user hadn't yet touched it — an unintended change to interaction
behavior — and it's unnecessary: the empty boot has no source key (already
skipped), and the final manifest is the apply where the source key first commits,
so a plain snap-on-source-key-change lands on the settled city. Same-source
re-applies (live-updates, config saves) no longer reframe, matching the prior
semantics. reset() still recomputes (the final reuse apply leaves bbox frozen, so
a cached pose would be stale).

Tests updated: initial load frames the city (not the empty boot); a same-source
re-apply does not reframe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI's Trivy scan fails on starlette 1.2.1 for CVE-2026-54283 (HIGH, fixed in
1.3.1): request.form() size limits silently ignored for
application/x-www-form-urlencoded, enabling DoS. Transitive via fastapi /
sse-starlette; bumped via `uv lock --upgrade-package starlette` (lock-only, no
pyproject change needed). Backend tests pass (242).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thalida thalida merged commit ca4afb3 into main Jun 16, 2026
1 check passed
@thalida thalida deleted the worktree-fix+issue-62-initial-frame branch June 16, 2026 02:39
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.

Initial world load doesn't frame correctly

1 participant