Text cuts: the 2D prism route takes a font's curves exactly (#1429)

The 2D prism route for text cuts takes a font's curves exactly instead of as polylines.
This commit is contained in:
phanoob authored and GitHub committed 2026-10-15 13:32:02 -04:00
1 parent f0c5bfcdf2
commit 3047aa60cd
6 files changed
+647 -62

No files matched your search

+55 -1
View File
@@ -276,6 +276,60 @@ pub(super) fn segment(e: &mt::Edge, plane: &Plane, tol: f64, work: &mut usize) -
if a.dist(b) > tol && spans.iter().all(|s| line_span(s, a, b, tol, error, work).is_some()) {
return Some(vec![Seg2::Line { a, b }]);
}
arc(&c, &spans, plane, a, b, tol, error, work).or_else(|| free_form(&spans, a, b, tol))
}
/// One Bézier span that is neither a line nor an arc, as the exact free-form segment it is
/// (a font's quadratic or cubic, a conic), its ends `a` and `b`. `None` for several spans (a
/// curve that may wind about more than once), degrees past [`solidcraft_geom::MAX_BEZIER_DEGREE`],
/// rational spans other than conics, and control polygons with no area (a curve doubling back
/// along a line).
fn free_form(spans: &[Vec<H>], a: Vec2, b: Vec2, tol: f64) -> Option<Vec<Seg2>> {
let [span] = spans else { return None };
let pts: Vec<Vec2> = span.iter().map(|p| point(*p)).collect();
let (first, last) = (*pts.first()?, *pts.last()?);
if first.dist(a) > tol || last.dist(b) > tol || first.dist(last) <= tol {
return None;
}
// Twice the area the control polygon spans off the chord: none is a line, maybe doubling back.
let chord = last - first;
let off = pts.iter().map(|p| chord.cross(*p - first).abs()).fold(0., f64::max) / chord.len();
if !(off > tol) {
return None;
}
let rational = span.iter().any(|p| (p[2] - 1.).abs() > 1e-12);
let seg = match (pts.len(), rational) {
(_, false) => solidcraft_geom::bezier(&pts)?,
(3, true) => {
// Weights normalised to 1 at the ends: the middle one is the conic's.
let (w0, w1, w2) = (span[0][2], span[1][2], span[2][2]);
let w = w1 / (w0 * w2).sqrt();
if !(w.is_finite() && w > 1e-9) {
return None;
}
Seg2::Conic { a: first, apex: pts[1], b: last, w }
}
_ => return None,
};
// The exact ends of the edge (the span's agree within the tolerance).
let seg = match seg {
Seg2::Cubic { p1, p2, .. } => Seg2::Cubic { p0: a, p1, p2, p3: b },
Seg2::Conic { apex, w, .. } => Seg2::Conic { a, apex, b, w },
Seg2::Bezier { n, mut p } => {
*p.first_mut()? = a;
*p.get_mut(n as usize)? = b;
Seg2::Bezier { n, p }
}
other => other,
};
Some(vec![seg])
}
/// The spline `c` (its Bézier `spans` in the plane, from `a` to `b`) as a circular arc, when
/// whole-span proofs show it is one.
#[allow(clippy::too_many_arguments)]
fn arc(c: &mt::Curve, spans: &[Vec<H>], plane: &Plane, a: Vec2, b: Vec2, tol: f64, error: f64, work: &mut usize) -> Option<Vec<Seg2>> {
let (t0, t1) = c.range_tuple();
// Samples are only a candidate proposal. Whole-span implicit and oriented
// angular proofs below are required before converting any spline to an arc.
let q = plane.to_local(from_p3(c.subs(t0 + (t1 - t0) * 5. / 16.))) - a;
@@ -290,7 +344,7 @@ pub(super) fn segment(e: &mt::Edge, plane: &Plane, tol: f64, work: &mut usize) -
return None;
}
let mut sweep: f64 = 0.;
for span in &spans {
for span in spans {
let ds = circle_span(span, center, radius, tol, error, 0, work)?;
if sweep != 0. && sweep.signum() != ds.signum() {
return None;