Compare commits

...
Author SHA1 Message Date
bherbruckandClaude Sonnet 5.5 a940f77447 Sketch: always show a grid on the open sketch's plane
While a sketch is open its grid is drawn on the sketch's own plane, centred
near the view target and snapped to the major step, whatever the Grid switch,
the sketch's Sketch Grid option or Grid follows the view say. Headless window
pictures follow. Outside a sketch nothing changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-18 01:44:15 +00:00
3 changed files with 72 additions and 25 deletions

No files matched your search

+25 -6
View File
@@ -349,6 +349,20 @@ pub fn scene_with_section(s: &Session, cam: &Camera, section: Option<(Vec3, Vec3
scene_with_guides(s, cam, section, Guides::default())
}
/// The plane of the sketch that is open, if any: it always shows a grid, whatever the Grid switch
/// says (outside a sketch the grid is the user's choice).
pub fn open_sketch_plane(s: &Session) -> Option<Plane> {
let id = s.active_sketch?;
s.world_state().sketch(id).map(|ss| ss.plane)
}
/// The grid on a sketch plane: its centre (the view target's foot on the plane, snapped to the
/// major step in the plane's own axes) and the plane's x and y directions.
pub fn sketch_grid_frame(plane: &Plane, target: Vec3, major: f64) -> (Vec3, Vec3, Vec3) {
let snap = |d: Vec3| d * (((target - plane.origin).dot(d) / major).round() * major);
(plane.origin + snap(plane.x) + snap(plane.y), plane.x, plane.y)
}
/// What a headless render draws besides the model: the ground grid and the origin's axes.
/// Both are on by default; the grid lies on Z 0, so it cuts through a model that straddles it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -434,12 +448,17 @@ fn build_scene(
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);
// 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);
// Headless pictures keep the grid on the ground, the viewport's default; an open sketch
// has its own plane's grid (world coordinates, no camera frame).
let (c, u, v, w): (Vec3, Vec3, Vec3, Box<dyn Fn(Vec3) -> Vec3>) = if let Some(pl) = open_sketch_plane(s) {
let (c, u, v) = sketch_grid_frame(&pl, cam.target, major);
(c, u, v, Box::new(|p| p))
} else {
let tf = cam.to_frame(cam.target);
let (u, v) = (Vec3::X, Vec3::Y);
let snap = |a: Vec3| a * ((tf.dot(a) / major).round() * major);
(snap(u) + snap(v), u, v, Box::new(|p| cam.from_frame(p)))
};
let n = ((ext / minor) as i64).clamp(1, 400);
for i in -n..=n {
let t = i as f64 * minor;
+1 -1
View File
@@ -208,7 +208,7 @@ pub(crate) fn view_scene(app: &SolveApp, guides: solidcraft_engine::view::Guides
/// The grid and the origin's axes as the window shows them (the Grid and Origin switches).
pub(crate) fn window_guides(app: &SolveApp) -> solidcraft_engine::view::Guides {
solidcraft_engine::view::Guides { grid: app.ui.show_grid && !crate::viewport::active_view(app).hide_grid, axes: app.origin_visible() }
solidcraft_engine::view::Guides { grid: app.session.active_sketch.is_some() || (app.ui.show_grid && !crate::viewport::active_view(app).hide_grid), axes: app.origin_visible() }
}
/// A picture of the view as the window shows it, RGBA, drawn on the GPU with the viewport's
+46 -18
View File
@@ -733,33 +733,39 @@ fn build_sketch_scene(app: &SolveApp, proj: &Proj) -> GpuScene {
let s = &app.session;
let st = s.world_state();
let mut fill_budget = 131_072;
if app.ui.show_grid && !active_view(app).hide_grid {
let sketch_plane = solidcraft_engine::view::open_sketch_plane(s);
if sketch_plane.is_some() || (app.ui.show_grid && !active_view(app).hide_grid) {
let (minor, major) = grid_step(app.cam.half_height());
let ext = (app.cam.half_height() * 3.0 / major).ceil() * major;
// 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);
// On the ground (XY) unless the Grid follows the view preference is on (#1470); then on
// the plane the last frame drew, so the choice is hysteretic near the thresholds (#1993).
let (u, v) = if app.preferences.grid_follows_view {
app.viewport.grid_plane.lock().map_or((Vec3::X, Vec3::Y), |mut last| {
*last = grid_plane(app.cam.to_frame(app.cam.back()), *last);
last.axes()
})
// An open sketch always has its grid on its own plane; otherwise 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 (c, u, v, w3): (Vec3, Vec3, Vec3, Box<dyn Fn(Vec3) -> [f32; 3]>) = if let Some(pl) = &sketch_plane {
let (c, u, v) = solidcraft_engine::view::sketch_grid_frame(pl, app.cam.target, major);
(c, u, v, Box::new(|p| p.to_f32()))
} else {
(Vec3::X, Vec3::Y)
let tf = app.cam.to_frame(app.cam.target);
// On the ground (XY) unless the Grid follows the view preference is on (#1470); then on
// the plane the last frame drew, so the choice is hysteretic near the thresholds (#1993).
let (u, v) = if app.preferences.grid_follows_view {
app.viewport.grid_plane.lock().map_or((Vec3::X, Vec3::Y), |mut last| {
*last = grid_plane(app.cam.to_frame(app.cam.back()), *last);
last.axes()
})
} else {
(Vec3::X, Vec3::Y)
};
let snap = |a: Vec3| a * ((tf.dot(a) / major).round() * major);
(snap(u) + snap(v), u, v, Box::new(|p| app.cam.from_frame(p).to_f32()))
};
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();
let n = ((ext / minor) as i64).clamp(1, 300);
for i in -n..=n {
let t = i as f64 * minor;
let maj = (t / major).round() * major == t;
let col = if maj { c4(tk.grid_major) } else { c4(tk.grid) };
let w = if maj { 1.0 } else { 0.7 };
sc.under_line(w3(c + u * t - v * ext), w3(c + u * t + v * ext), col, w);
sc.under_line(w3(c - u * ext + v * t), w3(c + u * ext + v * t), col, w);
let wd = if maj { 1.0 } else { 0.7 };
sc.under_line(w3(c + u * t - v * ext), w3(c + u * t + v * ext), col, wd);
sc.under_line(w3(c - u * ext + v * t), w3(c + u * ext + v * t), col, wd);
}
}
for sid in visible_sketches(app) {
@@ -6188,6 +6194,28 @@ mod grid_plane_tests {
assert_eq!(*app.viewport.grid_plane.lock().unwrap(), GridPlane::Ground, "a clear turn off the ground returns");
}
/// An open sketch always has its grid, on its own plane, whatever the Grid switch and the
/// follow-the-view preference say; outside a sketch the switch decides.
#[test]
fn an_open_sketch_always_shows_a_grid_on_its_plane() {
let rect = Rect::from_min_size(Pos2::ZERO, egui::vec2(800.0, 600.0));
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
app.ui.show_grid = false;
let grid = |app: &SolveApp| build_sketch_scene(app, &projection(app, rect)).under;
assert!(grid(&app).is_empty(), "no grid outside a sketch with the switch off");
app.run("sketch.create", json!({"plane": "XZ"})).unwrap();
for follows in [false, true] {
app.preferences.grid_follows_view = follows;
let under = grid(&app);
assert!(!under.is_empty(), "the open sketch shows a grid (follows the view: {follows})");
// Each line is 32 bytes: two f32 points, a colour and a width; the XZ plane is Y = 0.
let ys: Vec<f32> = under.chunks_exact(32).flat_map(|l| [&l[4..8], &l[16..20]]).map(|b| f32::from_le_bytes(b.try_into().unwrap())).collect();
assert!(ys.iter().all(|y| y.abs() < 1e-3), "the grid lies on the sketch plane");
}
app.ui.show_grid = true;
assert!(!grid(&app).is_empty());
}
/// A sketch is drawn in a parallel view even when the preference asks for perspective; the
/// preference comes back when the sketch is finished.
#[test]