diff --git a/crates/engine/src/cmd/sketch_constraints_tests.rs b/crates/engine/src/cmd/sketch_constraints_tests.rs index 8ebf5714..ebfbde03 100644 --- a/crates/engine/src/cmd/sketch_constraints_tests.rs +++ b/crates/engine/src/cmd/sketch_constraints_tests.rs @@ -187,3 +187,16 @@ fn drawing_from_a_line_midpoint_keeps_it_there() { let b = sk.resolve_point(&format!("{l}.end")).and_then(|i| sk.point(i)).unwrap(); assert!(st.dist((a + b) * 0.5) < 1e-7, "{st:?}"); } + +#[test] +fn drawing_onto_a_curve_keeps_the_point_on_it() { + let mut s = new_sketch(); + let c = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone(); + let r = run(&mut s, "DrawPolyline", json!({"points": [format!("on:{c}:7.2,7.1"), [30, 30]]})); + let n = ids(&r["curves"])[0].clone(); + run(&mut s, "SketchDimension", json!({"entities": [c], "value": 16})); + let sk = sketch(&s); + let st = sk.resolve_point(&format!("{n}.start")).and_then(|i| sk.point(i)).unwrap(); + let cc = sk.center(sk.curve_index(&c).unwrap()).unwrap(); + assert!((st.dist(cc) - 8.0).abs() < 1e-7, "{st:?} {cc:?}"); +} diff --git a/crates/engine/src/cmd/sketch_project.rs b/crates/engine/src/cmd/sketch_project.rs index bbc09a21..73b11d89 100644 --- a/crates/engine/src/cmd/sketch_project.rs +++ b/crates/engine/src/cmd/sketch_project.rs @@ -347,7 +347,16 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result:x,y": a point on a curve of the sketch near x,y. + let on = v.as_str().and_then(|x| x.strip_prefix("on:")).and_then(|t| { + let (c, xy) = t.rsplit_once(':')?; + let n: Vec = xy.split(',').filter_map(|x| x.trim().parse().ok()).collect(); + match n[..] { + [x, y] if x.is_finite() && y.is_finite() => Some((c.to_string(), solvecraft_geom::Vec2::new(x, y))), + _ => None, + } + }); + if vertex.is_none() && edge.is_none() && mid.is_none() && on.is_none() { return Ok(()); } if doc.is_none() { @@ -358,7 +367,16 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result sh.project(near), + None => near, + }; + let pi = sk.add_point(at, None)?; + sk.add_constraint(ConstraintKind::PointOnCurve { p: pi, c }, None)?; + sk.points.get(pi).map(|q| q.id.clone()).unwrap_or_default() + } else if let Some(line) = mid { // "mid:": a new point at the line's midpoint, kept there. let l = sk.curve_index(&line).ok_or_else(|| bad(cmd, format!("unknown curve `{line}`")))?; let Some(solvecraft_sketch::Shape::Line { a, b }) = sk.shape(l) else { return Err(bad(cmd, format!("`{line}` is not a line"))) }; diff --git a/crates/ui-egui/src/sketch_tools.rs b/crates/ui-egui/src/sketch_tools.rs index c80d01a9..2c8e615c 100644 --- a/crates/ui-egui/src/sketch_tools.rs +++ b/crates/ui-egui/src/sketch_tools.rs @@ -71,6 +71,18 @@ pub fn refine_snap( } None => PREV.with(Cell::get), }; + // On a curve (coincident with it). + if best.is_none() { + for (i, c) in sk.curves.iter().enumerate() { + let Some(sh) = sk.shape(i) else { continue }; + let d = sh.dist(lp); + if d < tol * 0.8 && best.is_none_or(|x| d < x.0) { + let q = sh.project(lp); + best = Some((d, "On", q)); + mid_of = Some(format!("{}:{},{}", c.id, q.x, q.y)); + } + } + } if best.is_none() && let Some(prev) = prev { @@ -86,7 +98,14 @@ pub fn refine_snap( } SNAP.with(|s| *s.borrow_mut() = best.map(|(_, l, p)| (l, p))); // A midpoint snap is a point argument the commands understand ("mid:"). - best.map(|(_, l, p)| (p, if l == "Mid" { mid_of.map(|m| format!("mid:{m}")) } else { None })) + best.map(|(_, l, p)| { + let arg = match l { + "Mid" => mid_of.map(|m| format!("mid:{m}")), + "On" => mid_of.map(|m| format!("on:{m}")), + _ => None, + }; + (p, arg) + }) } /// A click on a constraint glyph selects it (true: the click was used).