From 72145d4aba05eab66e7a49b383bee9c9f5764da3 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Sat, 17 Oct 2026 09:45:04 -0400 Subject: [PATCH] Grid: stay on the ground (XY) by default; following the view is a preference (#2035) #1583 turned the grid to the upright origin plane in side views for everyone. That is now Preferences > Grid follows the view, off by default; headless pictures keep the ground grid. Co-authored-by: Claude Opus 5.5 --- assets/locales/en.json | 2 ++ crates/engine/src/view.rs | 6 +++--- crates/ui-egui/src/prefs.rs | 8 ++++++++ crates/ui-egui/src/update/install.rs | 5 ++++- crates/ui-egui/src/viewport.rs | 3 ++- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/assets/locales/en.json b/assets/locales/en.json index 938b3274..18130fac 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -1146,6 +1146,8 @@ "ui.fully_constrained": "Fully constrained", "ui.general": "General", "ui.graphics": "Graphics", + "ui.grid_follows_view": "Grid follows the view", + "ui.grid_follows_view_hint": "Side views show the grid on the upright plane facing them instead of the ground", "ui.ground_shadow": "Ground shadow", "ui.groupSelected": "Group Selected", "ui.heading_add_ins": "ADD-INS", diff --git a/crates/engine/src/view.rs b/crates/engine/src/view.rs index e27e5027..5c1397ea 100644 --- a/crates/engine/src/view.rs +++ b/crates/engine/src/view.rs @@ -390,13 +390,13 @@ fn build_scene( }; let b = bounds(s); sc.radius = (b.diagonal() * 0.5).max(cam.half_height()).max(10.0) + b.center().dist(cam.target); - // Grid on the ground plane (XY with Z up, XZ with Y up), or the upright origin plane facing - // a view that sees the ground edge-on ([`grid_plane`]), drawn under the model as in the viewport. + // Grid on the ground plane (XY with Z up, XZ with Y up), drawn under the model as in the viewport. if guides.grid { let (minor, major) = grid_step(cam.half_height()); let ext = (cam.half_height() * 3.0 / major).ceil() * major; let tf = cam.to_frame(cam.target); - let (u, v) = grid_plane(cam.to_frame(cam.back())); + // Headless pictures keep the grid on the ground, the viewport's default. + let (u, v) = (Vec3::X, Vec3::Y); let snap = |a: Vec3| a * ((tf.dot(a) / major).round() * major); let c = snap(u) + snap(v); let w = |p: Vec3| cam.from_frame(p); diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index ac2cb5bd..ed8b8768 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -280,6 +280,9 @@ pub struct Prefs { pub space_mouse_axis_speed: [f32; 6], /// Anti-aliasing samples (applies on restart). pub msaa: u32, + /// The grid turns to the upright origin plane facing a side view (#1470). Off by default: the + /// grid stays on the ground plane (XY) whatever the view. + pub grid_follows_view: bool, /// How finely the viewport tessellates curved faces and edges: `coarse`, `normal`, `fine` or /// `ultra` (#1287). Only the display plan the viewport draws; never the design or exports. pub display_quality: String, @@ -410,6 +413,7 @@ impl Default for Prefs { space_mouse_reverse: [false; 6], space_mouse_axis_speed: [1.0; 6], msaa: 4, + grid_follows_view: false, display_quality: "normal".into(), supersampling: 1.0, supersampling_adaptive: true, @@ -1265,6 +1269,10 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) { ui.label(crate::i18n::tr_label("Grid")); ui.checkbox(&mut app.ui.show_grid, ""); ui.end_row(); + ui.label(crate::i18n::tr_label("Grid follows the view")); + ui.add_enabled(app.ui.show_grid, egui::Checkbox::without_text(&mut app.preferences.grid_follows_view)) + .on_hover_text(crate::i18n::tr_label("Side views show the grid on the upright plane facing them instead of the ground")); + ui.end_row(); ui.label(crate::i18n::tr_label("Ground shadow")); ui.checkbox(&mut app.ui.ground_shadow, ""); ui.end_row(); diff --git a/crates/ui-egui/src/update/install.rs b/crates/ui-egui/src/update/install.rs index 7f72cc10..ead6907e 100644 --- a/crates/ui-egui/src/update/install.rs +++ b/crates/ui-egui/src/update/install.rs @@ -290,7 +290,10 @@ fn unzip_one(zip: &Path, name: &str, to: &Path) -> Result { let file = std::fs::File::open(zip).map_err(|e| format!("{}: {e}", zip.display()))?; let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("{}: {e}", zip.display()))?; let index = (0..archive.len()).find(|&i| { - archive.name_for_index(i).is_some_and(|n| n.rsplit(['/', '\\']).next().is_some_and(|f| f.eq_ignore_ascii_case(name)) && !n.ends_with('/')) + archive + .name_for_index(i) + .and_then(Result::ok) + .is_some_and(|n| n.rsplit(['/', '\\']).next().is_some_and(|f| f.eq_ignore_ascii_case(name)) && !n.ends_with('/')) }); let Some(index) = index else { return Ok(false) }; let mut entry = archive.by_index(index).map_err(|e| format!("{}: {e}", zip.display()))?; diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index b3297d56..ddee0f50 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -733,7 +733,8 @@ fn build_sketch_scene(app: &SolveApp, proj: &Proj) -> GpuScene { // The ground plane: XY with Z up, XZ with Y up (laid out in the camera's Z-up frame), or // the upright origin plane facing the view when the ground is seen edge-on. let tf = app.cam.to_frame(app.cam.target); - let (u, v) = grid_plane(app.cam.to_frame(app.cam.back())); + // On the ground (XY) unless the Grid follows the view preference is on (#1470). + let (u, v) = if app.preferences.grid_follows_view { grid_plane(app.cam.to_frame(app.cam.back())) } else { (Vec3::X, Vec3::Y) }; let snap = |a: Vec3| a * ((tf.dot(a) / major).round() * major); let c = snap(u) + snap(v); let w3 = |p: Vec3| app.cam.from_frame(p).to_f32();