mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Sweep path: a curve of several pieces is turned round whole
The path chain oriented its first piece, not its first curve: a first curve of several exact pieces (a fit spline, an ellipse, a projected free-form edge) was never turned round, so a path whose next curve meets it at its start was refused as "not connected end to end". The chain now orients curve by curve: the first runs toward the second where they meet at one end only (else from the end nearest the profile), each next one starts where the previous ended, with one joint tolerance for both questions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+34
-25
@@ -5275,37 +5275,46 @@ pub(crate) fn path_segments_at(ps: &SolvedSketch, ids: &[String], start: Vec3, p
|
||||
if let Some(path) = path_points(ps, ids, start)? {
|
||||
return Ok(path.points().windows(2).map(|w| kernel::PathSeg::Line { a: w[0], b: w[1] }).collect());
|
||||
}
|
||||
let mut segs: Vec<solidcraft_geom::Seg2> = Vec::new();
|
||||
// Each curve as its exact pieces (Béziers, conics) where it has them, in its own direction
|
||||
// of travel. A curve is turned round whole: its pieces stay one run.
|
||||
let mut curves: Vec<Vec<solidcraft_geom::Seg2>> = Vec::with_capacity(ids.len());
|
||||
for id in ids {
|
||||
let ci = ps.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("path curve `{id}`")))?;
|
||||
// A free-form curve as its exact pieces (Béziers, conics) where it has them.
|
||||
segs.extend(ps.sketch.exact_segs(ci).unwrap_or_else(|| ps.sketch.segs(ci)));
|
||||
curves.push(ps.sketch.exact_segs(ci).unwrap_or_else(|| ps.sketch.segs(ci)));
|
||||
}
|
||||
// Orient the chain: the first segment starts at the end nearest `start`, each next one
|
||||
// starts where the previous ended.
|
||||
curves.retain(|c| !c.is_empty());
|
||||
let ends = |c: &[solidcraft_geom::Seg2]| c.first().zip(c.last()).map(|(f, l)| (f.start(), l.end()));
|
||||
let reversed = |c: Vec<solidcraft_geom::Seg2>| c.into_iter().rev().map(|s| s.reversed()).collect::<Vec<_>>();
|
||||
// Where two curves meet: one tolerance for telling which ends touch and for the joint.
|
||||
const JOINT: f64 = 1e-5;
|
||||
// Orient the first curve: it runs toward the second where it meets it at one end only;
|
||||
// otherwise (one curve, or a second meeting it at both ends) from the end nearest `start`
|
||||
// (no start given, a pipe: as drawn).
|
||||
let w = |p: Vec2| ps.plane.to_world(p);
|
||||
if let (Some(f), true) = (segs.first().copied(), !segs.is_empty())
|
||||
&& w(f.end()).dist(start) < w(f.start()).dist(start)
|
||||
&& (segs.len() == 1 || segs.get(1).is_some_and(|n| n.start().dist(f.start()) < 1e-6 || n.end().dist(f.start()) < 1e-6))
|
||||
{
|
||||
segs[0] = f.reversed();
|
||||
}
|
||||
// No start given (a pipe): the first curve runs toward the second.
|
||||
if !start.is_finite()
|
||||
&& let (Some(f), Some(n)) = (segs.first().copied(), segs.get(1).copied())
|
||||
&& (n.start().dist(f.start()) < 1e-6 || n.end().dist(f.start()) < 1e-6)
|
||||
{
|
||||
segs[0] = f.reversed();
|
||||
}
|
||||
for i in 1..segs.len() {
|
||||
let prev_end = segs[i - 1].end();
|
||||
if segs[i].end().dist(prev_end) < segs[i].start().dist(prev_end) {
|
||||
segs[i] = segs[i].reversed();
|
||||
}
|
||||
if segs[i].start().dist(prev_end) > 1e-5 {
|
||||
return Err(DocError::Invalid("the path curves are not connected end to end".into()));
|
||||
if let Some((a, b)) = curves.first().and_then(|c| ends(c)) {
|
||||
let touches = |p: Vec2| curves.get(1).and_then(|n| ends(n)).is_some_and(|(na, nb)| na.dist(p) <= JOINT || nb.dist(p) <= JOINT);
|
||||
let flip = match (touches(a), touches(b)) {
|
||||
(true, false) => true,
|
||||
(false, true) => false,
|
||||
_ => start.is_finite() && w(b).dist(start) < w(a).dist(start),
|
||||
};
|
||||
if flip && let Some(first) = curves.first_mut() {
|
||||
*first = reversed(std::mem::take(first));
|
||||
}
|
||||
}
|
||||
// Each next curve starts where the previous one ended.
|
||||
let mut segs: Vec<solidcraft_geom::Seg2> = Vec::new();
|
||||
for curve in curves {
|
||||
let (Some(prev_end), Some((a, b))) = (segs.last().map(solidcraft_geom::Seg2::end), ends(&curve)) else {
|
||||
segs.extend(curve);
|
||||
continue;
|
||||
};
|
||||
let curve = if b.dist(prev_end) < a.dist(prev_end) { reversed(curve) } else { curve };
|
||||
segs.extend(curve);
|
||||
}
|
||||
if segs.windows(2).any(|pair| pair[1].start().dist(pair[0].end()) > JOINT) {
|
||||
return Err(DocError::Invalid("the path curves are not connected end to end".into()));
|
||||
}
|
||||
if let Some(profile) = profile {
|
||||
segs = from_profile_on_closed_path(segs, &ps.plane, profile, start);
|
||||
}
|
||||
|
||||
@@ -192,3 +192,25 @@ fn a_sweep_round_a_closed_path_starts_where_the_profile_is() {
|
||||
assert!((got - want).abs() < 0.08, "{:?}", m.bbox);
|
||||
}
|
||||
}
|
||||
|
||||
/// #3604: a path whose first curve is a spline of several exact pieces, the next curve meeting
|
||||
/// it at its start, follows the spline back from its end: a curve is turned round whole, not
|
||||
/// piece by piece. A 2 x 2 profile at the spline's end (20, 0), swept along the spline through
|
||||
/// (20, 0), (10, 0), (0, 0) and on along the line to (-15, 0): a 35 long bar.
|
||||
#[test]
|
||||
fn a_path_turns_a_spline_of_several_pieces_round_whole() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Path"}));
|
||||
run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [10, 0], [20, 0]], "ids": ["s1"]}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[-15, 0], [0, 0]], "infer": false, "ids": ["l1"]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sketch.create", json!({"plane": {"origin": [20, 0, 0], "x_dir": [0, 1, 0], "y_dir": [0, 0, 1]}, "name": "Prof"}));
|
||||
run(&mut s, "sketch.rectangle.center", json!({"center": [0, 0], "corner": [1, 1]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.sweep", json!({"sketch": "Prof", "path_sketch": "Path", "path": ["s1", "l1"], "operation": "new", "body_name": "Bar"}));
|
||||
let m = measure(&s, "Bar");
|
||||
assert!((m.volume - 140.0).abs() < 140.0 * 1e-6, "{}", m.volume);
|
||||
for (got, want) in [(m.bbox.min.x, -15.0), (m.bbox.max.x, 20.0), (m.bbox.min.y, -1.0), (m.bbox.max.y, 1.0)] {
|
||||
assert!((got - want).abs() < 0.04, "{:?}", m.bbox);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user