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() {