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.