feat(editor): push-past zoom, framing, projection toggle, cardinal views (#211) - #260
Merged
Conversation
…ews (#211) Completes the GW-1 camera model on top of the p1-s1 pivot and chords. Zooming into the pivot used to dead-stop at the minimum distance. It now pushes past: the remainder of the dolly moves the pivot along the view direction, so the eye keeps travelling at exactly the rate it was and you can fly through a scene on the wheel alone. Zooming out never relocates the pivot. Orthographic derives its half-height from the orbit distance, matching what the perspective frustum spans at the pivot depth. That makes the O/P toggle jump-free for free and leaves zoom() with no mode-specific branch — distance IS the ortho scale. Its near plane sits behind the eye so geometry between eye and pivot is not clipped. Ortho's wheel zooms toward the cursor (zoom_about); perspective already does that via the eye dolly, so it is a plain zoom there. Framing was buggy, not just coarse: it copied whole roads into a temp NetworkMesh, so a selected signal framed its entire road, a lane framed its whole road, and a junction framed nothing at all. The new viewport/framing.cpp computes per-kind bounds — road extents, a lane's own patch vertices, an instance grown by its model's dimensions, a junction's floor — and test_framing.cpp pins each of those three bugs. No core/ changes: there is no Aabb there and framing is a viewport concern. Select moves V -> Q so V can be frame-on-cursor, which moves the pivot while preserving the zoom. Cardinals snap yaw only (pitch, distance and pivot survive), and top-down is near-vertical: exactly pi/2 makes the look-at basis degenerate. Numpad digits with the top-row digits as alternates, all of it also in the View menu. Picking and the gizmo screen math were already projection-agnostic, as discovery found — so ortho gets regression tests rather than changes: parallel rays, the same pixel hitting the same lane, and no foreshortening. Adds a --camera ortho screenshot preset so the projection is visually verifiable, including in CI's artifact job. Refs #211
16 tasks
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.
Second sprint of Pillar P1 — Interaction & Navigation (epic #250), on top of the merged p1-s1. Completes the GW-1 camera model.
Closes #211
Push-past-pivot zoom
Zooming in used to dead-stop when
distancehit its 2 m minimum. Now the remainder of the dolly moves the pivot along the view direction instead, so the eye keeps travelling at exactly the rate it was — you can fly through a scene on the wheel alone. Zooming out never relocates the pivot.The invariant worth reviewing is continuity: at the boundary, the eye's step must equal what an unclamped dolly would have taken.
EyeTravelIsContinuousAcrossThePushPastBoundarypins that;PushPastMovesTheTargetAlongTheViewAxisOnlypins that zooming in can't slew the view off its subject.Orthographic
ortho_half_height = distance · tan(kFovY/2)— what the perspective frustum spans at the pivot depth. Three things fall out of that one choice:zoom()needs no mode branch — distance is the ortho scale;Ortho's wheel zooms toward the cursor (
zoom_about). Perspective already does that via the eye dolly, so there it's a plain zoom — asserted, not assumed.Framing was buggy, not just coarse
Discovery found
frame_selectioncopied whole roads/props into a tempNetworkMeshand framed that. Consequences, all real:New
viewport/framing.cppcomputes per-kind bounds — road extents, a lane's own patch vertices (it indexes into the road's shared vertex array), an instance grown by its model's radius/height, a junction's floor.test_framing.cpphas a named regression test for each of the three bugs. Nocore/changes: there's noAabbthere, and framing is a viewport concern (approved decision).Bindings
Select
V→Q(approved), freeingVfor frame-on-cursor — which moves the pivot and preserves the zoom distance, so it re-aims rather than dollying. Cardinals snap yaw only; top-down is near-vertical (π/2 − 0.01) because exactly π/2 makes the look-at basis degenerate. Numpad digits primary, top-row digits alternate, and everything is in the View menu — the keys are accelerators, not the only route.Ortho didn't need code — it needed proof
Discovery said picking and gizmo screen math were already projection-agnostic. This PR confirms rather than changes them: ortho rays are parallel instead of fanning from an eye, the same pixel hits the same lane in either projection, and ortho doesn't foreshorten (with the perspective case asserted alongside, so the test can't pass vacuously).
Visual evidence
Added a
--camera orthoscreenshot preset so the projection is verifiable at all — including in CI'seditor visual artifactsjob. Renderinggs1_urban_intersection.xodrfrom directly overhead in each mode:--camera ortho— road widths stay constant edge to edge; the four arms are exactly parallel-projected.--camera top(perspective) — the same arms visibly taper toward the frame edges and the lane arrows shrink with distance.That difference is the feature working. GitHub attachments aren't reachable from the CLI; reproduce with
python scripts/editor_screenshot.py assets/samples/gs1_urban_intersection.xodr out.png --camera ortho.Tests
test_camera.cpp(+13): push-past (no stall, never below min, view-axis only, continuity, zoom-out doesn't relocate), ortho (toggle preserves pivot-plane scale, parallel matrix,zoom_aboutpins the anchor, perspective unaffected), cardinals (yaw-only snap, eye-side checks rather than yaw literals — a sign flip couldn't hide, near-vertical top-down keeps an orthonormal basis).test_framing.cpp(9): per-kind bounds, the three bug regressions, mixed union, unknown model still frames something, stale ids contribute nothing.test_projection.cpp(+3),test_picking.cpp(+3) for ortho.A test caught me twice.
PushPastMovesTheTargetAlongTheViewAxisOnlyfailed at first:zoom(20)only takes 80 m → 9.7 m, never reaching the 2 m minimum, so nothing pushed past — the test setup was wrong (needs ~40 steps). And the-Werrorbuild rejected adouble→floatnarrowing plus a wrong field name (PickHit::pointisposition) in the new picking test.Verification
ci-macos(-Werror); whole-treeclang-format --dry-run --Werrorclean.Vpreserves distance, yaw-only cardinals, near-vertical top-down);camera-navigation.mdgrows framing/projection/cardinals sections.Acceptance
GW-1 steps 4 and 7–14. With p1-s1 that's all of GW-1 bar a hand-run on a real mouse.