feat(editor): orbit-pivot navigation chords (#210)#258
Merged
Conversation
The camera was already a headless orbit-pivot camera, so this is pivot semantics plus input chords, not the rewrite the issue described (see docs/roadmap/pillars/p1_discovery.md, committed here). The pivot now starts 1.5 m above the origin, so an empty scene orbits a point in the world rather than a spot on the ground plane, and it can be lifted: elevate_target_pixels() reuses pan_pixels()' depth scale, which is now a single kFovY — the 50 deg literal had been duplicated between pan_pixels() and matrices() and could drift. Navigation chords live in a new headless NavController that the widget consults before the gizmo and the tools, so a chord can never leak into an edit: Alt+LMB orbits, Alt+RMB drag-zooms (up = in), Alt+LMB+RMB and MMB pan, Alt+Shift+LMB+RMB lifts the pivot. Right-drag orbit stays as a documented legacy alternate, still telling a click from a drag by the 4 px slop. The gesture latches at the button transition, so releasing Alt mid-drag finishes the gesture instead of dropping it, and a move with no buttons held recovers a chord whose release never arrived (a WM that grabs Alt+drag to move the window). A live gizmo drag still owns the mouse, and the plain-LMB gizmo -> tool path is unchanged; tools never used Alt, MMB, or the wheel, so the chord space was free. Two tests assumed the pivot was the origin and now read the target instead: the intent (target maps to (0,0,-distance); the centre ray passes through the target) is unchanged. Refs #210
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.
First sprint of Pillar P1 — Interaction & Navigation (epic #250). Pivot semantics + the ⌥ chord state machine.
Closes #210
The issue's premise was wrong, so this ships the discovery report too
#210 said "replace the free camera". There is no free camera:
OrbitCameraalready was a headless orbit-pivot camera, target/yaw/pitch/distance, unit-tested without a window. So this sprint is pivot semantics + input chords, not a rewrite — and the camera keeps its shape.The full P1 discovery pass (all 8 scope items against the code, the decisions it forced, the GW-1 amendments it proposes) lands here as
docs/roadmap/pillars/p1_discovery.md, linked from the roadmap README and posted on #250. Issues #210–#213 were amended to match.What changed
The pivot starts 1.5 m above the origin (GW-1 step 1) — an empty scene orbits a point in the world, not a spot on the ground plane.
move_targetgained adz, andelevate_target_pixels()lifts the pivot using the same depth scale aspan_pixels(). That scale came from a 50° FOV literal that was duplicated betweenpan_pixels()andmatrices()and free to drift; it is now oneconstexpr kFovYbehind aworld_per_pixel()helper.The chords live in a new headless
NavController(viewport/nav_controller.{hpp,cpp}) — no camera, no widget, no GL, so the whole matrix is testable without a window:⌥+LMB⌥+RMB⌥+LMB+RMB · MMB⌥+⇧+LMB+RMBViewportWidgetconsults it before the gizmo and the tools, so a chord can never leak into an edit. Discovery is what made this safe: tools only ever receive LMB + key_press — none uses ⌥, MMB, or the wheel — so the chord space was unoccupied and the plain-LMB gizmo→tool path is untouched.Design calls worth reviewing
⌥mid-drag finishes the gesture instead of dropping it; the cost is that adding⇧to a live pan doesn't upgrade it to a pivot lift. Documented in the header and the user guide.⌥+drag to move the window would otherwise leave the camera orbiting a button nobody is pressing. (Not in the sprint plan; found while re-reading the wiring.)Two tests changed, and that's the point
OrbitCamera.ViewPlacesTargetOnNegativeZAxisandPickRay.CenterPixelPassesThroughCameraTargetboth asserted against the origin while claiming to test the target — a shortcut that only held while the two coincided. They now readcamera.target(); the intent is unchanged. Nothing else in the suite noticed the new default.Tests
test_nav_controller.cpp(14 tests): the chord table,PlainLmbIsNotConsumed,ReleaseOrderUnwindsChordsCleanly(both orders), slop behaviour, latching, lost-release recovery.test_camera.cpp:DefaultPivotSitsAboveOrigin,ElevateTargetUsesExactDepthScale,AnchoredPanUnaffectedByRaisedPivot.Verification
ci-macospreset (-Werror); whole-treeclang-format --dry-run --Werrorclean.scene_bounds, so the new default pivot never reaches it — verified by renderinggs1_urban_intersection.xodron--camera orbitand--camera gs1before/after (identical framing, no corruption). Theeditor visual artifactsjob covers the canonical scenes on this run.Acceptance
GW-1 steps 1–3 and 5–6, bindings documented per platform (NEW
docs/user-guide/camera-navigation.md, linked from the user-guide index, with the LinuxAlt+drag window-grab caveat and its alternates).Not here (p1-s2, #211): push-past-pivot,
F/Vframing, ortho/persp, cardinals, Select→Q.