From 13b72bb011290afd264552be2cd721c965668ca3 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 8 Oct 2026 02:58:43 +0000 Subject: [PATCH] Sketch drawing: clicking a model vertex projects it into the sketch (linked point) and draws from it, as Fusion does ("vertex:x,y,z" point arguments) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- crates/engine/src/cmd/sketch_project.rs | 12 +++++++++++- crates/engine/src/cmd/sketch_project_tests.rs | 2 +- crates/ui-egui/src/sketch_tools.rs | 7 +++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/sketch_project.rs b/crates/engine/src/cmd/sketch_project.rs index b1a66513..bbc09a21 100644 --- a/crates/engine/src/cmd/sketch_project.rs +++ b/crates/engine/src/cmd/sketch_project.rs @@ -329,13 +329,23 @@ fn auto_project(s: &mut Session, p: &Value) -> Result { /// Point arguments that snap to model geometry while drawing, rewritten to sketch point ids: /// `{"vertex": [x,y,z]}` projects the vertex (a linked point); `{"on_edge": [x,y,z]}` projects /// the edge and makes a free point on it (coincident with the projection); `"mid:"` -/// makes a point held at the line's midpoint. Everything else is left as it is. The links are added to the active sketch in the session's document. +/// makes a point held at the line's midpoint (and `"vertex:x,y,z"` is the vertex form). Everything +/// else is left as it is. The links are added to the active sketch in the session's document. pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result { let Some(id) = s.active_sketch else { return Ok(p.clone()) }; let mut out = p.clone(); let mut doc: Option<(Document, Sketch)> = None; let fix = |v: &mut Value, s: &Session, doc: &mut Option<(Document, Sketch)>| -> Result<()> { let (vertex, edge) = (v.get("vertex").and_then(vec3), v.get("on_edge").and_then(vec3)); + // String forms from interactive snapping: "vertex:x,y,z" and "mid:". + let vertex = vertex.or_else(|| { + let t = v.as_str()?.strip_prefix("vertex:")?; + let n: Vec = t.split(',').filter_map(|x| x.trim().parse().ok()).collect(); + match n[..] { + [x, y, z] if x.is_finite() && y.is_finite() && z.is_finite() => Some(Vec3::new(x, y, z)), + _ => None, + } + }); let mid = v.as_str().and_then(|x| x.strip_prefix("mid:")).map(str::to_string); if vertex.is_none() && edge.is_none() && mid.is_none() { return Ok(()); diff --git a/crates/engine/src/cmd/sketch_project_tests.rs b/crates/engine/src/cmd/sketch_project_tests.rs index 11201f52..8bccf8ef 100644 --- a/crates/engine/src/cmd/sketch_project_tests.rs +++ b/crates/engine/src/cmd/sketch_project_tests.rs @@ -181,7 +181,7 @@ fn drawing_on_a_model_vertex_projects_it() { let mut s = Session::default(); box_part(&mut s); run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "DrawPolyline", json!({"points": [{"vertex": [40, 30, 20]}, [60, 45]]})); + let r = run(&mut s, "DrawPolyline", json!({"points": ["vertex:40,30,20", [60, 45]]})); let l = ids(&r["curves"])[0].clone(); let si = inspect(&mut s); let start = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == l).unwrap()["start"].clone(); diff --git a/crates/ui-egui/src/sketch_tools.rs b/crates/ui-egui/src/sketch_tools.rs index 2bb53a10..c80d01a9 100644 --- a/crates/ui-egui/src/sketch_tools.rs +++ b/crates/ui-egui/src/sketch_tools.rs @@ -40,6 +40,13 @@ pub fn refine_snap( let (a, b) = (proj.to_screen(plane.to_world(lp))?, proj.to_screen(plane.to_world(lp + Vec2::X))?); let px = 1.0 / (a.distance(b) as f64).max(1e-9); let tol = 7.0 * px; + // A model vertex under the cursor: drawing on it projects it into the sketch. + for h in pick(app, proj, a) { + if let Hit::Vertex { point, .. } = h { + SNAP.with(|s| *s.borrow_mut() = Some(("Pt", plane.to_local(point)))); + return Some((plane.to_local(point), Some(format!("vertex:{},{},{}", point.x, point.y, point.z)))); + } + } let mut best: Option<(f64, &'static str, Vec2)> = None; let mut mid_of: Option = None; for (i, c) in sk.curves.iter().enumerate() {