fix(camera): frame the city on initial load to match R (#62)#68
Merged
Conversation
thalida
commented
Jun 16, 2026
f6abc07 to
655e76f
Compare
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>
655e76f to
0e65025
Compare
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #62.
Problem
The first auto-frame on world load was off (city not centered/fit); pressing
rre-framed it correctly. Root cause: the initial frame was captured from a transient early state, whilerre-captures later once everything has settled.Three framing inputs differed between the auto-frame and
r(each confirmed via instrumentation —rootW32→96,tallestHnull→1024,labelHalfW128→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:
Tallest building height —
buildings.getTallest()looped the building cell meshes, which build asynchronously, so it wasnullat frame time and only valid by the timerran. Replaced with acityState.tallestBuildingcomputed derived fromlayout.buildings(already includes the worker's media-silhouette sizing). It trackslayout(reassigned every apply), so the height is fresh on the skeleton→final reuse apply where placeholder heights become real without bumpingstructureRevision. The rig reads it fromcityState; the now-deadbuildings.getTallestis removed.The final manifest applies as a REUSE (same
tree_signature→structureRevisionandbboxfrozen), so the oldbbox-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 (OrbitControlsstart), 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.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.tallestBuildingunit tests, incl. the reuse-apply-without-structureRevisioncase (the crux).initialFramingintegration tests: re-snaps on a post-load layout rebuild before the user interacts; stops following once the user takes control.cameraRigtest locking in that the repo-label width does not affect start framing (only its top-edge height).🤖 Generated with Claude Code