From e20cf1858a52d089a4ce1bdaa9a8c7532408efda Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 15 Oct 2026 14:48:24 -0400 Subject: [PATCH] Flat pattern gets its own pan and zoom through the shared navigation (#1484) The flat pattern now keeps a 2D view of its own, driven by navigation::handle with orbit off, so pan, zoom to the cursor and reverse zoom follow the navigation preset. Input over it no longer reaches the Design camera, and Fit and Home frame the pattern. Closes #1483. --- crates/ui-egui/src/dialogs_sheet.rs | 62 ++++++- crates/ui-egui/src/dialogs_sheet_tests.rs | 211 +++++++++++++++++++++- crates/ui-egui/src/lib.rs | 16 ++ crates/ui-egui/src/navigation.rs | 17 +- crates/ui-egui/src/viewport.rs | 8 +- 5 files changed, 297 insertions(+), 17 deletions(-) diff --git a/crates/ui-egui/src/dialogs_sheet.rs b/crates/ui-egui/src/dialogs_sheet.rs index 86f8e04d..07c1f0aa 100644 --- a/crates/ui-egui/src/dialogs_sheet.rs +++ b/crates/ui-egui/src/dialogs_sheet.rs @@ -808,7 +808,7 @@ fn bend_colors() -> (Color32, Color32) { /// The flat pattern view: the outline, cut-outs and bend lines (up and down in their colours) /// drawn flat over the viewport, fitted to it. True when the dialog is a flat pattern. -pub fn overlay(app: &SolveApp, painter: &egui::Painter, rect: Rect, d: &Dialog) -> bool { +pub fn overlay(app: &mut SolveApp, painter: &egui::Painter, rect: Rect, d: &Dialog) -> bool { let Kind::Sheet(Sm::FlatPattern { data, .. }) = &d.kind else { return false }; let t = Tokens::get(); let (up_col, down_col) = bend_colors(); @@ -842,9 +842,13 @@ pub fn overlay(app: &SolveApp, painter: &egui::Painter, rect: Rect, d: &Dialog) if area.width() <= 0.0 || area.height() <= 0.0 { return true; } - let scale = (f64::from(area.width()) / (hi.0 - lo.0)).min(f64::from(area.height()) / (hi.1 - lo.1)); - let ox = f64::from(area.center().x) - (lo.0 + hi.0) * 0.5 * scale; - let oy = f64::from(area.center().y) + (lo.1 + hi.1) * 0.5 * scale; + if !app.flat_view.fitted { + app.flat_view.fit(rect, area, lo, hi); + } + let cam = app.flat_view.cam; + let scale = f64::from(rect.height()) / (2.0 * cam.half_height()); + let ox = f64::from(rect.center().x) - cam.target.x * scale; + let oy = f64::from(rect.center().y) + cam.target.y * scale; let to = |p: (f64, f64)| -> Pos2 { pos2((ox + p.0 * scale) as f32, (oy - p.1 * scale) as f32) }; for l in &loops { let pts: Vec = l.iter().map(|p| to(*p)).collect(); @@ -914,6 +918,56 @@ pub fn overlay(app: &SolveApp, painter: &egui::Painter, rect: Rect, d: &Dialog) true } +/// The flat pattern's own 2D view: a top-down orthographic camera over the flat plane (x right, +/// y up, millimetres), so the shared navigation moves it like any other camera. The Design +/// camera is left alone while this has the viewport. +pub struct FlatView { + pub cam: solidcraft_engine::render::Camera, + /// The camera frames the pattern; cleared by Fit and Home, and whenever the dialog is closed. + pub fitted: bool, +} + +impl Default for FlatView { + fn default() -> Self { + let mut cam = solidcraft_engine::render::Camera::default(); + cam.set_view(solidcraft_engine::render::StandardView::Top); + cam.fov = 0.0; + cam.up = solidcraft_engine::render::UpAxis::Z; + FlatView { cam, fitted: false } + } +} + +impl FlatView { + /// Frame the pattern's bounds `lo..hi` in `area`, the part of `rect` the command panel leaves. + fn fit(&mut self, rect: Rect, area: Rect, lo: (f64, f64), hi: (f64, f64)) { + let scale = (f64::from(area.width()) / (hi.0 - lo.0)).min(f64::from(area.height()) / (hi.1 - lo.1)); + self.cam.target.x = (lo.0 + hi.0) * 0.5 - f64::from(area.center().x - rect.center().x) / scale; + self.cam.target.y = (lo.1 + hi.1) * 0.5 + f64::from(area.center().y - rect.center().y) / scale; + self.cam.target.z = 0.0; + self.cam.distance = f64::from(rect.height()) / (2.0 * scale) / 0.4; + self.fitted = true; + } +} + +/// The Create Flat Pattern dialog is open, so the viewport shows its 2D view. +pub(crate) fn flat_active(app: &SolveApp) -> bool { + matches!(app.dialog.as_ref().map(|d| &d.kind), Some(Kind::Sheet(Sm::FlatPattern { .. }))) +} + +/// Camera input for the flat pattern: the shared navigation without orbit, driving the flat view +/// instead of the Design camera. The result reports nothing under the pointer, so the model +/// beneath neither hovers, selects nor navigates. +pub(crate) fn navigate(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, resp: &egui::Response) -> crate::navigation::CameraInput { + use crate::navigation::{NavCaps, handle}; + let design = std::mem::replace(&mut app.cam, app.flat_view.cam); + let mut input = handle(app, ui, rect, resp, NavCaps { orbit: false, pick: false }); + app.flat_view.cam = std::mem::replace(&mut app.cam, design); + app.viewport.mouse = None; + input.inside = false; + input.hover = None; + input +} + fn pocket_label(app: &SolveApp, pocket: &Value) -> Option { let depth = pocket["depth_mm"].as_f64()?; if !depth.is_finite() || depth <= 0.0 { diff --git a/crates/ui-egui/src/dialogs_sheet_tests.rs b/crates/ui-egui/src/dialogs_sheet_tests.rs index 6de6beaf..7ef429b2 100644 --- a/crates/ui-egui/src/dialogs_sheet_tests.rs +++ b/crates/ui-egui/src/dialogs_sheet_tests.rs @@ -147,7 +147,7 @@ fn shallow_pocket_overlay_labels_depth_without_drawing_a_through_void() { let ctx = egui::Context::default(); let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("flat-pocket-test"))); - assert!(overlay(&app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog)); + assert!(overlay(&mut app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog)); }); output.textures_delta.clear(); let texts: Vec<&str> = output @@ -200,7 +200,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() { ui.label("Flat size: 50 × 10 × 2.5 mm"); }); let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Background, egui::Id::new("island-fit"))); - assert!(overlay(&app, &painter, Rect::from_min_max(pos2(250.0, 126.0), pos2(1400.0, 832.0)), &dialog)); + assert!(overlay(&mut app, &painter, Rect::from_min_max(pos2(250.0, 126.0), pos2(1400.0, 832.0)), &dialog)); }); output.textures_delta.clear(); } @@ -239,7 +239,7 @@ fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() { let ctx = egui::Context::default(); let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("coincident-pocket-test"))); - assert!(overlay(&app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog)); + assert!(overlay(&mut app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog)); }); output.textures_delta.clear(); let labels: Vec<_> = output @@ -472,3 +472,208 @@ fn convert_takes_the_fixed_face_and_the_bend_edges() { assert_eq!(bends(&mut app), Some(4)); assert!(app.session.model.results.iter().all(|r| r.error.is_none()), "{:?}", app.session.model.results); } + +// ---- flat pattern navigation ---- + +fn flat_app(preset: &str) -> SolveApp { + let mut app = plate(); + app.run("sheet.flange", json!({"sketch": "Base"})).unwrap(); + let flat = app.session.execute("sheet.flat_pattern", &json!({})).unwrap(); + app.start("sheet.flat_pattern"); + let Some(Kind::Sheet(Sm::FlatPattern { data, .. })) = app.dialog.as_mut().map(|d| &mut d.kind) else { panic!("flat dialog") }; + *data = Some(Ok(flat)); + app.viewport.no_pixels = true; + app.preferences.nav = preset.into(); + app.preferences.zoom_to_cursor = true; + app +} + +/// Same view in the flat plane: the Design viewport may anchor a zoom on the model's surface +/// (a little out of the plane), the flat view anchors on its plane. +fn same_view(a: &solidcraft_engine::render::Camera, b: &solidcraft_engine::render::Camera) -> bool { + (a.target.x - b.target.x).abs() < 1e-3 + && (a.target.y - b.target.y).abs() < 1e-3 + && (a.distance - b.distance).abs() < 1e-9 + && a.yaw == b.yaw + && a.pitch == b.pitch +} + +fn frame(app: &mut SolveApp, ctx: &egui::Context, time: &mut f64, mods: egui::Modifiers, events: Vec) { + *time += 1.0 / 60.0; + let events = std::iter::once(egui::Event::ModifiersChanged(mods)).chain(events).collect(); + let raw = egui::RawInput { + screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), + time: Some(*time), + focused: true, + events, + ..Default::default() + }; + let mut out = ctx.run_ui(raw, |ui| crate::viewport::show(app, ui)); + out.textures_delta.clear(); +} + +/// The same pan and zoom input moves the flat pattern's view exactly as it moves the Design +/// viewport's camera, under every navigation preset, with and without reverse zoom and zoom to the +/// cursor. The flat view never orbits. +#[test] +fn flat_pattern_pans_and_zooms_like_the_design_viewport() { + use egui::{Event, Modifiers, PointerButton}; + let none = Modifiers::NONE; + let chords = [none, Modifiers::SHIFT, Modifiers::CTRL, Modifiers::ALT, Modifiers::SHIFT | Modifiers::ALT]; + for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) { + for (reverse, to_cursor) in [(false, true), (true, true), (false, false)] { + let (flat_ctx, design_ctx) = (egui::Context::default(), egui::Context::default()); + let mut flat = flat_app(preset); + let mut design = flat_app(preset); + design.dialog = None; + for app in [&mut flat, &mut design] { + app.preferences.zoom_reverse = reverse; + app.preferences.zoom_to_cursor = to_cursor; + } + let (mut ft, mut dt) = (0.0, 0.0); + frame(&mut flat, &flat_ctx, &mut ft, none, vec![]); + frame(&mut flat, &flat_ctx, &mut ft, none, vec![]); + assert!(flat.flat_view.fitted); + // The Design viewport starts from the flat view's camera: top-down, orthographic. + design.cam = flat.flat_view.cam; + design.ui.perspective = false; + let design_before = flat.cam; + let (mut panned, mut zoomed) = (0, 0); + let at = pos2(300.0, 250.0); + for button in [PointerButton::Primary, PointerButton::Middle, PointerButton::Secondary] { + for mods in chords { + let mode = crate::prefs::nav_mode(&flat, button.into_nav(), mods); + if !matches!(mode, Some(crate::viewport::NavMode::Pan | crate::viewport::NavMode::Zoom)) { + continue; + } + let before = flat.flat_view.cam; + let mut both = |events: Vec, mods: Modifiers, flat: &mut SolveApp, design: &mut SolveApp| { + frame(flat, &flat_ctx, &mut ft, mods, events.clone()); + frame(design, &design_ctx, &mut dt, mods, events); + }; + both(vec![Event::PointerMoved(at)], mods, &mut flat, &mut design); + both(vec![Event::PointerButton { pos: at, button, pressed: true, modifiers: mods }], mods, &mut flat, &mut design); + let mut p = at; + for _ in 0..5 { + p += vec2(14.0, -9.0); + both(vec![Event::PointerMoved(p)], mods, &mut flat, &mut design); + } + both(vec![Event::PointerButton { pos: p, button, pressed: false, modifiers: mods }], mods, &mut flat, &mut design); + for _ in 0..10 { + both(vec![], none, &mut flat, &mut design); + } + assert!(same_view(&flat.flat_view.cam, &design.cam), "{preset}: {button:?} drag with {mods:?} ({mode:?})"); + assert_ne!(flat.flat_view.cam, before, "{preset}: {button:?} drag with {mods:?} moves the flat view"); + if mode == Some(crate::viewport::NavMode::Pan) { + panned += 1 + } else { + zoomed += 1 + } + flat.flat_view.cam = before; + design.cam = before; + } + } + assert!(panned + zoomed >= 1, "{preset}: the preset has a pan or zoom drag"); + // The wheel zooms (to the cursor when asked), identically. + let before = flat.flat_view.cam; + let wheel = Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta: vec2(0.0, 3.0), modifiers: none, phase: egui::TouchPhase::Move }; + for (app, ctx, t) in [(&mut flat, &flat_ctx, &mut ft), (&mut design, &design_ctx, &mut dt)] { + frame(app, ctx, t, none, vec![Event::PointerMoved(at)]); + frame(app, ctx, t, none, vec![wheel.clone()]); + for _ in 0..30 { + frame(app, ctx, t, none, vec![]); + } + } + assert_ne!(flat.flat_view.cam.distance, before.distance, "{preset}: the wheel zooms the flat view"); + assert!(same_view(&flat.flat_view.cam, &design.cam), "{preset} reverse {reverse} cursor {to_cursor}: wheel zoom"); + assert_eq!(flat.flat_view.cam.distance < before.distance, !reverse, "{preset}: reverse zoom flips the wheel"); + if to_cursor { + assert!(flat.flat_view.cam.target.dist(before.target) > 1e-6, "{preset}: zoom to the cursor moves the target"); + } + assert_eq!(flat.flat_view.cam.pitch, before.pitch); + assert_eq!(flat.flat_view.cam.yaw, before.yaw); + assert_eq!(flat.cam, design_before, "{preset}: the Design camera is untouched"); + } + } +} + +trait IntoNav { + fn into_nav(self) -> crate::prefs::NavButton; +} +impl IntoNav for egui::PointerButton { + fn into_nav(self) -> crate::prefs::NavButton { + match self { + egui::PointerButton::Middle => crate::prefs::NavButton::Middle, + egui::PointerButton::Secondary => crate::prefs::NavButton::Right, + _ => crate::prefs::NavButton::Left, + } + } +} + +/// Input over the flat pattern, including orbit bindings, wheel and pinch, leaves the Design +/// camera and selection alone. +#[test] +fn design_camera_is_unchanged_while_the_flat_pattern_has_focus() { + use egui::{Event, Modifiers, PointerButton}; + let none = Modifiers::NONE; + for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) { + let ctx = egui::Context::default(); + let mut app = flat_app(preset); + let mut t = 0.0; + frame(&mut app, &ctx, &mut t, none, vec![]); + let cam = app.cam; + let at = pos2(300.0, 250.0); + for button in [PointerButton::Primary, PointerButton::Middle, PointerButton::Secondary] { + for mods in [none, Modifiers::SHIFT, Modifiers::CTRL, Modifiers::ALT, Modifiers::SHIFT | Modifiers::ALT] { + frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerMoved(at)]); + frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerButton { pos: at, button, pressed: true, modifiers: mods }]); + for i in 1..6 { + frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerMoved(at + vec2(14.0, -9.0) * i as f32)]); + } + frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerButton { pos: at, button, pressed: false, modifiers: mods }]); + } + } + let wheel = Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta: vec2(0.0, 3.0), modifiers: none, phase: egui::TouchPhase::Move }; + frame(&mut app, &ctx, &mut t, none, vec![wheel]); + frame(&mut app, &ctx, &mut t, none, vec![Event::Zoom(1.3)]); + for _ in 0..30 { + frame(&mut app, &ctx, &mut t, none, vec![]); + } + assert_eq!(app.cam, cam, "{preset}: the Design camera stays put under the flat pattern"); + assert!(app.session.selection.is_empty(), "{preset}: nothing underneath is selected"); + assert_eq!(app.flat_view.cam.pitch, app.flat_view.cam.pitch.clamp(1.5, 2.0), "{preset}: still looking straight down"); + } +} + +/// Fit and Home frame the flat pattern (not the model), and leave the Design camera alone. +#[test] +fn fit_and_home_frame_the_flat_pattern() { + let ctx = egui::Context::default(); + let mut app = flat_app("fusion"); + let mut t = 0.0; + let none = egui::Modifiers::NONE; + frame(&mut app, &ctx, &mut t, none, vec![]); + let fitted = app.flat_view.cam; + // The 100 x 60 pattern sits inside the view with room for the command panel. + let scale = 600.0 / (2.0 * fitted.half_height()); + assert!(100.0 * scale < 800.0 && 60.0 * scale < 600.0 && 100.0 * scale > 200.0, "fits: {scale}"); + let design = app.cam; + for view in ["fit", "home"] { + app.flat_view.cam.pan(120.0, -80.0, 600.0); + app.flat_view.cam.zoom_at(0.3, None); + assert_ne!(app.flat_view.cam, fitted); + app.animate_view(view); + frame(&mut app, &ctx, &mut t, none, vec![]); + assert!(app.flat_view.cam.target.dist(fitted.target) < 1e-9 && (app.flat_view.cam.distance - fitted.distance).abs() < 1e-9, "{view} refits"); + assert_eq!(app.cam, design, "{view} leaves the Design camera alone"); + } + app.flat_view.cam.pan(50.0, 50.0, 600.0); + app.fit_view(); + frame(&mut app, &ctx, &mut t, none, vec![]); + assert!(app.flat_view.cam.target.dist(fitted.target) < 1e-9, "fit_view refits"); + // Closing the dialog returns the viewport to the Design camera, and reopening refits. + app.flat_view.cam.pan(50.0, 50.0, 600.0); + app.dialog = None; + frame(&mut app, &ctx, &mut t, none, vec![]); + assert!(!app.flat_view.fitted); +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index de50cb2c..cb07b176 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -363,6 +363,8 @@ pub struct SolveApp { styled: bool, fitted: bool, pending_fit: Option<(Camera, solidcraft_engine::geom::Aabb3, bool)>, + /// The sheet metal flat pattern's own pan and zoom. + pub flat_view: dialogs_sheet::FlatView, /// The start page. pub home: home::HomeState, pub file_load: file_load::FileLoad, @@ -448,6 +450,7 @@ impl SolveApp { styled: false, fitted: false, pending_fit: None, + flat_view: Default::default(), home: home::HomeState::default(), file_load: file_load::FileLoad::default(), docs: documents::Documents::default(), @@ -986,6 +989,10 @@ impl SolveApp { } pub fn fit_view(&mut self) { + if dialogs_sheet::flat_active(self) { + self.flat_view.fitted = false; + return; + } if render_workspace::active(self) { self.cancel_view_animation(); match solidcraft_engine::product_scene::extract(&self.session) { @@ -1073,6 +1080,10 @@ impl SolveApp { /// Frame everything shown, keeping the orientation (animated; the selection is ignored). pub fn animate_fit_all(&mut self) { + if dialogs_sheet::flat_active(self) { + self.animate_view("fit"); + return; + } if render_workspace::active(self) { // The Render preview frames its product scene (see `fit_view`). self.fit_view(); @@ -1087,6 +1098,11 @@ impl SolveApp { /// Animated view changes for the axis view and navigation bar. pub fn animate_view(&mut self, v: &str) { + // The flat pattern is a 2D view: Fit and Home frame it, and the Design camera stays put. + if dialogs_sheet::flat_active(self) && matches!(v, "fit" | "home") { + self.flat_view.fitted = false; + return; + } if render_workspace::active(self) && matches!(v, "fit" | "home") { if v == "home" { self.cam.set_view(StandardView::Iso); diff --git a/crates/ui-egui/src/navigation.rs b/crates/ui-egui/src/navigation.rs index 64719c9d..654ed752 100644 --- a/crates/ui-egui/src/navigation.rs +++ b/crates/ui-egui/src/navigation.rs @@ -127,11 +127,12 @@ pub(crate) fn pinch_factor(input: &egui::InputState) -> f32 { /// Zoom anchor: the model point under the cursor, else the target's depth. /// Preferences › Navigation › Zoom to the cursor (off: toward the view's centre). -pub(crate) fn zoom_anchor(app: &mut SolveApp, rect: egui::Rect, hover: Option) -> Option { +pub(crate) fn zoom_anchor(app: &mut SolveApp, rect: egui::Rect, hover: Option, surface: bool) -> Option { hover.filter(|_| app.preferences.zoom_to_cursor).map(|p| { let proj = projection(app, rect); // Toward the model's surface under the cursor, else the target's depth. - if let Some(q) = pick_cached(app, &proj, p).iter().find_map(|h| if let Hit::Face { point, .. } = h { Some(*point) } else { None }) { + if surface && let Some(q) = pick_cached(app, &proj, p).iter().find_map(|h| if let Hit::Face { point, .. } = h { Some(*point) } else { None }) + { return q; } let (o, d) = proj.ray(p); @@ -242,7 +243,7 @@ pub(crate) fn handle(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, resp: &e app.viewport.mouse = hover.filter(|_| inside); // Ctrl or Alt held: drawing and dragging don't snap. crate::drag_snap::set_suppressed(mods.ctrl || mods.alt || mods.command); - middle_double_click(app, ui, inside, caps.orbit); + middle_double_click(app, ui, inside); // ---- navigation ---- let trackpad_pan = app.preferences.trackpad_pans(); @@ -278,7 +279,7 @@ pub(crate) fn handle(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, resp: &e app.viewport.orbit_free = None; // Spread zooms in: the camera distance shrinks by the pinch factor. let f = f64::from(pinch).powf(-crate::prefs::zoom_sign(app)); - let anchor = zoom_anchor(app, rect, hover); + let anchor = zoom_anchor(app, rect, hover, caps.orbit); if app.preferences.nav == crate::prefs::TOUCHPAD_NAV { let mut params = json!({"motion":"zoom", "factor":f.clamp(0.05,20.0)}); if let Some(anchor) = anchor { @@ -294,7 +295,7 @@ pub(crate) fn handle(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, resp: &e app.viewport.orbit_snap = None; app.viewport.orbit_free = None; let f = (-scroll as f64 * 0.0015 * crate::prefs::zoom_sign(app)).exp(); - let anchor = zoom_anchor(app, rect, hover); + let anchor = zoom_anchor(app, rect, hover, caps.orbit); if app.preferences.nav == crate::prefs::TOUCHPAD_NAV { let mut params = json!({"motion":"zoom", "factor":f.clamp(0.05,20.0)}); if let Some(anchor) = anchor { @@ -569,7 +570,7 @@ pub(crate) fn step_orbit_snap(app: &mut SolveApp, rect: Rect, dt: f64) { /// A middle-button double-click goes Home (or fits, or nothing: Preferences › Navigation). Only /// clicks count: a press that moves (a pan or orbit drag) or is held is not one, and the two /// clicks must both be the middle button's, close together in time and place. -pub(crate) fn middle_double_click(app: &mut SolveApp, ui: &egui::Ui, inside: bool, orbit: bool) { +pub(crate) fn middle_double_click(app: &mut SolveApp, ui: &egui::Ui, inside: bool) { use egui::PointerButton::{Extra1, Extra2, Middle, Primary, Secondary}; let (double, releases, middle, other, dragging) = ui.input(|i| { ( @@ -588,9 +589,7 @@ pub(crate) fn middle_double_click(app: &mut SolveApp, ui: &egui::Ui, inside: boo return; } match crate::prefs::MiddleDoubleClick::of(app) { - // Home turns the view; a flat page only frames its content. - crate::prefs::MiddleDoubleClick::Home if orbit => app.animate_view("home"), - crate::prefs::MiddleDoubleClick::Home => app.animate_fit_all(), + crate::prefs::MiddleDoubleClick::Home => app.animate_view("home"), crate::prefs::MiddleDoubleClick::Fit => app.animate_fit_all(), crate::prefs::MiddleDoubleClick::Off => {} } diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index ec9fde46..a2b72dbf 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -2100,6 +2100,12 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) { app.viewport.rect = Some(rect); refresh_layout(app, ui.ctx().pixels_per_point()); let resp = ui.interact(rect, ui.id().with("viewport"), Sense::click_and_drag()); + let flat = crate::dialogs_sheet::flat_active(app); + if !flat { + app.flat_view.fitted = false; + } + let input = + if flat { crate::dialogs_sheet::navigate(app, ui, rect, &resp) } else { crate::navigation::handle(app, ui, rect, &resp, NavCaps::MODEL) }; let CameraInput { hover, scroll, @@ -2115,7 +2121,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) { pan, primary_nav, mode, - } = crate::navigation::handle(app, ui, rect, &resp, NavCaps::MODEL); + } = input; app.viewport.physical_scale = ui.ctx().pixels_per_point(); let proj = projection(app, rect); if let Ok(mut cache) = app.viewport.display.lock() {