mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
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:
@@ -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]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user