fix: text cut into a large part stays on the exact 2D route (#1903)

Thanks @NOWN-DEV.
This commit is contained in:
NOWN-DEV authored and GitHub committed 2026-10-17 04:08:12 -04:00
1 parent cd9db57366
commit 9a4d936b7b
4 files changed
+74 -4

No files matched your search

+9 -3
View File
@@ -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}");
}
+9
View File
@@ -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(&reg.outer).chain(&reg.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);
}
+30 -1
View File
@@ -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>| {