A side constrained onto a projected line keeps the sketch's closed profile. Fixes #635.
This commit is contained in:
1 parent
dfa9e094cd
commit
159f9913c1
4 files changed
+441
-6
No files matched your search
@@ -526,3 +526,62 @@ fn faces_split_by_hidden_seams_project_whole() {
|
||||
assert_eq!(radii.len(), 4, "{round}");
|
||||
assert!(radii[..2].iter().all(|r| (r - 17.).abs() < 1e-6) && radii[2..].iter().all(|r| (r - 20.).abs() < 1e-6), "{round}");
|
||||
}
|
||||
|
||||
/// #635: a rectangle whose corner is made coincident with a projected edge of the solid under
|
||||
/// the sketch, so that its bottom side lies on that edge, is still one closed profile (the
|
||||
/// projected line stays, and names the shared edge as it was there first), and it extrudes to
|
||||
/// its own volume, also when the design is opened again.
|
||||
#[test]
|
||||
fn a_rectangle_put_onto_a_projected_edge_stays_a_closed_profile() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 20}));
|
||||
let block = s.model.state().bodies[0].name.clone();
|
||||
let id = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
|
||||
// The solid's bottom edge along X, from (0, 0) to (40, 0).
|
||||
let r = run(&mut s, "sketch.project", json!({"refs": [{"edge": [20, 0, 0]}]}));
|
||||
let projected = r["links"][0]["curves"][0].as_str().unwrap().to_string();
|
||||
// A 20 x 15 rectangle clear of it is a closed profile of its own.
|
||||
let sides = ids(&run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 10], "p1": [25, 25]}))["curves"]);
|
||||
run(&mut s, "sketch.dimension", json!({"entities": [sides[0]], "value": 20}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": [sides[1]], "value": 15}));
|
||||
let mut own = sides.clone();
|
||||
own.sort();
|
||||
let profile_curves = |si: &Value| -> Vec<Vec<String>> {
|
||||
si["profiles"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let mut c = ids(&p["curves"]);
|
||||
c.sort();
|
||||
c
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
assert_eq!(profile_curves(&inspect(&mut s)), vec![own.clone()]);
|
||||
// Its bottom-left corner onto the projected line: the bottom side lies on it.
|
||||
run(&mut s, "sketch.constraint.coincident", json!({"a": format!("{}.start", sides[0]), "b": projected}));
|
||||
let si = inspect(&mut s);
|
||||
assert_eq!(si["status"], "solved", "{si}");
|
||||
let curve = |id: &str| si["curves"].as_array().unwrap().iter().find(|c| c["id"] == id).unwrap().clone();
|
||||
let bottom = curve(&sides[0]);
|
||||
for end in ["start_at", "end_at"] {
|
||||
assert!(bottom[end][1].as_f64().unwrap().abs() < 1e-9, "{bottom}");
|
||||
}
|
||||
assert!(curve(&projected).get("link").is_some(), "the projected line stays");
|
||||
let mut on_line = vec![projected.clone(), sides[1].clone(), sides[2].clone(), sides[3].clone()];
|
||||
on_line.sort();
|
||||
assert_eq!(profile_curves(&si), vec![on_line], "{si}");
|
||||
assert!((si["profiles"][0]["area_mm2"].as_f64().unwrap() - 300.0).abs() < 1e-6, "{si}");
|
||||
// Picked by a point inside, it extrudes to a 20 x 15 x 10 block of its own.
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": id, "profiles": [{"point": [15, 7.5]}], "distance": 10, "operation": "new", "body_name": "Pad"}));
|
||||
let volume = |s: &Session, name: &str| s.model.state().body(name).unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
assert!((volume(&s, "Pad") - 3000.0).abs() < 1e-6, "{}", volume(&s, "Pad"));
|
||||
assert!((volume(&s, &block) - 24000.0).abs() < 1e-6, "{}", volume(&s, &block));
|
||||
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
assert!((volume(&reopened, "Pad") - 3000.0).abs() < 1e-6, "{}", volume(&reopened, "Pad"));
|
||||
}
|
||||
@@ -61,6 +61,47 @@ struct Edge {
|
||||
/// A piece of an exact free-form segment: (curve, segment, piece, pieces).
|
||||
tag: Option<Piece>,
|
||||
coverage: Option<Piece>,
|
||||
/// A piece of a line, arc or circle (not one sampled from a free-form curve).
|
||||
analytic: bool,
|
||||
}
|
||||
|
||||
/// Do two circles lie within `tol` of each other all the way round?
|
||||
fn same_circle(c: Vec2, r: f64, c2: Vec2, r2: f64, tol: f64) -> bool {
|
||||
c.dist(c2) + (r - r2).abs() <= tol
|
||||
}
|
||||
|
||||
/// Which edges stay in the graph: of pieces lying on top of each other between the same two
|
||||
/// nodes (a side drawn onto a projected edge, which the solver puts there only up to round-off),
|
||||
/// the first, so the earliest curve in sketch order names the shared edge, as when the pieces
|
||||
/// lie exactly on each other. Kept both, they would bound a sliver of no area, and the
|
||||
/// round-off in their directions would make the turns at their ends disagree, so the regions on
|
||||
/// both sides would be lost (#635).
|
||||
fn without_overlaps(edges: &[Edge], tol: f64) -> Vec<bool> {
|
||||
// Between the same nodes, two pieces of lines run along one path, and so do two pieces of
|
||||
// arcs or circles on one circle through the same middle (not the two halves of a circle). A
|
||||
// line and an arc never do, nor does a sampled piece of a free-form curve: a chord and its
|
||||
// curve bound a region, however thin.
|
||||
let same_path = |a: &Edge, b: &Edge| match (a.seg, b.seg) {
|
||||
_ if !(a.analytic && b.analytic) => false,
|
||||
(Seg2::Line { .. }, Seg2::Line { .. }) => true,
|
||||
(Seg2::Arc { center, radius, .. }, Seg2::Arc { center: c2, radius: r2, .. }) => {
|
||||
same_circle(center, radius, c2, r2, tol) && a.seg.mid().dist(b.seg.mid()) <= 2.0 * tol
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let mut kept: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
|
||||
edges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, e)| {
|
||||
let between = kept.entry((e.u.min(e.v), e.u.max(e.v))).or_default();
|
||||
let first = !between.iter().any(|k| edges.get(*k).is_some_and(|f| same_path(f, e)));
|
||||
if first {
|
||||
between.push(i);
|
||||
}
|
||||
first
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
type Piece = (usize, usize, usize, usize);
|
||||
@@ -119,6 +160,20 @@ fn complete_loop(tags: &[Option<(Piece, bool)>], expected: &std::collections::Ha
|
||||
Some(ids)
|
||||
}
|
||||
|
||||
/// Do two shapes of lines, arcs or circles lie on one line or circle (within `tol` all along)?
|
||||
/// Where they overlap they don't cross: the points where one ends on the other cut them (each
|
||||
/// end lies within `tol` of the other). Their crossing, computed from round-off alone, would
|
||||
/// land anywhere along them and cut both apart there. Two whole circles have no ends to cut
|
||||
/// them, so they keep their crossings.
|
||||
fn overlapping(x: &Shape, y: &Shape, tol: f64) -> bool {
|
||||
let near_line = |a: Vec2, b: Vec2, ps: [Vec2; 2]| (b - a).normalized().is_some_and(|d| ps.iter().all(|p| (*p - a).cross(d).abs() <= tol));
|
||||
match (*x, *y) {
|
||||
(Shape::Line { a, b }, Shape::Line { a: c, b: d }) => near_line(a, b, [c, d]) && near_line(c, d, [a, b]),
|
||||
(Shape::Round { c, r, .. }, Shape::Round { c: c2, r: r2, .. }) => !(x.is_full() && y.is_full()) && same_circle(c, r, c2, r2, tol),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameter of `p` at an end of the shape (lines: 0 or 1), for intersections at end points.
|
||||
fn end_param(sh: &Shape, p: Vec2, tol: f64) -> Option<f64> {
|
||||
match *sh {
|
||||
@@ -493,6 +548,9 @@ pub fn find_profiles(sk: &Sketch) -> Vec<Profile> {
|
||||
|
||||
fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
||||
let tol = MERGE_TOL * 10.0;
|
||||
// Lines, arcs and circles (free-form curves take part as sampled pieces).
|
||||
let analytic =
|
||||
|ci: usize| sk.curves.get(ci).is_some_and(|c| matches!(c.kind, CurveKind::Line { .. } | CurveKind::Arc { .. } | CurveKind::Circle { .. }));
|
||||
// Analytic shapes of the profile curves.
|
||||
let mut shapes: Vec<(usize, Shape)> = Vec::new();
|
||||
let mut tags: Vec<Option<Piece>> = Vec::new();
|
||||
@@ -640,14 +698,51 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
||||
p.sort_unstable();
|
||||
p.dedup();
|
||||
}
|
||||
// Lines (and arcs) lying on a line (or an arc) are one path with it, carried by it: another
|
||||
// curve meets them all where it meets the carrier, so they are cut at the same points. Met
|
||||
// one by one, a curve touching them, or crossing them at a shallow angle, would meet each a
|
||||
// little apart, and pieces cut at different points would not be one edge. Their crossings
|
||||
// with each other, computed from round-off alone, are no cuts. The carrier is the first of
|
||||
// them, and each lies on it itself (not just on one that lies on it); a carrier takes one
|
||||
// whole circle at most, as two whole circles keep their crossings.
|
||||
let mut carrier: Vec<usize> = (0..shapes.len()).collect();
|
||||
let mut anchors: Vec<Vec2> = Vec::new();
|
||||
if shapes.len() <= 50_000 {
|
||||
let mut whole: Vec<bool> = shapes.iter().map(|(_, s)| s.is_full()).collect();
|
||||
for i in 0..shapes.len() {
|
||||
let Some((ci, x)) = shapes.get(i) else { continue };
|
||||
if carrier.get(i) != Some(&i) || !analytic(*ci) {
|
||||
continue;
|
||||
}
|
||||
for &j in partners.get(i).map(Vec::as_slice).unwrap_or(&[]) {
|
||||
let Some((cj, y)) = shapes.get(j) else { continue };
|
||||
let taken = y.is_full() && whole.get(i).copied().unwrap_or(true);
|
||||
if carrier.get(j) == Some(&j) && !taken && overlap(i, j) && analytic(*cj) && overlapping(x, y, tol) {
|
||||
if let Some(c) = carrier.get_mut(j) {
|
||||
*c = i;
|
||||
}
|
||||
if let Some(w) = whole.get_mut(i) {
|
||||
*w |= y.is_full();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if shapes.len() <= 50_000 {
|
||||
for i in 0..shapes.len() {
|
||||
for &j in partners.get(i).map(Vec::as_slice).unwrap_or(&[]) {
|
||||
if !overlap(i, j) {
|
||||
continue;
|
||||
}
|
||||
let (Some((_, x)), Some((_, y))) = (shapes.get(i), shapes.get(j)) else { continue };
|
||||
let mut pts = intersections(x, y);
|
||||
let (gi, gj) = (carrier.get(i).copied().unwrap_or(i), carrier.get(j).copied().unwrap_or(j));
|
||||
let (Some((_, x)), Some((_, y)), Some((_, gx)), Some((_, gy))) = (shapes.get(i), shapes.get(j), shapes.get(gi), shapes.get(gj))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if gi == gj {
|
||||
continue;
|
||||
}
|
||||
let mut pts = intersections(gx, gy);
|
||||
// A tangent touch gives two nearly equal roots: one point.
|
||||
if let [p, q] = pts[..]
|
||||
&& p.dist(q) < tol * 100.0
|
||||
@@ -682,6 +777,9 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
||||
{
|
||||
c.push(ty);
|
||||
}
|
||||
if (gi, gj) != (i, j) {
|
||||
anchors.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -708,6 +806,11 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
||||
grid.entry((cx, cy)).or_default().push(i);
|
||||
i
|
||||
};
|
||||
// Where curves meet at their carriers' crossing, the cuts on each lie within the tolerance of
|
||||
// that point, though not always of each other: they meet at a node there.
|
||||
for a in &anchors {
|
||||
node(*a);
|
||||
}
|
||||
let mut loops: Vec<(Loop2, Vec<String>, usize, Option<Vec<String>>)> = Vec::new(); // (ccw loop, curve ids, component)
|
||||
let mut edges: Vec<Edge> = Vec::new();
|
||||
for (k, (ci, sh)) in shapes.iter().enumerate() {
|
||||
@@ -728,6 +831,7 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
||||
ts.pop();
|
||||
}
|
||||
let id = sk.curves.get(*ci).map(|c| c.id.clone()).unwrap_or_default();
|
||||
let analytic = analytic(*ci);
|
||||
let coverage = tags.get(k).copied().flatten().or_else(|| {
|
||||
sk.curves
|
||||
.get(*ci)
|
||||
@@ -746,7 +850,7 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
||||
let (p, q) = (w[0], w[1]);
|
||||
if p.dist(q) > MERGE_TOL {
|
||||
let (u, v) = (node(p), node(q));
|
||||
edges.push(Edge { seg: Seg2::Line { a: p, b: q }, u, v, id: id.clone(), tag, coverage });
|
||||
edges.push(Edge { seg: Seg2::Line { a: p, b: q }, u, v, id: id.clone(), tag, coverage, analytic });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -769,14 +873,14 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
||||
}
|
||||
let seg = Seg2::Arc { center: c, radius: r, start: start + t0, sweep: t1 - t0 };
|
||||
let (u, v) = (node(seg.start()), node(seg.end()));
|
||||
edges.push(Edge { seg, u, v, id: id.clone(), tag: None, coverage });
|
||||
edges.push(Edge { seg, u, v, id: id.clone(), tag: None, coverage, analytic });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let n = nodes.len();
|
||||
// Prune dangling edges.
|
||||
let mut alive = vec![true; edges.len()];
|
||||
// One edge where pieces overlap, then prune dangling edges.
|
||||
let mut alive = without_overlaps(&edges, tol);
|
||||
loop {
|
||||
let mut deg = vec![0usize; n];
|
||||
for (e, a) in edges.iter().zip(&alive) {
|
||||
|
||||
@@ -320,6 +320,246 @@ fn profiles_split_at_crossings_and_touch_points() {
|
||||
assert!(total > PI * 500.0, "{total}");
|
||||
}
|
||||
|
||||
/// Sorted curve ids.
|
||||
fn id_set(sk: &Sketch, curves: &[usize]) -> Vec<String> {
|
||||
let mut ids: Vec<String> = curves.iter().map(|c| sk.curves[*c].id.clone()).collect();
|
||||
ids.sort();
|
||||
ids
|
||||
}
|
||||
|
||||
/// A projected line of `a`–`b` in `sk`.
|
||||
fn reference_line(sk: &mut Sketch, a: Vec2, b: Vec2) -> usize {
|
||||
let link = sk.add_link(LinkKind::Project, LinkSource::SketchCurve { sketch: 1, curve: "edge".into() }, &[LinkGeom::Line(a, b)]).unwrap();
|
||||
sk.link_curves(&link)[0]
|
||||
}
|
||||
|
||||
/// #635: a side constrained onto a longer projected line lies on it only up to the solver's
|
||||
/// round-off. Where they overlap they are one edge of the drawing, so the rectangle is still a
|
||||
/// closed profile, and the curve that came first names that edge, as when the side lies on the
|
||||
/// line exactly.
|
||||
#[test]
|
||||
fn a_side_on_a_projected_line_still_closes_its_rectangle() {
|
||||
// Round-off left on the side's two ends (the first pair as solved in #635), and none.
|
||||
for (y0, y1) in
|
||||
[(1.6e-12, 2.2e-12), (2.2e-12, 1.6e-12), (-1e-12, 1e-12), (1e-12, -1e-12), (-3e-11, -1e-11), (-3e-10, 3e-10), (2e-10, 4e-10), (0.0, 0.0)]
|
||||
{
|
||||
for reference_first in [true, false] {
|
||||
for (ra, rb) in [(v(40.0, 0.0), v(-10.0, 0.0)), (v(-10.0, 0.0), v(40.0, 0.0))] {
|
||||
let mut sk = Sketch::new();
|
||||
let early = reference_first.then(|| reference_line(&mut sk, ra, rb));
|
||||
let sides = rect(&mut sk, [v(5.0, y0), v(25.0, y1), v(25.0, 15.0), v(5.0, 15.0)]);
|
||||
let reference = early.unwrap_or_else(|| reference_line(&mut sk, ra, rb));
|
||||
let case = format!("ends at y {y0}, {y1}; reference {} {ra:?} to {rb:?}", if reference_first { "first" } else { "last" });
|
||||
let ps = find_profiles(&sk);
|
||||
assert_eq!(ps.len(), 1, "{case}: {:?}", ps.iter().map(|p| (p.area, &p.outer_curves)).collect::<Vec<_>>());
|
||||
assert!((ps[0].area - 300.0).abs() < 1e-6, "{case}: {}", ps[0].area);
|
||||
let mut got = ps[0].outer_curves.clone();
|
||||
got.sort();
|
||||
let want = id_set(&sk, &[if reference_first { reference } else { sides[0] }, sides[1], sides[2], sides[3]]);
|
||||
assert_eq!(got, want, "{case}");
|
||||
// Whole curves make a lasting identity; a piece of the reference cut at the corners does not.
|
||||
let whole = (!reference_first).then(|| (want, Vec::new()));
|
||||
assert_eq!(ps[0].whole_curve_boundaries(), whole, "{case}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A third curve meeting a side and the projected line under it would meet each a little apart:
|
||||
/// a circle touching the line from inside the rectangle (it misses or crosses the side by the
|
||||
/// round-off), and a line crossing both at a shallow angle (the crossings 3e-5 mm apart). It
|
||||
/// meets both where it meets the first of them, so they still make one edge, whichever way the
|
||||
/// side runs.
|
||||
#[test]
|
||||
fn a_curve_meeting_a_side_on_a_projected_line_meets_both_at_once() {
|
||||
for y0 in [3e-10, -3e-10, 0.0] {
|
||||
for reference_first in [true, false] {
|
||||
for reversed in [false, true] {
|
||||
let case = format!("side at y {y0}, reference {}, side reversed {reversed}", if reference_first { "first" } else { "last" });
|
||||
let corners = |x1: f64| {
|
||||
let (a, b) = if reversed { (x1, 5.0) } else { (5.0, x1) };
|
||||
[v(a, y0), v(b, y0), v(b, 15.0), v(a, 15.0)]
|
||||
};
|
||||
let mut sk = Sketch::new();
|
||||
let early = reference_first.then(|| reference_line(&mut sk, v(-10.0, 0.0), v(40.0, 0.0)));
|
||||
rect(&mut sk, corners(25.0));
|
||||
sk.add_circle(v(15.0, 5.0), 5.0, None, None).unwrap();
|
||||
early.unwrap_or_else(|| reference_line(&mut sk, v(-10.0, 0.0), v(40.0, 0.0)));
|
||||
let mut areas: Vec<f64> = find_profiles(&sk).iter().map(|p| p.area).collect();
|
||||
areas.sort_by(f64::total_cmp);
|
||||
assert_eq!(areas.len(), 2, "touching circle, {case}: {areas:?}");
|
||||
assert!((areas[0] - 25.0 * PI).abs() < 1e-6 && (areas[1] - (300.0 - 25.0 * PI)).abs() < 1e-6, "touching circle, {case}: {areas:?}");
|
||||
|
||||
let mut sk = Sketch::new();
|
||||
let early = reference_first.then(|| reference_line(&mut sk, v(-10.0, 0.0), v(440.0, 0.0)));
|
||||
rect(&mut sk, corners(405.0));
|
||||
// Through (200, 0), rising 1e-5 per mm: a sliver of 205 x 0.00205 / 2 under it.
|
||||
sk.add_line(v(0.0, -0.002), v(430.0, 0.0023), None, None, None).unwrap();
|
||||
early.unwrap_or_else(|| reference_line(&mut sk, v(-10.0, 0.0), v(440.0, 0.0)));
|
||||
let mut areas: Vec<f64> = find_profiles(&sk).iter().map(|p| p.area).collect();
|
||||
areas.sort_by(f64::total_cmp);
|
||||
assert_eq!(areas.len(), 2, "shallow line, {case}: {areas:?}");
|
||||
assert!((areas[0] - 0.210125).abs() < 1e-6 && (areas[1] - (6000.0 - 0.210125)).abs() < 1e-6, "shallow line, {case}: {areas:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same for drawn curves on drawn curves: two rectangles drawn side by side, the second's
|
||||
/// side solved onto the first's; and a quarter disc whose arc is solved onto a projected circle.
|
||||
#[test]
|
||||
fn curves_solved_onto_each_other_keep_the_regions_on_both_sides() {
|
||||
for (x0, x1) in [(1e-12, -1e-12), (-2e-12, -1e-12), (2.2e-12, 1.6e-12), (0.0, 0.0)] {
|
||||
let mut sk = Sketch::new();
|
||||
let a = rect(&mut sk, [v(0.0, 0.0), v(20.0, 0.0), v(20.0, 15.0), v(0.0, 15.0)]);
|
||||
let b = rect(&mut sk, [v(20.0 + x0, 0.0), v(40.0, 0.0), v(40.0, 15.0), v(20.0 + x1, 15.0)]);
|
||||
let ps = find_profiles(&sk);
|
||||
let areas: Vec<f64> = ps.iter().map(|p| p.area).collect();
|
||||
assert_eq!(ps.len(), 2, "{x0}, {x1}: {areas:?}");
|
||||
assert!(areas.iter().all(|a| (a - 300.0).abs() < 1e-6), "{x0}, {x1}: {areas:?}");
|
||||
// The shared side is the first rectangle's (it was there first).
|
||||
let mut first = ps[0].outer_curves.clone();
|
||||
first.sort();
|
||||
assert_eq!(first, id_set(&sk, &a), "{x0}, {x1}");
|
||||
let mut second = ps[1].outer_curves.clone();
|
||||
second.sort();
|
||||
assert_eq!(second, id_set(&sk, &[b[0], b[1], b[2], a[1]]), "{x0}, {x1}");
|
||||
}
|
||||
let hole = |sk: &mut Sketch| {
|
||||
let link = sk
|
||||
.add_link(LinkKind::Project, LinkSource::SketchCurve { sketch: 1, curve: "hole".into() }, &[LinkGeom::Circle(v(0.0, 0.0), 10.0)])
|
||||
.unwrap();
|
||||
sk.link_curves(&link)[0]
|
||||
};
|
||||
for (dx, dr) in [(1e-12, 2e-12), (-1e-12, -1e-12), (0.0, 0.0)] {
|
||||
for circle_first in [true, false] {
|
||||
let mut sk = Sketch::new();
|
||||
let early = circle_first.then(|| hole(&mut sk));
|
||||
let c = sk.add_point(v(dx, -dx), None).unwrap();
|
||||
let (p0, p1) = (sk.add_point(v(10.0 + dr, 0.0), None).unwrap(), sk.add_point(v(0.0, 10.0 + dr), None).unwrap());
|
||||
let arc = sk.add_arc(v(dx, -dx), v(10.0 + dr, 0.0), v(0.0, 10.0 + dr), [Some(c), Some(p0), Some(p1)], None).unwrap();
|
||||
let (l0, l1) = (sk.add_line_pts(c, p0, None).unwrap(), sk.add_line_pts(p1, c, None).unwrap());
|
||||
let circle = early.unwrap_or_else(|| hole(&mut sk));
|
||||
let case = format!("{dx}, {dr}, circle {}", if circle_first { "first" } else { "last" });
|
||||
let ps = find_profiles(&sk);
|
||||
let areas: Vec<f64> = ps.iter().map(|p| p.area).collect();
|
||||
assert_eq!(ps.len(), 2, "{case}: {areas:?}");
|
||||
let quarter = ps.iter().find(|p| p.area < 100.0).unwrap();
|
||||
assert!((quarter.area - 25.0 * PI).abs() < 1e-6, "{case}: {areas:?}");
|
||||
// One piece each: round-off alone does not cut the arc where it crosses the circle.
|
||||
let mut got = quarter.outer_curves.clone();
|
||||
got.sort();
|
||||
assert_eq!(got, id_set(&sk, &[if circle_first { circle } else { arc }, l0, l1]), "{case}");
|
||||
let rest = ps.iter().find(|p| p.area > 100.0).unwrap();
|
||||
assert!((rest.area - 75.0 * PI).abs() < 1e-6, "{case}: {areas:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What does not run along one path stays apart: a chord and an arc bulging past it by about
|
||||
/// the merge tolerance still bound their thin region, and two whole circles further apart than
|
||||
/// it are still cut where they cross, into a lens and two crescents. Closer than it, their
|
||||
/// halves are one path: one disc.
|
||||
#[test]
|
||||
fn near_overlaps_that_are_not_one_path_keep_their_regions() {
|
||||
let s = 1.5e-5;
|
||||
let r = (25.0 + s * s) / (2.0 * s);
|
||||
let mut sk = Sketch::new();
|
||||
let (a, b) = (sk.add_point(v(-5.0, 0.0), None).unwrap(), sk.add_point(v(5.0, 0.0), None).unwrap());
|
||||
let chord = sk.add_line_pts(a, b, None).unwrap();
|
||||
let arc = sk.add_arc(v(0.0, s - r), v(5.0, 0.0), v(-5.0, 0.0), [None, Some(b), Some(a)], None).unwrap();
|
||||
let ps = find_profiles(&sk);
|
||||
assert_eq!(ps.len(), 1, "{:?}", ps.iter().map(|p| p.area).collect::<Vec<_>>());
|
||||
// Two thirds of chord times height, to well within the rounding of the area.
|
||||
assert!((ps[0].area - 2.0 / 3.0 * 10.0 * s).abs() < 1e-8, "{}", ps[0].area);
|
||||
let mut got = ps[0].outer_curves.clone();
|
||||
got.sort();
|
||||
assert_eq!(got, id_set(&sk, &[chord, arc]));
|
||||
|
||||
// The same with a free-form curve: a flat cubic (height s) over its chord. It takes part as
|
||||
// sampled pieces, one of them on the chord, but its exact curve bounds the region.
|
||||
let s = 8e-6;
|
||||
let mut sk = Sketch::new();
|
||||
let ids: Vec<usize> = [v(-5.0, 0.0), v(-5.0 / 3.0, 4.0 * s / 3.0), v(5.0 / 3.0, 4.0 * s / 3.0), v(5.0, 0.0)]
|
||||
.iter()
|
||||
.map(|p| sk.add_point(*p, None).unwrap())
|
||||
.collect();
|
||||
sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: true, degree: 3, periodic: false }, None).unwrap();
|
||||
sk.add_line_pts(ids[3], ids[0], None).unwrap();
|
||||
let ps = find_profiles(&sk);
|
||||
assert_eq!(ps.len(), 1, "{:?}", ps.iter().map(|p| p.area).collect::<Vec<_>>());
|
||||
assert!((ps[0].area - 20.0 * s / 3.0).abs() < 1e-9, "{}", ps[0].area);
|
||||
|
||||
// An arc a little off a circle, more than the merge tolerance apart at its ends (so they
|
||||
// don't cut the circle): the two still cut each other where they cross, and no profile is
|
||||
// the uncut circle.
|
||||
let mut sk = Sketch::new();
|
||||
let circle = sk.add_circle(v(0.0, 0.0), 10.0, None, None).unwrap();
|
||||
let (c, r) = (v(8e-6, 0.0), 10.0 + 6e-6);
|
||||
let (p0, p1) = (c + Vec2::from_angle(PI / 4.0) * r, c + Vec2::from_angle(-PI / 4.0) * r);
|
||||
let (a0, a1, tip) = (sk.add_point(p0, None).unwrap(), sk.add_point(p1, None).unwrap(), sk.add_point(v(30.0, 0.0), None).unwrap());
|
||||
sk.add_arc(c, p0, p1, [None, Some(a0), Some(a1)], None).unwrap();
|
||||
sk.add_line_pts(a1, tip, None).unwrap();
|
||||
sk.add_line_pts(tip, a0, None).unwrap();
|
||||
let ps = find_profiles(&sk);
|
||||
let circle_id = sk.curves[circle].id.clone();
|
||||
assert!(ps.iter().all(|p| p.outer_curves != [circle_id.clone()] && p.whole_curve_boundaries().is_none()), "{ps:?}");
|
||||
// Profiles don't overlap: together they cover the disc and the arc's region once, that is the
|
||||
// disc and the triangle to the tip less the disc's segment inside it (to within the slivers
|
||||
// between arc and circle).
|
||||
let beyond = 0.5 * (p0 - v(30.0, 0.0)).cross(p1 - v(30.0, 0.0)).abs() - 50.0 * (PI / 2.0 - 1.0);
|
||||
let covered = ps.iter().map(|p| p.area).sum::<f64>();
|
||||
assert!((covered - PI * 100.0 - beyond).abs() < 1e-2, "{:?}", ps.iter().map(|p| p.area).collect::<Vec<_>>());
|
||||
|
||||
let r = 10.0_f64;
|
||||
for (d, regions) in [(1e-3, 3), (1e-4, 3), (8e-6, 1)] {
|
||||
let mut sk = Sketch::new();
|
||||
let first = sk.add_circle(v(0.0, 0.0), r, None, None).unwrap();
|
||||
sk.add_circle(v(d, 0.0), r, None, None).unwrap();
|
||||
let ps = find_profiles(&sk);
|
||||
let areas: Vec<f64> = ps.iter().map(|p| p.area).collect();
|
||||
assert_eq!(ps.len(), regions, "{d}: {areas:?}");
|
||||
let lens = 2.0 * r * r * (d / (2.0 * r)).acos() - d / 2.0 * (4.0 * r * r - d * d).sqrt();
|
||||
let union = if regions == 1 { PI * r * r } else { 2.0 * PI * r * r - lens };
|
||||
assert!((areas.iter().sum::<f64>() - union).abs() < 1e-6, "{d}: {areas:?}");
|
||||
// Cut where they cross: no whole-circle identity.
|
||||
assert!(ps.iter().all(|p| p.whole_curve_boundaries().is_none()), "{d}");
|
||||
if regions == 1 {
|
||||
assert_eq!(ps[0].outer_curves, vec![sk.curves[first].id.clone(); 2], "{d}: the first circle's halves");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Curves count as lying on one another only within the merge tolerance of the one carrying
|
||||
/// them, not along a chain: an arc between two whole circles 1.6e-5 apart (each within 8e-6 of
|
||||
/// it) does not stop the circles cutting each other where they cross.
|
||||
#[test]
|
||||
fn curves_lying_on_one_another_do_not_chain_past_the_tolerance() {
|
||||
let (d, r) = (16e-6, 10.0_f64);
|
||||
let mut sk = Sketch::new();
|
||||
sk.add_circle(v(0.0, 0.0), r, None, None).unwrap();
|
||||
sk.add_circle(v(d, 0.0), r, None, None).unwrap();
|
||||
let c = v(d / 2.0, 0.0);
|
||||
let (p0, p1) = (c + Vec2::from_angle(PI / 4.0) * r, c + Vec2::from_angle(3.0 * PI / 4.0) * r);
|
||||
sk.add_arc(c, p0, p1, [None, None, None], None).unwrap();
|
||||
let areas: Vec<f64> = find_profiles(&sk).iter().map(|p| p.area).collect();
|
||||
let lens = 2.0 * r * r * (d / (2.0 * r)).acos() - d / 2.0 * (4.0 * r * r - d * d).sqrt();
|
||||
// The discs' union once, not each disc whole.
|
||||
assert!((areas.iter().sum::<f64>() - (2.0 * PI * r * r - lens)).abs() < 1e-6, "{areas:?}");
|
||||
|
||||
// Two dividers of a square, each carried by a short line 8e-6 off it that does not reach
|
||||
// their crossing: they still cross at one node, and the square has four quarters.
|
||||
let mut sk = Sketch::new();
|
||||
sk.add_line(v(-20.0, 0.0), v(-10.0, 0.0), None, None, None).unwrap();
|
||||
sk.add_line(v(0.0, -20.0), v(0.0, -10.0), None, None, None).unwrap();
|
||||
rect(&mut sk, [v(-15.0, -15.0), v(15.0, -15.0), v(15.0, 15.0), v(-15.0, 15.0)]);
|
||||
sk.add_line(v(-15.0, 8e-6), v(15.0, 8e-6), None, None, None).unwrap();
|
||||
sk.add_line(v(8e-6, -15.0), v(8e-6, 15.0), None, None, None).unwrap();
|
||||
let areas: Vec<f64> = find_profiles(&sk).iter().map(|p| p.area).collect();
|
||||
assert_eq!(areas.len(), 4, "{areas:?}");
|
||||
assert!((areas.iter().sum::<f64>() - 900.0).abs() < 1e-6, "{areas:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merging_adjacent_regions() {
|
||||
let a =
|
||||
|
||||
@@ -120,3 +120,35 @@ fn oversized_preselection_does_not_silently_project_a_subset() {
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
assert_eq!(h.app.session.selection.len(), 1001);
|
||||
}
|
||||
|
||||
/// #635: a rectangle whose corner is made coincident with a projected edge of the solid, so
|
||||
/// that its bottom side lies on the edge, is still a closed profile: a click in its middle
|
||||
/// selects it, bounded by the rectangle's other three sides and the projected edge under the
|
||||
/// fourth.
|
||||
#[test]
|
||||
fn a_rectangle_put_onto_a_projected_edge_is_still_a_profile_to_click() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
h.app.run("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[40,30]})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
h.app.run("solid.extrude", json!({"distance":20})).unwrap();
|
||||
let sketch = h.app.run("sketch.create", json!({"plane":"XY"})).unwrap()["sketch"].as_u64().unwrap();
|
||||
let projected = h.app.run("sketch.project", json!({"refs":[{"edge":[20,0,0]}]})).unwrap()["links"][0]["curves"][0].as_str().unwrap().to_string();
|
||||
let rectangle = h.app.run("sketch.rectangle.two_point", json!({"p0":[5,10],"p1":[25,25]})).unwrap();
|
||||
let mut sides: Vec<String> = rectangle["curves"].as_array().unwrap().iter().map(|c| c.as_str().unwrap().to_string()).collect();
|
||||
h.app.run("sketch.constraint.coincident", json!({"a":format!("{}.start", sides[0]),"b":projected})).unwrap();
|
||||
// The bottom side lies under the projected edge.
|
||||
sides[0] = projected.clone();
|
||||
// From below, the sketch plane is in front of the solid standing on it.
|
||||
h.call("ui.view", json!({"view":"bottom"}));
|
||||
h.step(&json!({"click":{"sketch":[15,12]}})).unwrap();
|
||||
assert_eq!(h.app.session.selection, vec![Sel::Profile { sketch, index: 0 }]);
|
||||
let state = h.app.session.world_state();
|
||||
let ss = state.sketch(sketch).unwrap();
|
||||
assert_eq!(ss.profiles.len(), 1);
|
||||
let mut bounded_by = ss.profiles[0].outer_curves.clone();
|
||||
bounded_by.sort();
|
||||
sides.sort();
|
||||
assert_eq!(bounded_by, sides);
|
||||
assert!(ss.sketch.curves.iter().any(|curve| curve.id == projected && curve.link.is_some()), "the projected edge stays");
|
||||
}
|
||||
Reference in new issue
Block a user