From 8fa566af331b4b843204d7d8a729f22cf3178237 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 8 Oct 2026 13:40:34 +0000 Subject: [PATCH] Continuous orbit (navigation bar): the view turns about the vertical until the next click or key Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- crates/ui-egui/src/icons.rs | 8 ++++++++ crates/ui-egui/src/viewport.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index d6dc0167..0fb7a3f3 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -421,6 +421,14 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce pen.arrow((17.0, 17.5), (19.6, 14.5), a); pen.dot(12.0, 12.0, ink); } + "spin" => { + // A turntable: an ellipse with an arrow running round it, a post in the middle. + let pts: Vec = (0..=28).map(|i| 0.2 + i as f32 / 28.0 * 5.6).map(|t| pen.at(12.0 + 9.0 * t.cos(), 16.0 + 3.5 * t.sin())).collect(); + p.add(Shape::line(pts, Stroke::new(pen.w, ink))); + pen.arrow((17.0, 19.0), (20.5, 17.0), a); + pen.line(&[(12.0, 15.5), (12.0, 4.0)], ink); + pen.poly(&[(9.0, 9.0), (15.0, 9.0), (15.0, 4.0), (9.0, 4.0)], Color32::TRANSPARENT, ink); + } "pan" => { for (dx, dy) in [(0.0, -1.0), (0.0, 1.0), (-1.0, 0.0), (1.0, 0.0)] { pen.arrow((12.0 + 3.0 * dx, 12.0 + 3.0 * dy), (12.0 + 9.0 * dx, 12.0 + 9.0 * dy), ink); diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 8aec1121..1419c6c5 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -47,6 +47,8 @@ pub struct ViewportState { pub hover_bodies: Vec, /// The model point an orbit turns around (Preferences: orbit around the cursor). pub orbit_pivot: Option, + /// Continuous orbit: the view turns about the vertical until the next click or key. + pub spin: bool, pub build_ms: f64, /// The right-click menu, open at this screen position. pub context_menu: Option, @@ -1221,6 +1223,23 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) { if !dragging { app.viewport.orbit_pivot = None; } + if app.viewport.spin { + // A click on the navigation bar (its own button) is not a stop. + let bar = Rect::from_center_size(pos2(rect.center().x, rect.bottom() - 22.0), vec2(320.0, 30.0)); + let (pressed, key, dt) = ui.input(|i| { + ( + i.pointer.any_pressed() && inside && !i.pointer.press_origin().is_some_and(|p| bar.contains(p)), + i.events.iter().any(|e| matches!(e, egui::Event::Key { pressed: true, .. })), + i.stable_dt, + ) + }); + if pressed || key || dragging { + app.viewport.spin = false; + } else { + app.cam.orbit(f64::from(dt.min(0.1)) * 30.0, 0.0); + ui.ctx().request_repaint(); + } + } if !app.cam.is_valid() { app.cam = Camera::default(); app.fit_view(); @@ -1897,8 +1916,9 @@ pub fn look_at_selection(app: &mut SolveApp) { fn nav_bar(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) { let t = Tokens::get(); - let items: [(&str, &str); 9] = [ + let items: [(&str, &str); 10] = [ ("orbit", "Orbit (drag; or Shift+middle / right drag)"), + ("spin", "Continuous orbit (stops at the next click or key)"), ("pan", "Pan (drag; or middle drag)"), ("zoom", "Zoom (drag; or wheel)"), ("fit", "Fit (F6)"), @@ -1921,6 +1941,7 @@ fn nav_bar(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) { "zoom" => app.viewport.nav == Some(NavMode::Zoom), "perspective" => app.ui.perspective, "settings" => app.ui.show_grid, + "spin" => app.viewport.spin, _ => false, }; if active || resp.hovered() { @@ -1934,6 +1955,10 @@ fn nav_bar(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) { "pan" => app.viewport.nav = toggle(NavMode::Pan, app.viewport.nav), "zoom" => app.viewport.nav = toggle(NavMode::Zoom, app.viewport.nav), "fit" => app.animate_view("fit"), + "spin" => { + app.cancel_view_animation(); + app.viewport.spin = !app.viewport.spin; + } "home" => app.animate_view("home"), "lookat" => look_at_selection(app), "style" => app.ui.visual_style = (app.ui.visual_style + 1) % 4,