From f30b30be6ecc4e2e6544a851243137c01a354934 Mon Sep 17 00:00:00 2001 From: N3MI-DG Date: Sun, 11 Oct 2026 09:19:05 +1100 Subject: [PATCH] =?UTF-8?q?A=20left=20press=20on=20the=20view=20cube=20hel?= =?UTF-8?q?d=20and=20dragged=20orbits=20the=20view:=20the=20press=20is=20n?= =?UTF-8?q?ot=20a=20click=20until=20it=20moves=20or=20is=20held=20past=20o?= =?UTF-8?q?ne,=20then=20the=20pointer=20grabs=20and=20the=20view=20orbits?= =?UTF-8?q?=20about=20its=20centre=20as=20the=20other=20orbits=20do=20(the?= =?UTF-8?q?=20move=20it=20took=20to=20tell=20a=20drag=20from=20a=20click?= =?UTF-8?q?=20turns=20the=20view=20too).=20A=20press=20that=20doesn't=20mo?= =?UTF-8?q?ve=20is=20still=20a=20click=20on=20the=20face,=20edge=20or=20co?= =?UTF-8?q?rner.=20While=20the=20cube=20is=20dragged=20its=20hover=20highl?= =?UTF-8?q?ight=20is=20off,=20the=20release=20is=20not=20a=20click,=20and?= =?UTF-8?q?=20the=20navigation=20bar's=20orbit=20or=20pan=20tool=20doesn't?= =?UTF-8?q?=20also=20act=20on=20the=20drag.=20A=20press=20on=20a=20floatin?= =?UTF-8?q?g=20window=20over=20the=20cube=20is=20the=20window's=20and=20st?= =?UTF-8?q?arts=20no=20orbit,=20as=20with=20the=20cube's=20hover.=20Tests:?= =?UTF-8?q?=20the=20s39=20scenario=20drags=2048=20px=20right=20from=20FRON?= =?UTF-8?q?T=20and=20the=20view=20turns=2022=C2=B0=20without=20clicking=20?= =?UTF-8?q?the=20face?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/viewport.rs | 45 +++++++++++++++++-- .../tests/scenarios/s39_view_cube_hover.json | 5 +++ docs/book/src/guide/ui-tour.md | 3 +- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 98ce8116..081865df 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -51,6 +51,9 @@ pub struct ViewportState { pub orbit_pivot: Option, /// Continuous orbit: the view turns about the vertical until the next click or key. pub spin: bool, + /// A left press on the view cube, held: where the pointer was when the view last orbited + /// (dragging from the cube orbits; a press that doesn't move is a click). + pub cube_drag: Option, pub build_ms: f64, /// The right-click menu, open at this screen position. pub context_menu: Option, @@ -1401,7 +1404,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) { let (dx, dy) = (delta.x as f64, delta.y as f64); let mode = if middle || secondary_down { crate::prefs::nav_mode(app, middle, secondary_down, mods) - } else if primary_down { + } else if primary_down && app.viewport.cube_drag.is_none() { app.viewport.nav } else { None @@ -2080,21 +2083,55 @@ fn cube_basis(cam: &Camera) -> (Vec3, Vec3, Vec3) { (cam.to_frame(r), cam.to_frame(u), cam.to_frame(b)) } +/// A left press on the view cube held and dragged orbits the view about its centre, the model +/// following the pointer as with the other orbits. The press itself turns nothing: a press that +/// doesn't move is a click. A press on a floating window over the cube is the window's. Returns +/// whether the cube is being dragged (held past a click). +fn view_cube_orbit(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, targets: &[CubeTarget], c: Pos2) -> bool { + let (pressed, down, decided, origin, pos) = ui.input(|i| { + let p = &i.pointer; + (p.primary_pressed(), p.primary_down(), p.is_decidedly_dragging(), p.press_origin(), p.interact_pos()) + }); + if pressed && ui.rect_contains_pointer(rect) { + let (r, u, b) = cube_basis(&app.cam); + let shapes = cube_projected(targets, (r, u, b), c); + app.viewport.cube_drag = origin.filter(|p| cube_hit(&shapes, *p).is_some()); + } + if !down { + app.viewport.cube_drag = None; + } + let Some(last) = app.viewport.cube_drag.filter(|_| decided) else { return false }; + ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing); + // From where the view last turned (the press, at first), so the move it took to tell a drag + // from a click turns the view too. + if let Some(p) = pos + && p != last + { + app.cancel_view_animation(); + app.cam.orbit(f64::from(p.x - last.x), f64::from(p.y - last.y)); + app.viewport.cube_drag = Some(p); + ui.ctx().request_repaint(); + } + true +} + /// The view cube (top right): a cube with chamfered corners. Its 26 targets are the 6 faces /// (orthographic views), the 12 edges (the views halfway between their two faces) and the 8 /// corner facets (iso views); an edge is picked by a band along it on both faces it joins. -/// Hover lights the target under the pointer, a click turns the view to it, the house goes home. +/// Hover lights the target under the pointer, a click turns the view to it, a drag from the cube +/// orbits, the house goes home. fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) { let t = Tokens::get(); let c = pos2(rect.right() - 80.0, rect.top() + 80.0); + let targets = cube_targets(); + let orbiting = view_cube_orbit(app, ui, rect, &targets, c); let (r, u, b) = cube_basis(&app.cam); let (wr, wu, _) = app.cam.basis(); let painter = ui.painter_at(rect); let hover = ui.input(|i| i.pointer.hover_pos()).filter(|_| ui.rect_contains_pointer(rect)); - let targets = cube_targets(); let drawn = cube_drawn(&targets, (r, u, b), c); let shapes = cube_projected(&targets, (r, u, b), c); - let hovered = hover.and_then(|h| cube_hit(&shapes, h)); + let hovered = if orbiting { None } else { hover.and_then(|h| cube_hit(&shapes, h)) }; // Axis triad from the cube's back corner, drawn first: the cube hides what is behind it. // The corner is in the cube's frame, the axes are the world's. let o = Vec3::new(-1.0, -1.0, -1.0); diff --git a/crates/ui-egui/tests/scenarios/s39_view_cube_hover.json b/crates/ui-egui/tests/scenarios/s39_view_cube_hover.json index 5f0b30d4..b74fe48e 100644 --- a/crates/ui-egui/tests/scenarios/s39_view_cube_hover.json +++ b/crates/ui-egui/tests/scenarios/s39_view_cube_hover.json @@ -31,6 +31,11 @@ {"call": "ui.view", "params": {"view": "home"}}, {"click": {"handle": "cube:top-back-right"}}, {"expect": {"camera": {"yaw_deg": -135, "pitch_deg": 35.26}}}, + {"note": "Pressing on the cube and dragging orbits the view by the whole drag (48 px right: 22°), and the release is not a click on the face pressed."}, + {"call": "ui.view", "params": {"view": "front"}}, + {"expect": {"camera": {"yaw_deg": 0, "pitch_deg": 0}}}, + {"drag": [{"cube": [0, -1, 0]}, {"cube": [1.5, -1, 0]}]}, + {"expect": {"camera": {"yaw_deg": 22, "pitch_deg": 0}}}, {"move": [800, 500]}, {"expect": {"count": {"name": "cube_highlights", "value": 0}}} ] diff --git a/docs/book/src/guide/ui-tour.md b/docs/book/src/guide/ui-tour.md index ef83c3dc..55e55d66 100644 --- a/docs/book/src/guide/ui-tour.md +++ b/docs/book/src/guide/ui-tour.md @@ -17,7 +17,8 @@ Preferences switches to SolidWorks or Inventor style). The view cube and the navigation bar change the view: a cube face turns to look straight at it (TOP and BOTTOM keep the current heading, squared to the nearest quarter turn), an edge to the view halfway between - its two faces, a chamfered corner to that corner's iso view, and the house goes home. Right-click for the marking menu: eight commands around the cursor, plus a + its two faces, a chamfered corner to that corner's iso view, and the house goes home. + Press on the cube and drag to orbit. Right-click for the marking menu: eight commands around the cursor, plus a menu for whatever is under it. - **Dialogs** dock on the right with a live preview. Most features also show drag handles and a value box in the viewport.