fix: text cut into a large part stays on the exact 2D route (#1903)
Thanks @NOWN-DEV.
This commit is contained in:
1 parent
cd9db57366
commit
9a4d936b7b
4 files changed
+74
-4
No files matched your search
@@ -1754,7 +1754,9 @@ fn fill_void_holes(st: &ModelState, r: &Region2, op: Operation, targets: &[Strin
|
||||
let size = bodies.iter().map(|b| b.body.size()).fold(0.0, f64::max);
|
||||
let margin = (size * 1e-4).max(1e-4);
|
||||
let n = plane.normal();
|
||||
let inside_any = |p: Vec3| meshes.iter().any(|m| m.contains(p));
|
||||
// Many probes on one mesh each: indexed, the same answers as `Mesh::contains`.
|
||||
let inside: Vec<_> = meshes.iter().map(|m| m.inside_index()).collect();
|
||||
let inside_any = |p: Vec3| inside.iter().any(|m| m.contains(p));
|
||||
let want = op == Operation::Join;
|
||||
let mut holes = Vec::with_capacity(r.holes.len());
|
||||
for h in &r.holes {
|
||||
@@ -1816,7 +1818,9 @@ fn grow_profile_for_coplanar(st: &ModelState, r: &Region2, op: Operation, target
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let inside_any = |p: Vec3| meshes.iter().any(|m| m.contains(p));
|
||||
// Many probes on one mesh each: indexed, the same answers as `Mesh::contains`.
|
||||
let inside: Vec<_> = meshes.iter().map(|m| m.inside_index()).collect();
|
||||
let inside_any = |p: Vec3| inside.iter().any(|m| m.contains(p));
|
||||
let mut grow = vec![0.0; segs.len()];
|
||||
let mut any = false;
|
||||
for (i, seg) in segs.iter().enumerate() {
|
||||
@@ -1912,7 +1916,9 @@ fn extend_for_coplanar(st: &ModelState, plane: &Plane, r: &Region2, lo: f64, hi:
|
||||
let meshes: Vec<Arc<Mesh>> = bodies.iter().map(|b| b.mesh()).collect();
|
||||
let samples = region_samples(r);
|
||||
let n = plane.normal();
|
||||
let inside_any = |p: Vec3| meshes.iter().any(|m| m.contains(p));
|
||||
// Many probes on one mesh each: indexed, the same answers as `Mesh::contains`.
|
||||
let inside: Vec<_> = meshes.iter().map(|m| m.inside_index()).collect();
|
||||
let inside_any = |p: Vec3| inside.iter().any(|m| m.contains(p));
|
||||
// Heights of the targets' faces parallel to the profile plane.
|
||||
let tol = (size * 1e-7).max(1e-9);
|
||||
let levels: Vec<f64> = meshes
|
||||
|
||||
@@ -1251,3 +1251,29 @@ fn a_line_of_text_cut_again_deeper_is_one_exact_boolean() {
|
||||
let again = cut(&mut s, 2.0);
|
||||
assert!(rel(again, first) < 1e-3, "{first} {again}");
|
||||
}
|
||||
|
||||
/// Text cut through a large plate (2 m) is decided in 2D in one exact boolean, as it is in a
|
||||
/// small one. The 2D route's tolerances grow with the part: a font's curve that is merely near a
|
||||
/// circle was read as an arc ending a micron off the next curve, so the letter's outline did not
|
||||
/// close, and where the curves of a 3 come close the arrangement left a sliver that closed up to
|
||||
/// nothing. The cut went to the B-rep boolean instead, which took tens of seconds and moved the
|
||||
/// tool to find a result at all.
|
||||
#[test]
|
||||
fn text_cut_through_a_large_plate_is_one_exact_boolean() {
|
||||
let taken = |w: f64, h: f64, at: [f64; 2]| {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length":w,"width":h,"height":1.5,"corner":[0,0,0]}));
|
||||
let before = volume(&mut s);
|
||||
run(&mut s, "sketch.create", json!({"plane":"XY","offset":1.5}));
|
||||
run(&mut s, "sketch.text", json!({"text":"PART-23","at":at,"height":7}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let id = run(&mut s, "solid.extrude", json!({"distance":-2,"operation":"cut"}))["feature"].as_u64().unwrap();
|
||||
let r = s.model.result(id).unwrap();
|
||||
assert!(r.error.is_none() && r.boolean_diagnostics.events.is_empty(), "{w} x {h}: {r:?}");
|
||||
before - volume(&mut s)
|
||||
};
|
||||
let (small, large) = (taken(200.0, 120.0, [60.0, 40.0]), taken(2000.0, 1200.0, [860.0, 125.0]));
|
||||
// The letters right through the plate, their counters left as pieces of their own: the same
|
||||
// material in either plate (measured on meshes, which are coarser on the larger one).
|
||||
assert!(small > 10.0 && rel(large, small) < 5e-2, "{small} {large}");
|
||||
}
|
||||
@@ -1252,6 +1252,15 @@ fn exact_again(out: Section, sources: &[Seg2], tol: f64) -> Section {
|
||||
let mut loop_tags = Vec::new();
|
||||
for (l, lp) in std::iter::once(®.outer).chain(®.holes).enumerate() {
|
||||
let (segs, ts) = exact_loop(lp, tags.get(l).map(Vec::as_slice).unwrap_or(&[]), sources, near);
|
||||
// A sliver the arrangement found where a curve's arcs nearly touch another part of
|
||||
// the outline (at the waist of a 3) closes up to nothing on the exact curves: it
|
||||
// encloses nothing and would make edges with no length, so it is left out.
|
||||
if segs.iter().map(Seg2::length).sum::<f64>() <= near {
|
||||
if l == 0 {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
loops.push(Loop2 { segs });
|
||||
loop_tags.push(ts);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ use solidcraft_geom::{Plane, Seg2, Vec2};
|
||||
use truck_modeling::{self as mt, BoundedCurve, Invertible, ParametricCurve};
|
||||
|
||||
const MAX_DEGREE: usize = 32;
|
||||
/// How far an arc read off a curve may end from the curve's end (mm): half the gap a profile
|
||||
/// loop may have between segments and still close (`crate::build`).
|
||||
const ARC_END_TOL: f64 = 5e-7;
|
||||
const MAX_CONTROLS: usize = 4096;
|
||||
type H = [f64; 3];
|
||||
|
||||
@@ -353,7 +356,10 @@ fn arc(c: &mt::Curve, spans: &[Vec<H>], plane: &Plane, a: Vec2, b: Vec2, tol: f6
|
||||
}
|
||||
let start = (a - center).angle();
|
||||
let end = center + Vec2::new((start + sweep).cos(), (start + sweep).sin()) * radius;
|
||||
if end.dist(b) + error > tol {
|
||||
// The arc ends where the curve does, as closely as a profile loop must close: within `tol`
|
||||
// only (which grows with the model), a curve that is merely near a circle (a font's) would
|
||||
// come back as an arc ending off the next segment's start, and the loop would not close.
|
||||
if end.dist(b) + error > tol.min(ARC_END_TOL) {
|
||||
return None;
|
||||
}
|
||||
let closed = a.dist(b) < tol;
|
||||
@@ -471,6 +477,29 @@ mod tests {
|
||||
assert!(segment(&arc, &Plane::XY, 1e-5, &mut 0).is_none());
|
||||
}
|
||||
|
||||
/// A curve read as an arc ends where the curve does, however large the tolerance (it grows
|
||||
/// with the part): a font's quadratic that is within a micron of a circle, not on one, is
|
||||
/// kept as the curve it is. Read as an arc whose end is a micron off, the outline of a letter
|
||||
/// cut into a 2 m plate did not close.
|
||||
#[test]
|
||||
fn a_curve_near_a_circle_is_not_an_arc_ending_off_it() {
|
||||
// A parabola within a micron of the circle through its ends and middle.
|
||||
let e = edge(&[0., 2., 0.], &[0., 0., 0.2], &[1.], 2);
|
||||
let (a, b) = (Vec2::new(0., 0.), Vec2::new(2., 0.2));
|
||||
assert!(from_p3(e.back().point()).dist(solidcraft_geom::Vec3::new(2., 0.2, 0.)) < 1e-12);
|
||||
for tol in [1e-5, 2e-3] {
|
||||
let segs = segment(&e, &Plane::XY, tol, &mut 2_000_000).unwrap();
|
||||
let (first, last) = (segs.first().unwrap(), segs.last().unwrap());
|
||||
assert!(first.start().dist(a) < 1e-9 && last.end().dist(b) < 1e-9, "{tol}: {segs:?}");
|
||||
}
|
||||
// A true arc is still one at either tolerance.
|
||||
let (p, q) = (mt::Vertex::new(mt::Point3::new(10., 0., 0.)), mt::Vertex::new(mt::Point3::new(0., 10., 0.)));
|
||||
let arc = builder::circle_arc(&p, &q, mt::Point3::new(10. / 2f64.sqrt(), 10. / 2f64.sqrt(), 0.));
|
||||
for tol in [1e-5, 2e-3] {
|
||||
assert!(matches!(segment(&arc, &Plane::XY, tol, &mut 2_000_000).unwrap()[..], [Seg2::Arc { .. }]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discontinuous_and_multiple_winding_curves_decline() {
|
||||
let edge_from = |knots: Vec<f64>, points: Vec<mt::Vector4>| {
|
||||
|
||||
Reference in new issue
Block a user