From bbd0a192601e9e40aa370064eb1c064b3c1338c4 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 8 Oct 2026 07:03:08 +0000 Subject: [PATCH] Sketch Palette, docked at the right while sketching: line type of the selected curves (normal, construction, centerline), Look At, and options kept with each sketch (sketch.options: Sketch Grid, Snap, Slice, Show Profile, Show Points, Show Dimensions, Show Constraints, Show Projected Geometries, 3D Sketch), the degrees of freedom, Finish Sketch. Slice cuts the model at the sketch plane; Snap off places points at the cursor Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- crates/engine/src/cmd/sketch.rs | 52 ++++++ crates/engine/src/cmd/sketch_ux_tests.rs | 33 ++++ crates/engine/src/view.rs | 5 +- crates/sketch/src/lib.rs | 2 +- crates/sketch/src/model.rs | 36 ++++ crates/ui-egui/src/dim_view.rs | 4 + crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/sketch_palette.rs | 218 +++++++++++++++++++++++ crates/ui-egui/src/sketch_tools.rs | 4 + crates/ui-egui/src/viewport.rs | 33 +++- 10 files changed, 382 insertions(+), 7 deletions(-) create mode 100644 crates/ui-egui/src/sketch_palette.rs diff --git a/crates/engine/src/cmd/sketch.rs b/crates/engine/src/cmd/sketch.rs index 0d78ac1a..424e51f6 100644 --- a/crates/engine/src/cmd/sketch.rs +++ b/crates/engine/src/cmd/sketch.rs @@ -157,6 +157,9 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("sketch.dimension_text", "Move Dimension Text", dimension_text) .enabled(in_sketch) .params("dimension: constraint id or parameter name; at: [x,y] text centre (sketch coordinates) | reset: true (default place)"), + CommandSpec::new("sketch.options", "Sketch Palette Options", sketch_options).icon("sketch").params( + "sketch?: id|name (default active); any of show_profile, show_points, show_dimensions, show_constraints, show_projected, grid, snap, slice, sketch_3d: bool. Returns them all", + ), CommandSpec::new("sketch.solve", "Solve Sketch", solve_cmd) .enabled(in_sketch) .noundo() @@ -1514,6 +1517,55 @@ fn move_point(s: &mut Session, p: &Value) -> Result { Ok(json!({"sketch": info})) } +/// The Sketch Palette options of a sketch, by their palette names (true = shown / on). +fn options_json(v: &solvecraft_sketch::SketchView) -> Value { + json!({ + "show_profile": !v.hide_profile, + "show_points": !v.hide_points, + "show_dimensions": !v.hide_dimensions, + "show_constraints": !v.hide_constraints, + "show_projected": !v.hide_projected, + "grid": !v.hide_grid, + "snap": !v.no_snap, + "slice": v.slice, + "sketch_3d": v.three_d, + }) +} + +fn sketch_options(s: &mut Session, p: &Value) -> Result { + let cmd = "sketch.options"; + let keys = ["show_profile", "show_points", "show_dimensions", "show_constraints", "show_projected", "grid", "snap", "slice", "sketch_3d"]; + if let Some(o) = p.as_object() + && let Some(k) = o.keys().find(|k| *k != "sketch" && !keys.contains(&k.as_str())) + { + return Err(bad(cmd, format!("unknown option `{k}`"))); + } + let set = |k: &str| bool_(p, k); + if keys.iter().all(|k| set(k).is_none()) { + let id = target_sketch(s, p, cmd)?; + return Ok(json!({"options": options_json(&s.doc.sketch(id)?.view)})); + } + let (v, info) = edit(s, p, cmd, false, |sk, _| { + let v = &mut sk.view; + let put = |k: &str, flag: &mut bool, shown_means_off: bool| { + if let Some(b) = set(k) { + *flag = if shown_means_off { !b } else { b }; + } + }; + put("show_profile", &mut v.hide_profile, true); + put("show_points", &mut v.hide_points, true); + put("show_dimensions", &mut v.hide_dimensions, true); + put("show_constraints", &mut v.hide_constraints, true); + put("show_projected", &mut v.hide_projected, true); + put("grid", &mut v.hide_grid, true); + put("snap", &mut v.no_snap, true); + put("slice", &mut v.slice, false); + put("sketch_3d", &mut v.three_d, false); + Ok(options_json(v)) + })?; + Ok(json!({"options": v, "sketch": info})) +} + fn solve_cmd(s: &mut Session, p: &Value) -> Result { let id = target_sketch(s, p, "sketch.solve")?; let st = s.model.state(); diff --git a/crates/engine/src/cmd/sketch_ux_tests.rs b/crates/engine/src/cmd/sketch_ux_tests.rs index a052c44b..bbf5e886 100644 --- a/crates/engine/src/cmd/sketch_ux_tests.rs +++ b/crates/engine/src/cmd/sketch_ux_tests.rs @@ -400,3 +400,36 @@ fn point_constraints_keep_the_first_pick() { assert!(same(&snap(&s, "p1"), &a0) && same(&snap(&s, "l1"), &l0)); assert!(pt(&s, "p2").dist(solvecraft_geom::Vec2::new(3.0, -7.0)) < 1e-6, "{:?}", pt(&s, "p2")); } + +// --------------------------------------------------------------------------------------------- +// Sketch Palette options + +#[test] +fn palette_options_are_kept_with_each_sketch() { + let mut s = new_sketch(); + let o = run(&mut s, "sketch.options", json!({})); + assert_eq!(o["options"]["show_dimensions"], true); + assert_eq!(o["options"]["slice"], false); + let o = run(&mut s, "sketch.options", json!({"show_dimensions": false, "snap": false, "slice": true, "sketch_3d": true})); + assert_eq!(o["options"]["show_dimensions"], false); + assert_eq!(o["options"]["snap"], false); + assert_eq!(o["options"]["slice"], true); + let v = sketch(&s).view; + assert!(v.hide_dimensions && v.no_snap && v.slice && v.three_d && !v.hide_points); + // Another sketch starts with the defaults; the first keeps its options after finishing. + let first = s.active_sketch.expect("sketch"); + run(&mut s, "SketchStop", json!({})); + run(&mut s, "SketchCreate", json!({"plane": "XZ"})); + assert_eq!(sketch(&s).view, solvecraft_sketch::SketchView::default()); + let o = run(&mut s, "sketch.options", json!({"sketch": first})); + assert_eq!(o["options"]["show_dimensions"], false); + // Saved with the design. + let text = serde_json::to_string(&s.doc.sketch(first).expect("first").clone()).expect("json"); + let back: Sketch = serde_json::from_str(&text).expect("parse"); + assert!(back.view.slice); + assert!(s.execute("sketch.options", &json!({"bogus": true})).is_err()); + // Undo restores the option. + run(&mut s, "sketch.options", json!({"show_points": false})); + run(&mut s, "UndoCommand", json!({})); + assert!(!sketch(&s).view.hide_points); +} diff --git a/crates/engine/src/view.rs b/crates/engine/src/view.rs index a0b19c14..124a8f50 100644 --- a/crates/engine/src/view.rs +++ b/crates/engine/src/view.rs @@ -72,6 +72,9 @@ pub fn grid_step(half_height: f64) -> (f64, f64) { pub fn sketch_lines(sk: &Sketch, plane: &Plane, active: bool, determined: &[bool]) -> Vec<(Vec, Rgb, bool)> { let mut out = Vec::new(); for (i, c) in sk.curves.iter().enumerate() { + if c.link.is_some() && sk.view.hide_projected && !sk.is_text_curve(i) { + continue; + } let col = if c.link.is_some() && !sk.is_text_curve(i) { if sk.is_lost_curve(i) { colors::SKETCH_LOST } else { colors::SKETCH_PROJECTED } } else if c.fixed { @@ -92,7 +95,7 @@ pub fn sketch_lines(sk: &Sketch, plane: &Plane, active: bool, determined: &[bool } } // 3D curves are in world coordinates already. - for w in &sk.wires { + for w in sk.wires.iter().filter(|_| !sk.view.hide_projected) { out.push((w.pts.clone(), colors::SKETCH_PROJECTED, false)); } out diff --git a/crates/sketch/src/lib.rs b/crates/sketch/src/lib.rs index a2a3d8d0..02beb522 100644 --- a/crates/sketch/src/lib.rs +++ b/crates/sketch/src/lib.rs @@ -24,7 +24,7 @@ pub use curves::{ }; pub use dims::{DimFrame, DimLayout, chain_centres, default_text, dim_frame, dim_layout, encode_text}; pub use link::{Link, LinkGeom, LinkKind, LinkSource, MAX_LINK_ENTITIES}; -pub use model::{Constraint, ConstraintKind, Curve, CurveKind, SPoint, Sketch, SketchError}; +pub use model::{Constraint, ConstraintKind, Curve, CurveKind, SPoint, Sketch, SketchError, SketchView}; pub use profiles::{Profile, find_drawn_profiles, find_profiles, merge_regions}; pub use shape::{Shape, intersections}; pub use solver::{Hold, SolveReport, SolveStatus, solve, solve_holding}; diff --git a/crates/sketch/src/model.rs b/crates/sketch/src/model.rs index 13d6e884..f20d8301 100644 --- a/crates/sketch/src/model.rs +++ b/crates/sketch/src/model.rs @@ -430,6 +430,34 @@ pub struct Constraint { pub text: Option, } +/// How a sketch is shown and drawn in (the Sketch Palette options kept with the sketch). Every +/// flag is off by default, so a sketch shows everything, with grid and snap. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct SketchView { + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub hide_profile: bool, + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub hide_points: bool, + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub hide_dimensions: bool, + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub hide_constraints: bool, + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub hide_projected: bool, + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub hide_grid: bool, + /// Clicks don't snap to the grid. + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub no_snap: bool, + /// Cut away the model in front of the sketch plane while editing. + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub slice: bool, + /// 3D sketch: drawing may leave the sketch plane. + #[serde(skip_serializing_if = "std::ops::Not::not")] + pub three_d: bool, +} + /// A 2D sketch. Point 0 is always the fixed sketch origin (id `origin`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Sketch { @@ -445,6 +473,13 @@ pub struct Sketch { /// Next number per id prefix (`l` → l1, l2…). #[serde(default)] counters: std::collections::BTreeMap, + /// Display options (Sketch Palette). + #[serde(default, skip_serializing_if = "is_default_view")] + pub view: SketchView, +} + +fn is_default_view(v: &SketchView) -> bool { + *v == SketchView::default() } impl Default for Sketch { @@ -462,6 +497,7 @@ impl Sketch { links: Vec::new(), wires: Vec::new(), counters: Default::default(), + view: SketchView::default(), } } diff --git a/crates/ui-egui/src/dim_view.rs b/crates/ui-egui/src/dim_view.rs index 468514b7..ef83cc5f 100644 --- a/crates/ui-egui/src/dim_view.rs +++ b/crates/ui-egui/src/dim_view.rs @@ -186,6 +186,10 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P return; }; let sk = &ss.sketch; + if sk.view.hide_dimensions { + DRAWN.with(|d| d.borrow_mut().clear()); + return; + } let tk = Tokens::get(); let sel = selected(); // A selected dimension that was deleted (undo, another command) is forgotten. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index dacd2c70..a44a6481 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -32,6 +32,7 @@ pub mod selection; pub mod sketch_dims; #[cfg(test)] mod sketch_edit_tests; +pub mod sketch_palette; pub mod sketch_tools; pub mod theme; pub mod timeline; @@ -609,6 +610,7 @@ impl SolveApp { viewport::show(self, ui); }); dialogs::show(self, ui.ctx()); + sketch_palette::show(self, ui.ctx()); params_dialog::show(self, ui.ctx()); context_menu::show(self, ui.ctx()); delete::show(self, ui.ctx()); diff --git a/crates/ui-egui/src/sketch_palette.rs b/crates/ui-egui/src/sketch_palette.rs new file mode 100644 index 00000000..9622e242 --- /dev/null +++ b/crates/ui-egui/src/sketch_palette.rs @@ -0,0 +1,218 @@ +//! The Sketch Palette: a panel docked at the right while a sketch is being edited, like a +//! command dialog. Options: the line type of the selected curves (normal, construction, +//! centerline), Look At, and the display toggles kept with the sketch (grid, snap, slice, +//! profiles, points, dimensions, constraints, projected geometry, 3D sketch); the sketch's +//! degrees of freedom; Finish Sketch. Toggles run `sketch.options`, line types +//! `sketch.construction` / `sketch.centerline`. + +use std::cell::Cell; + +use egui::{Color32, RichText, Stroke, vec2}; +use serde_json::{Value, json}; +use solvecraft_engine::Sel; + +use crate::SolveApp; +use crate::theme::Tokens; + +thread_local! { + static FOLDED: Cell = const { Cell::new(false) }; + static OPTIONS_FOLDED: Cell = const { Cell::new(false) }; +} + +/// Palette width (logical px). +const WIDTH: f32 = 236.0; + +/// The display toggles: option key and label. +pub const TOGGLES: [(&str, &str); 9] = [ + ("grid", "Sketch Grid"), + ("snap", "Snap"), + ("slice", "Slice"), + ("show_profile", "Show Profile"), + ("show_points", "Show Points"), + ("show_dimensions", "Show Dimensions"), + ("show_constraints", "Show Constraints"), + ("show_projected", "Show Projected Geometries"), + ("sketch_3d", "3D Sketch"), +]; + +/// The degrees-of-freedom line: "Fully constrained" or "N degrees of freedom". +pub fn dof_text(dof: usize, ok: bool) -> String { + if !ok { + "Constraints conflict".into() + } else if dof == 0 { + "Fully constrained".into() + } else if dof == 1 { + "1 degree of freedom".into() + } else { + format!("{dof} degrees of freedom") + } +} + +/// Selected curves of the active sketch. +fn selected_curves(app: &SolveApp) -> Vec { + app.session.selection.iter().filter_map(|s| if let Sel::SketchCurve { id } = s { Some(id.clone()) } else { None }).collect() +} + +/// Set the line type of the selected curves: 0 normal, 1 construction, 2 centerline (lines). +pub fn set_linetype(app: &mut SolveApp, kind: u8) { + let curves = selected_curves(app); + if curves.is_empty() { + app.set_status("Select sketch curves first", false); + return; + } + let lines: Vec = { + let st = app.session.model.state(); + let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return }; + curves + .iter() + .filter(|id| { + ss.sketch + .curve_index(id) + .and_then(|i| ss.sketch.curves.get(i)) + .is_some_and(|c| matches!(c.kind, solvecraft_engine::sketch::CurveKind::Line { .. })) + }) + .cloned() + .collect() + }; + let r = match kind { + 0 => { + if !lines.is_empty() { + let _ = app.run("sketch.centerline", json!({ "curves": lines, "value": false })); + } + app.run("sketch.construction", json!({ "curves": curves, "value": false })) + } + 1 => app.run("sketch.construction", json!({ "curves": curves, "value": true })), + _ if lines.is_empty() => Err("a centerline must be a line".to_string()), + _ => app.run("sketch.centerline", json!({ "curves": lines, "value": true })), + }; + if let Err(e) = r { + app.set_status(e, true); + } +} + +/// Draw the palette (while sketching and no command dialog is open). +pub fn show(app: &mut SolveApp, ctx: &egui::Context) { + if app.dialog.is_some() { + return; + } + let (opts, dof_line, ok) = { + let st = app.session.model.state(); + let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return }; + (ss.sketch.view.clone(), dof_text(ss.report.dof, ss.report.ok()), ss.report.ok() && ss.report.dof == 0) + }; + let t = Tokens::get(); + let vp = app.viewport.rect.unwrap_or_else(|| ctx.content_rect()); + let anchor = egui::pos2(vp.right(), vp.top() + crate::viewport::VIEW_CUBE_CLEARANCE); + let frame = egui::Frame::window(&ctx.global_style()) + .fill(t.dialog_bg) + .stroke(Stroke::new(1.0, t.dialog_border)) + .corner_radius(egui::CornerRadius { nw: 4, sw: 4, ne: 0, se: 0 }) + .shadow(egui::Shadow { offset: [-2, 2], blur: 8, spread: 0, color: Color32::from_black_alpha(40) }); + let shown = |k: &str| match k { + "grid" => !opts.hide_grid, + "snap" => !opts.no_snap, + "slice" => opts.slice, + "show_profile" => !opts.hide_profile, + "show_points" => !opts.hide_points, + "show_dimensions" => !opts.hide_dimensions, + "show_constraints" => !opts.hide_constraints, + "show_projected" => !opts.hide_projected, + "sketch_3d" => opts.three_d, + _ => false, + }; + let mut set: Option<(String, bool)> = None; + let mut linetype: Option = None; + let mut look = false; + let mut finish = false; + let has_curves = !selected_curves(app).is_empty(); + egui::Window::new("SKETCH PALETTE") + .id(egui::Id::new("sc_sketch_palette")) + .title_bar(false) + .frame(frame) + .pivot(egui::Align2::RIGHT_TOP) + .fixed_pos(anchor) + .constrain(false) + .auto_sized() + .collapsible(false) + .show(ctx, |ui| { + ui.set_width(WIDTH); + ui.horizontal(|ui| { + let folded = FOLDED.with(Cell::get); + if ui.add(egui::Button::new(RichText::new(if folded { "+" } else { "−" }).size(14.0)).frame(false)).clicked() { + FOLDED.with(|f| f.set(!folded)); + } + ui.label(RichText::new("SKETCH PALETTE").strong().size(12.5)); + }); + if FOLDED.with(Cell::get) { + return; + } + let ofold = OPTIONS_FOLDED.with(Cell::get); + if ui.add(egui::Button::new(RichText::new(format!("{} Options", if ofold { "+" } else { "−" })).size(12.0)).frame(false)).clicked() { + OPTIONS_FOLDED.with(|f| f.set(!ofold)); + } + if !ofold { + egui::Grid::new("sc_palette_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| { + ui.label("Linetype"); + ui.horizontal(|ui| { + for (i, (label, tip)) in [("—", "Normal"), ("- -", "Construction"), ("-·-", "Centerline")].iter().enumerate() { + if ui.add_enabled(has_curves, egui::Button::new(*label).min_size(vec2(30.0, 20.0))).on_hover_text(*tip).clicked() { + linetype = Some(i as u8); + } + } + }); + ui.end_row(); + ui.label("Look At"); + if ui.button("Look At").clicked() { + look = true; + } + ui.end_row(); + for (k, label) in TOGGLES { + ui.label(label); + let mut on = shown(k); + if ui.checkbox(&mut on, "").changed() { + set = Some((k.to_string(), on)); + } + ui.end_row(); + } + }); + } + ui.add_space(4.0); + ui.label(RichText::new(dof_line.as_str()).color(if ok { t.sketch_accent } else { t.text_dim })); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.add_space(WIDTH - 100.0); + if ui.add(egui::Button::new(RichText::new("Finish Sketch").color(Color32::WHITE)).fill(t.accent)).clicked() { + finish = true; + } + }); + }); + if let Some((k, on)) = set { + let mut p = serde_json::Map::new(); + p.insert(k, Value::Bool(on)); + if let Err(e) = app.run("sketch.options", Value::Object(p)) { + app.set_status(e, true); + } + } + if let Some(k) = linetype { + set_linetype(app, k); + } + if look { + crate::dialogs::look_at_sketch(app); + } + if finish { + app.finish_sketch(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dof_readout() { + assert_eq!(dof_text(0, true), "Fully constrained"); + assert_eq!(dof_text(1, true), "1 degree of freedom"); + assert_eq!(dof_text(4, true), "4 degrees of freedom"); + assert_eq!(dof_text(4, false), "Constraints conflict"); + } +} diff --git a/crates/ui-egui/src/sketch_tools.rs b/crates/ui-egui/src/sketch_tools.rs index 82b662d7..b9fb0d47 100644 --- a/crates/ui-egui/src/sketch_tools.rs +++ b/crates/ui-egui/src/sketch_tools.rs @@ -710,6 +710,10 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { let Some(sid) = active_sketch(app) else { return }; let st = app.session.model.state(); let Some(ss) = st.sketch(sid) else { return }; + if ss.sketch.view.hide_constraints { + GLYPHS.with(|g| g.borrow_mut().clear()); + return; + } let tk = crate::theme::Tokens::get(); let mut placed: Vec = Vec::new(); let picked = PICKED.with(|p| p.borrow().clone()); diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 87490415..067d51b5 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -195,7 +195,7 @@ fn build_scene(app: &SolveApp) -> GpuScene { let s = &app.session; // Bodies where their occurrences place them. let st = s.world_state(); - if app.ui.show_grid { + if 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; let c = Vec3::new((app.cam.target.x / major).round() * major, (app.cam.target.y / major).round() * major, 0.0); @@ -250,7 +250,7 @@ fn build_scene(app: &SolveApp) -> GpuScene { let active = s.active_sketch == Some(sid); // Closed profiles are shaded (lifted off a face they may lie on). let lift = ss.plane.normal() * 0.01; - let profiles = if app.ui.hidden_profiles.contains(&sid) { &[][..] } else { &ss.profiles[..] }; + let profiles = if app.ui.hidden_profiles.contains(&sid) || ss.sketch.view.hide_profile { &[][..] } else { &ss.profiles[..] }; for p in profiles { for tri in p.region.triangulate(0.05) { for q in tri { @@ -469,6 +469,9 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec { let Some(ss) = st.sketch(sid) else { continue }; let bonus = if s.active_sketch == Some(sid) { 2.0 } else { 0.0 }; for (i, c) in ss.sketch.curves.iter().enumerate() { + if c.link.is_some() && ss.sketch.view.hide_projected { + continue; + } let straight = matches!(c.kind, CurveKind::Line { .. }); for seg in ss.sketch.segs(i) { let pts: Vec = seg.polyline(0.05).iter().filter_map(|q| proj.to_screen(ss.plane.to_world(*q))).collect(); @@ -578,7 +581,9 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec { continue; } let lp = ss.plane.to_local(o + d * t); - for (pi, p) in ss.profiles.iter().enumerate() { + // Show Profile off: the sketch being edited offers no profiles. + let profiles = if ss.sketch.view.hide_profile && s.active_sketch == Some(sid) { &[][..] } else { &ss.profiles[..] }; + for (pi, p) in profiles.iter().enumerate() { if p.region.contains(lp) && bestp.as_ref().is_none_or(|(_, a, _)| p.area < *a) { bestp = Some((t, p.area, Hit::Profile { sketch: sid, index: pi })); } @@ -606,6 +611,12 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec { } /// The section cut shown now: the open Section Analysis dialog's plane, else the session's. +/// The active sketch's display options (defaults when not sketching). +pub fn active_view(app: &SolveApp) -> solvecraft_engine::sketch::SketchView { + let st = app.session.model.state(); + app.session.active_sketch.and_then(|s| st.sketch(s)).map(|ss| ss.sketch.view.clone()).unwrap_or_default() +} + pub fn section_plane(app: &SolveApp) -> Option<(Vec3, Vec3)> { if let Some(d) = app.dialog.as_ref().filter(|d| matches!(d.kind, crate::dialogs::Kind::Section { .. })) { let cmds = crate::dialogs::apply_commands(app, d).ok()?; @@ -614,6 +625,13 @@ pub fn section_plane(app: &SolveApp) -> Option<(Vec3, Vec3)> { scratch.execute("FusionHalfSectionViewCommand", &p).ok()?; return scratch.section; } + // Slice (Sketch Palette): cut the model at the plane of the sketch being edited. + if app.session.section.is_none() { + let st = app.session.model.state(); + if let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)).filter(|ss| ss.sketch.view.slice) { + return Some((ss.plane.origin, ss.plane.normal())); + } + } app.session.section } @@ -958,6 +976,10 @@ pub fn sketch_point_at(app: &SolveApp, proj: &Proj, pos: Pos2) -> Option<(Vec2, if let Some(p) = crate::sketch_tools::refine_snap(app, proj, &ss.sketch, &ss.plane, lp) { return Some(p); } + // With Snap off (Sketch Palette) the point is where the cursor is. + if ss.sketch.view.no_snap { + return Some((lp, None)); + } Some((Vec2::new((lp.x / step).round() * step, (lp.y / step).round() * step), None)) } @@ -1401,7 +1423,8 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { if !solvecraft_engine::view::sketch_point_visible(sk, i) { continue; } - let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)) else { continue }; + let hidden = (sk.view.hide_points && i != 0) || (sk.view.hide_projected && p.link.is_some()); + let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)).filter(|_| !hidden) else { continue }; let det = ss.report.point_determined.get(i).copied().unwrap_or(false); let c = if p.link.is_none() && locked.get(i).copied().unwrap_or(false) { t.sketch_locked @@ -1416,7 +1439,7 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { painter.rect_filled(Rect::from_center_size(sp, vec2(5.0, 5.0)), 0.0, c); } } - for c in sk.constraints.iter().filter(|c| !c.kind.is_dimension()) { + for c in sk.constraints.iter().filter(|c| !c.kind.is_dimension() && !sk.view.hide_constraints) { glyph(painter, proj, ss, &c.kind); } // Sketch status.