Thanks @SychPL.
This commit is contained in:
1 parent
3eae8c34f6
commit
a2245b2f51
2 files changed
+125
-18
No files matched your search
@@ -639,7 +639,9 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
let grid = Grid::new(&boxes);
|
let grid = Grid::new(&boxes);
|
||||||
let mut cuts: Vec<Vec<f64>> = vec![Vec::new(); shapes.len()];
|
// Each cut's parameter, and whether it is weak: a crossing next to an end that misses one of
|
||||||
|
// the two curves (below), which gives way to any other cut it is merged with.
|
||||||
|
let mut cuts: Vec<Vec<(f64, bool)>> = vec![Vec::new(); shapes.len()];
|
||||||
// End points of other curves lying on each shape.
|
// End points of other curves lying on each shape.
|
||||||
let mut end_cells: std::collections::HashMap<(i64, i64), Vec<usize>> = std::collections::HashMap::new();
|
let mut end_cells: std::collections::HashMap<(i64, i64), Vec<usize>> = std::collections::HashMap::new();
|
||||||
for (k, p) in ends.iter().enumerate() {
|
for (k, p) in ends.iter().enumerate() {
|
||||||
@@ -657,7 +659,7 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
|||||||
&& let Some(t) = sh.param_on(*p, tol)
|
&& let Some(t) = sh.param_on(*p, tol)
|
||||||
&& let Some(cu) = cuts.get_mut(i)
|
&& let Some(cu) = cuts.get_mut(i)
|
||||||
{
|
{
|
||||||
cu.push(t);
|
cu.push((t, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -752,30 +754,35 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
|||||||
for p in pts {
|
for p in pts {
|
||||||
let on_x = x.param_on(p, tol).or_else(|| end_param(x, p, tol));
|
let on_x = x.param_on(p, tol).or_else(|| end_param(x, p, tol));
|
||||||
let on_y = y.param_on(p, tol).or_else(|| end_param(y, p, tol));
|
let on_y = y.param_on(p, tol).or_else(|| end_param(y, p, tol));
|
||||||
// Crossings at (or next to) curve ends are already cuts.
|
// Crossings at (or next to) curve ends are already cuts. Not next to an end of
|
||||||
let near_end = || {
|
// one of the two curves that misses the other: nothing cut the other curve
|
||||||
|
// there, and it would cross without meeting it (a line 3e-5 from the
|
||||||
|
// junction of two curves of a loop, #755). Such a crossing is a weak cut.
|
||||||
|
let on = |sh: &Shape, e: Vec2| sh.param_on(e, tol).is_some() || end_param(sh, e, tol).is_some();
|
||||||
|
let misses = |e: Vec2| (end_param(x, e, tol).is_some() && !on(y, e)) || (end_param(y, e, tol).is_some() && !on(x, e));
|
||||||
|
let near_ends = || {
|
||||||
let (cx, cy) = grid.cell(p);
|
let (cx, cy) = grid.cell(p);
|
||||||
(-1..=1).any(|dx| {
|
(-1..=1)
|
||||||
(-1..=1).any(|dy| {
|
.flat_map(|dx| (-1..=1).map(move |dy| (cx + dx, cy + dy)))
|
||||||
end_cells
|
.flat_map(|c| end_cells.get(&c).map(Vec::as_slice).unwrap_or(&[]))
|
||||||
.get(&(cx + dx, cy + dy))
|
.filter_map(|k| ends.get(*k).copied())
|
||||||
.is_some_and(|v| v.iter().any(|k| ends.get(*k).is_some_and(|e| e.dist(p) < tol * 100.0)))
|
.filter(|e| e.dist(p) < tol * 100.0)
|
||||||
})
|
.fold((false, false), |(cut, weak), e| if misses(e) { (cut, true) } else { (true, weak) })
|
||||||
})
|
|
||||||
};
|
};
|
||||||
if on_x.is_none() || on_y.is_none() || near_end() {
|
let (already, weak) = near_ends();
|
||||||
|
if on_x.is_none() || on_y.is_none() || already {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let (Some(tx), Some(ty)) = (on_x, on_y) {
|
if let (Some(tx), Some(ty)) = (on_x, on_y) {
|
||||||
if x.param_on(p, tol).is_some()
|
if x.param_on(p, tol).is_some()
|
||||||
&& let Some(c) = cuts.get_mut(i)
|
&& let Some(c) = cuts.get_mut(i)
|
||||||
{
|
{
|
||||||
c.push(tx);
|
c.push((tx, weak));
|
||||||
}
|
}
|
||||||
if y.param_on(p, tol).is_some()
|
if y.param_on(p, tol).is_some()
|
||||||
&& let Some(c) = cuts.get_mut(j)
|
&& let Some(c) = cuts.get_mut(j)
|
||||||
{
|
{
|
||||||
c.push(ty);
|
c.push((ty, weak));
|
||||||
}
|
}
|
||||||
if (gi, gj) != (i, j) {
|
if (gi, gj) != (i, j) {
|
||||||
anchors.push(p);
|
anchors.push(p);
|
||||||
@@ -815,21 +822,33 @@ fn find_profiles_bounded(sk: &Sketch, max_edges: usize) -> Vec<Profile> {
|
|||||||
let mut edges: Vec<Edge> = Vec::new();
|
let mut edges: Vec<Edge> = Vec::new();
|
||||||
for (k, (ci, sh)) in shapes.iter().enumerate() {
|
for (k, (ci, sh)) in shapes.iter().enumerate() {
|
||||||
let mut ts = cuts.get(k).cloned().unwrap_or_default();
|
let mut ts = cuts.get(k).cloned().unwrap_or_default();
|
||||||
ts.sort_by(f64::total_cmp);
|
ts.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||||
// Cuts closer than a few tolerances are one cut (tangent touches give near-double roots).
|
// Cuts closer than a few tolerances are one cut (tangent touches give near-double roots).
|
||||||
|
// A weak cut gives way to the other: it only adds a cut where there was none.
|
||||||
let scale = match *sh {
|
let scale = match *sh {
|
||||||
Shape::Line { a, b } => a.dist(b),
|
Shape::Line { a, b } => a.dist(b),
|
||||||
Shape::Round { r, .. } => r,
|
Shape::Round { r, .. } => r,
|
||||||
};
|
};
|
||||||
let close = (tol * 100.0 / scale.max(1e-12)).max(1e-9);
|
let close = (tol * 100.0 / scale.max(1e-12)).max(1e-9);
|
||||||
ts.dedup_by(|a, b| (*a - *b).abs() < close);
|
ts.dedup_by(|a, b| {
|
||||||
|
let same = (a.0 - b.0).abs() < close;
|
||||||
|
if same && b.1 && !a.1 {
|
||||||
|
*b = *a;
|
||||||
|
}
|
||||||
|
same
|
||||||
|
});
|
||||||
if let (Shape::Round { sweep, .. }, Some(&first), Some(&last)) = (*sh, ts.first(), ts.last())
|
if let (Shape::Round { sweep, .. }, Some(&first), Some(&last)) = (*sh, ts.first(), ts.last())
|
||||||
&& sweep >= TAU - 1e-12
|
&& sweep >= TAU - 1e-12
|
||||||
&& ts.len() > 1
|
&& ts.len() > 1
|
||||||
&& (first + TAU - last).abs() < close
|
&& (first.0 + TAU - last.0).abs() < close
|
||||||
{
|
{
|
||||||
ts.pop();
|
if first.1 && !last.1 {
|
||||||
|
ts.remove(0);
|
||||||
|
} else {
|
||||||
|
ts.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
let ts: Vec<f64> = ts.into_iter().map(|(t, _)| t).collect();
|
||||||
let id = sk.curves.get(*ci).map(|c| c.id.clone()).unwrap_or_default();
|
let id = sk.curves.get(*ci).map(|c| c.id.clone()).unwrap_or_default();
|
||||||
let analytic = analytic(*ci);
|
let analytic = analytic(*ci);
|
||||||
let coverage = tags.get(k).copied().flatten().or_else(|| {
|
let coverage = tags.get(k).copied().flatten().or_else(|| {
|
||||||
|
|||||||
@@ -967,3 +967,91 @@ fn a_degree_nine_control_spline_stays_exact_and_closes_a_profile_exactly() {
|
|||||||
high.push(sk.add_point(v(21.0, -2.0), None).unwrap());
|
high.push(sk.add_point(v(21.0, -2.0), None).unwrap());
|
||||||
assert!(sk.add_curve(CurveKind::Spline { pts: high, control: true, degree: 10, periodic: false }, None).is_err());
|
assert!(sk.add_curve(CurveKind::Spline { pts: high, control: true, degree: 10, periodic: false }, None).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #755: a line drawn through a junction of a loop, but 2e-5 off it, still splits the loop. The
|
||||||
|
/// junction lies on the loop's two curves only, so it cuts neither of them where the line
|
||||||
|
/// crosses, and that crossing, next to the junction, was taken for one already cut there: the
|
||||||
|
/// line crossed the loop without meeting it, and the sketch had no profiles at all.
|
||||||
|
#[test]
|
||||||
|
fn profiles_line_passing_next_to_a_junction_splits_the_loop() {
|
||||||
|
let o = v(0.0, 0.0);
|
||||||
|
let at = |r: f64, a: f64| v(r * a.cos(), r * a.sin());
|
||||||
|
// A slot: half circles of radius 1 round (0, 0) and (-5, 0), joined by the lines y = 1 and
|
||||||
|
// y = -1, which meet the arcs tangentially at (0, 1) and (0, -1).
|
||||||
|
let mut sk = Sketch::new();
|
||||||
|
let right = sk.add_arc(o, v(0.0, -1.0), v(0.0, 1.0), [None; 3], None).unwrap();
|
||||||
|
let left = sk.add_arc(v(-5.0, 0.0), v(-5.0, 1.0), v(-5.0, -1.0), [None; 3], None).unwrap();
|
||||||
|
let ends = |i: usize| match sk.curves[i].kind {
|
||||||
|
CurveKind::Arc { a, b, .. } => (a, b),
|
||||||
|
_ => panic!("an arc"),
|
||||||
|
};
|
||||||
|
let ((r0, r1), (l0, l1)) = (ends(right), ends(left));
|
||||||
|
sk.add_line_pts(r1, l0, None).unwrap();
|
||||||
|
sk.add_line_pts(l1, r0, None).unwrap();
|
||||||
|
// Lines from (0, 0) through the junctions to an arc of radius 2. The upper one is turned by
|
||||||
|
// 2e-5 rad, so it passes 2e-5 from (0, 1) and crosses the line y = 1 next to it.
|
||||||
|
let d = 2e-5;
|
||||||
|
let (top, bottom) = (at(2.0, FRAC_PI_2 + d), v(0.0, -2.0));
|
||||||
|
sk.add_line(o, top, None, None, None).unwrap();
|
||||||
|
sk.add_line(o, bottom, None, None, None).unwrap();
|
||||||
|
sk.add_arc(o, bottom, top, [None; 3], None).unwrap();
|
||||||
|
let mut areas: Vec<f64> = find_profiles(&sk).iter().map(|p| p.area).collect();
|
||||||
|
areas.sort_by(f64::total_cmp);
|
||||||
|
// The right half disc, the band round it out to radius 2, and the rest of the slot.
|
||||||
|
let want = [FRAC_PI_2, FRAC_PI_2 * (4.0 - 1.0), 10.0 + FRAC_PI_2];
|
||||||
|
assert_eq!(areas.len(), 3, "{areas:?}");
|
||||||
|
for (got, want) in areas.iter().zip(want) {
|
||||||
|
assert!((got - want).abs() < 1e-3, "{areas:?} against {want}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A line ending 2e-5 from a corner, next to a crossing there: the corner, where the side of the
|
||||||
|
/// square meets the long line, stays a cut of the long line, and the square stays a profile.
|
||||||
|
#[test]
|
||||||
|
fn profiles_crossing_next_to_a_corner_keeps_the_corner() {
|
||||||
|
let mut sk = Sketch::new();
|
||||||
|
let (a, b) = (sk.add_point(v(1.0, 0.0), None).unwrap(), sk.add_point(v(2.0, 0.0), None).unwrap());
|
||||||
|
let (c, d) = (sk.add_point(v(2.0, 1.0), None).unwrap(), sk.add_point(v(1.0, 1.0), None).unwrap());
|
||||||
|
// The square's base is part of a longer line, which its sides end on.
|
||||||
|
sk.add_line(v(0.0, 0.0), v(3.0, 0.0), None, None, None).unwrap();
|
||||||
|
for (p, q) in [(b, c), (c, d), (d, a)] {
|
||||||
|
sk.add_line_pts(p, q, None).unwrap();
|
||||||
|
}
|
||||||
|
// A line hanging across the long one, 2e-5 from the square's corner (1, 0).
|
||||||
|
sk.add_line(v(1.0 - 2e-5, -0.5), v(1.0 - 2e-5, 0.5), None, None, None).unwrap();
|
||||||
|
let areas: Vec<f64> = find_profiles(&sk).iter().map(|p| p.area).collect();
|
||||||
|
assert_eq!(areas.len(), 1, "{areas:?}");
|
||||||
|
assert!(areas.iter().all(|x| (x - 1.0).abs() < 1e-9), "{areas:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A line across a rectangle, and a stray line ending on it 1e-3 from where it crosses the
|
||||||
|
/// rectangle's side: both the crossing and that end cut the long line, and the rectangle is
|
||||||
|
/// split into its two halves.
|
||||||
|
#[test]
|
||||||
|
fn profiles_crossing_next_to_a_stray_end_still_splits() {
|
||||||
|
let mut sk = Sketch::new();
|
||||||
|
rect(&mut sk, [v(0.0, -1.0), v(1.0, -1.0), v(1.0, 1.0), v(0.0, 1.0)]);
|
||||||
|
sk.add_line(v(-1.0, 0.0), v(2.0, 0.0), None, None, None).unwrap();
|
||||||
|
sk.add_line(v(0.00099998, 0.0000099), v(0.3, 0.6), None, None, None).unwrap();
|
||||||
|
let areas: Vec<f64> = find_profiles(&sk).iter().map(|p| p.area).collect();
|
||||||
|
assert_eq!(areas.len(), 2, "{areas:?}");
|
||||||
|
assert!(areas.iter().all(|x| (x - 1.0).abs() < 1e-9), "{areas:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A line whose own end overshoots a crossing line by 2e-5, with a rectangle standing on it 1e-3
|
||||||
|
/// further on: the crossing next to that end gives way to the rectangle's corner, which still
|
||||||
|
/// cuts the line, and the rectangle stays a profile.
|
||||||
|
#[test]
|
||||||
|
fn profiles_crossing_next_to_an_overshooting_end_keeps_the_corner_beside_it() {
|
||||||
|
let mut sk = Sketch::new();
|
||||||
|
sk.add_line(v(-2e-5, 0.0), v(3.0, 0.0), None, None, None).unwrap();
|
||||||
|
sk.add_line(v(0.0, -0.5), v(0.0, 0.5), None, None, None).unwrap();
|
||||||
|
let x = 0.00099999;
|
||||||
|
let p = [v(x, 0.0000099), v(x, 1.0), v(2.0, 1.0), v(2.0, 0.0)].map(|q| sk.add_point(q, None).unwrap());
|
||||||
|
for w in p.windows(2) {
|
||||||
|
sk.add_line_pts(w[0], w[1], None).unwrap();
|
||||||
|
}
|
||||||
|
let areas: Vec<f64> = find_profiles(&sk).iter().map(|p| p.area).collect();
|
||||||
|
assert_eq!(areas.len(), 1, "{areas:?}");
|
||||||
|
assert!(areas.iter().all(|a| (a - (2.0 - x)).abs() < 1e-4), "{areas:?}");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user