Sketch: the cursor snaps On a spline, ellipse or conic while drawing, as on a line or circle (#9321) (#9343)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
semmelrinde
2026-10-30 11:10:25 +00:00
committed by GitHub
parent 66bd64cc5e
commit 491687d4c1
2 changed files with 73 additions and 4 deletions
+50
View File
@@ -361,6 +361,56 @@ fn place_from_the_origin_fully_constrains_new_shapes() {
assert_eq!(draw(false, true), (0, 4), "placing needs Dimension new shapes");
}
/// A line ended on a spline, between its fit points (#9321): the cursor snaps On the spline as it
/// does on a line or a circle, and the click ties the end to the spline.
#[test]
fn a_line_end_on_a_spline_snaps_on_it_and_stays_on_it() {
use solidcraft_engine::sketch::{ConstraintKind, CurveKind};
let mut h = Harness::new();
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
h.app.run("sketch.spline.fit_point", json!({"points": [[-30, 0], [-10, 18], [10, 6], [30, 20]]})).unwrap();
h.settle();
let sid = h.app.session.active_sketch.unwrap();
let (spline, on) = {
let st = h.app.session.world_state();
let sk = &st.sketch(sid).unwrap().sketch;
let ci = sk.curves.iter().position(|c| matches!(c.kind, CurveKind::Spline { .. })).unwrap();
let poly = sk.polyline(ci);
// Partway along the spline, away from its fit points.
let w = poly.len() * 3 / 8;
(ci, (poly[w] + poly[w + 1]) * 0.5)
};
h.step(&json!({"start": "sketch.line"})).unwrap();
h.step(&json!({"click": {"world": [0, -25, 0]}})).unwrap();
h.step(&json!({"move": {"world": [on.x, on.y, 0]}})).unwrap();
let hover = h.app.tool.as_ref().unwrap().hover.clone().unwrap();
assert!(hover.1.as_deref().is_some_and(|s| s.starts_with("on:")), "{hover:?}");
h.step(&json!({"click": {"world": [on.x, on.y, 0]}})).unwrap();
let st = h.app.session.world_state();
let sk = &st.sketch(sid).unwrap().sketch;
let b = sk
.curves
.iter()
.find_map(|c| if let CurveKind::Line { b, .. } = c.kind { Some(b) } else { None })
.unwrap_or_else(|| panic!("no line, {:?}", h.app.status));
assert!(
sk.constraints.iter().any(|c| matches!(c.kind, ConstraintKind::PointOnCurve { p, c } if p == b && c == spline)),
"the end is not tied to the spline: {:?}",
sk.constraints
);
let end = sk.point(b).unwrap();
let to_segment = |a: solidcraft_engine::geom::Vec2, b: solidcraft_engine::geom::Vec2| {
let ab = b - a;
let t = ((end - a).dot(ab) / ab.len2().max(1e-300)).clamp(0.0, 1.0);
(a + ab * t).dist(end)
};
// The constraint puts the end on the spline itself; the polyline it is drawn with lies within
// a few µm of it between its samples.
let off = sk.polyline(spline).windows(2).map(|w| to_segment(w[0], w[1])).fold(f64::MAX, f64::min);
assert!(off < 0.02, "the end is {off} mm off the spline at {end:?}");
assert!(end.dist(on) < 0.5, "the end moved from the cursor {on:?} to {end:?}");
}
/// A line from a circle's centre to the circle, ending a little off the quadrant: the preview
/// snaps to horizontal or vertical, so the click must place exactly that line, on the circle.
#[test]
+23 -4
View File
@@ -471,6 +471,18 @@ pub(crate) fn constrain_lined(app: &mut SolveApp, tool: &mut Tool, placed: &[Vec
app.session.undo = history;
}
/// The point of `poly` nearest `p`; None for fewer than two points.
fn nearest_on_polyline(poly: &[Vec2], p: Vec2) -> Option<Vec2> {
poly.windows(2)
.map(|w| {
let (a, b) = (w[0], w[1]);
let ab = b - a;
let t = if ab.len2() > 0.0 { ((p - a).dot(ab) / ab.len2()).clamp(0.0, 1.0) } else { 0.0 };
a + ab * t
})
.min_by(|x, y| x.dist(p).total_cmp(&y.dist(p)))
}
/// Snap a cursor point on the sketch plane to what is inferred while drawing: a crossing of
/// two curves, a line's midpoint, or alignment (horizontal/vertical) with the previous point
/// of the tool. None: nothing near, use the grid.
@@ -563,13 +575,20 @@ pub fn refine_snap(
best = Some((i.dist, i.label(), i.at));
mid_of = i.arg;
}
// On a curve (coincident with it).
// On a curve (coincident with it). A spline, ellipse or conic has no exact shape here: the
// nearest point of the polyline it is drawn with, which the click's point-on-curve
// constraint then puts on the curve itself (#9321).
if !mid_only && best.is_none() {
for (i, c) in sk.curves.iter().enumerate() {
let Some(sh) = sk.shape(i) else { continue };
let d = sh.dist(lp);
let (d, q) = match sk.shape(i) {
Some(sh) => (sh.dist(lp), sh.project(lp)),
None if c.kind.is_freeform() => {
let Some(q) = nearest_on_polyline(&sk.polyline(i), lp) else { continue };
(q.dist(lp), q)
}
None => continue,
};
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));
}