Skip to content

feat(editor): orbit-pivot navigation chords (#210)#258

Merged
JArmandoAnaya merged 1 commit into
mainfrom
p1-s1-orbit-chords
Jul 15, 2026
Merged

feat(editor): orbit-pivot navigation chords (#210)#258
JArmandoAnaya merged 1 commit into
mainfrom
p1-s1-orbit-chords

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

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: OrbitCamera already 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_target gained a dz, and elevate_target_pixels() lifts the pivot using the same depth scale as pan_pixels(). That scale came from a 50° FOV literal that was duplicated between pan_pixels() and matrices() and free to drift; it is now one constexpr kFovY behind a world_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:

Chord Gesture
+LMB orbit
+RMB zoom-drag (up = in)
+LMB+RMB · MMB pan (ground-anchored, unchanged math)
++LMB+RMB lift the pivot
RMB legacy orbit / context menu, by the 4 px slop

ViewportWidget consults 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

  • The gesture latches at the button transition, not on every move. Releasing 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.
  • A move with no buttons held drops a latched chord. Recovery for a release that never arrives — a Linux WM that grabs +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.)
  • A live gizmo drag still owns the mouse — chords don't interrupt it, which is also what happened before P1.
  • Hover stays frozen under a sub-slop right-press, exactly as before.

Two tests changed, and that's the point

OrbitCamera.ViewPlacesTargetOnNegativeZAxis and PickRay.CenterPixelPassesThroughCameraTarget both asserted against the origin while claiming to test the target — a shortcut that only held while the two coincided. They now read camera.target(); the intent is unchanged. Nothing else in the suite noticed the new default.

Tests

  • NEW 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

  • 770/770 green on the ci-macos preset (-Werror); whole-tree clang-format --dry-run --Werror clean.
  • ASan + UBSan build of the camera/nav/picking suites: clean (camera math changed → sanitizer run per the contributing rules).
  • No visual change to any capture. A loaded scene frames from scene_bounds, so the new default pivot never reaches it — verified by rendering gs1_urban_intersection.xodr on --camera orbit and --camera gs1 before/after (identical framing, no corruption). The editor visual artifacts job covers the canonical scenes on this run.
  • The chords themselves are gestures, so they are not screenshot-able; automated evidence is the NavController matrix, and GW-1 steps 1–3 / 5–6 need a hand-run on a real mouse.

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 Linux Alt+drag window-grab caveat and its alternates).

Not here (p1-s2, #211): push-past-pivot, F/V framing, ortho/persp, cardinals, Select→Q.

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
@JArmandoAnaya JArmandoAnaya merged commit 4656d90 into main Jul 15, 2026
14 checks passed
@JArmandoAnaya JArmandoAnaya deleted the p1-s1-orbit-chords branch July 15, 2026 20:30
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.

p1-s1: orbit-pivot camera model — rotate, zoom, pan, vertical pivot

1 participant