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

+443 -50
View File
@@ -17,6 +17,10 @@ use crate::{BoolOp, KernelError, Result};
#[path = "prism_lineage.rs"]
mod lineage;
#[cfg(test)]
#[path = "prism_spline_tests.rs"]
mod spline_tests;
const MAX_PLANAR_SEGMENTS: usize = 8192;
const MAX_PLANAR_EVENTS: usize = 131_072;
const MAX_PLANAR_PAIRS: usize = 16_000_000;
@@ -216,10 +220,13 @@ pub(crate) struct Tag {
pub far: Option<usize>,
pub start: Option<usize>,
pub end: Option<usize>,
/// Inside [`boolean2d_tagged`] only: the free-form segment (by number) the arc this tags
/// was fitted to, so the result's pieces of it get the exact curve back.
pub free: Option<usize>,
}
/// A segment nothing is known of.
const BLANK: Tag = Tag { operand: 0, wall: None, edge: None, far: None, start: None, end: None };
const BLANK: Tag = Tag { operand: 0, wall: None, edge: None, far: None, start: None, end: None, free: None };
impl Tag {
/// The segment run the other way.
@@ -475,6 +482,7 @@ impl Index {
far: None,
start: self.vertices.get(&e.front().id()).copied(),
end: self.vertices.get(&e.back().id()).copied(),
free: None,
}
}
@@ -1055,8 +1063,307 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option<Vec<R
}
/// [`boolean2d`] of tagged sections: each segment of the result tagged as the segment of `a` or
/// `b` it is (a piece of).
/// `b` it is (a piece of). Free-form segments (a font's curves) take part as arcs within
/// [`PLANAR_FIT_TOL`]; each piece of one in the result is the exact curve again, ending where
/// it exactly meets the segment next to it (see [`exact_again`]).
fn boolean2d_tagged(a: &Section, b: &Section, op: BoolOp, tol: f64) -> Option<Section> {
let line_arc = |s: &Section| s.regions.iter().all(Region2::is_line_arc);
if line_arc(a) && line_arc(b) {
return boolean2d_arcs(a, b, op, tol);
}
let mut sources = Vec::new();
let (fa, fb) = (fitted(a, &mut sources, PLANAR_FIT_TOL)?, fitted(b, &mut sources, PLANAR_FIT_TOL)?);
let out = boolean2d_arcs(&fa, &fb, op, tol)?;
Some(exact_again(out, &sources, tol))
}
/// `s` with each free-form segment replaced by lines and arcs within `fit`, tagged with its
/// number in `sources` (where it is put).
fn fitted(s: &Section, sources: &mut Vec<Seg2>, fit: f64) -> Option<Section> {
let mut out = Section::default();
let mut pieces = 0usize;
for (r, reg) in s.regions.iter().enumerate() {
let mut loops = Vec::new();
let mut tags = Vec::new();
for (l, lp) in std::iter::once(&reg.outer).chain(&reg.holes).enumerate() {
let (mut segs, mut ts) = (Vec::new(), Vec::new());
for (i, seg) in lp.segs.iter().enumerate() {
let tag = s.tag(r, l, i);
if seg.is_line_or_arc() {
segs.push(*seg);
ts.push(tag);
continue;
}
let arcs = seg.fit_arcs(fit)?;
pieces = pieces.checked_add(arcs.len()).filter(|n| *n <= MAX_PLANAR_SEGMENTS)?;
let n = arcs.len();
ts.extend((0..n).map(|k| Tag { free: Some(sources.len()), ..tag.piece(k, n) }));
segs.extend(arcs);
sources.push(*seg);
}
loops.push(Loop2 { segs });
tags.push(ts);
}
let mut loops = loops.into_iter();
out.regions.push(Region2 { outer: loops.next()?, holes: loops.collect() });
out.tags.push(tags);
}
Some(out)
}
/// A loop's piece in [`exact_again`]: a run of arcs fitted to free-form segment `src`, standing
/// for its exact curve from parameter `t0` to `t1` (`t1 < t0`: run backwards), from `a` to `b`;
/// or a line or arc as it is.
#[derive(Clone, Copy)]
enum Piece {
Exact { src: usize, t0: f64, t1: f64, a: Vec2, b: Vec2, tag: Tag },
Plain(Seg2, Tag),
}
impl Piece {
fn end(&self) -> Vec2 {
match self {
Piece::Exact { b, .. } => *b,
Piece::Plain(s, _) => s.end(),
}
}
/// The piece ending at `at` (an exact curve: at its parameter `t`, when given).
fn set_end(&mut self, t: Option<f64>, at: Vec2) {
match self {
Piece::Exact { t1, b, .. } => {
if let Some(t) = t {
*t1 = t;
}
*b = at;
}
Piece::Plain(s, _) => *s = seg_with_ends(s, s.start(), at),
}
}
/// The piece starting at `at` (an exact curve: at its parameter `t`, when given).
fn set_start(&mut self, t: Option<f64>, at: Vec2) {
match self {
Piece::Exact { t0, a, .. } => {
if let Some(t) = t {
*t0 = t;
}
*a = at;
}
Piece::Plain(s, _) => *s = seg_with_ends(s, at, s.end()),
}
}
}
/// `s` running from `a` to `b` instead (points on it or a hair off it): a line between them, an
/// arc of the same circle, a free-form segment with those end control points.
fn seg_with_ends(s: &Seg2, a: Vec2, b: Vec2) -> Seg2 {
match *s {
Seg2::Line { .. } => Seg2::Line { a, b },
Seg2::Arc { center, radius, start, sweep } => {
let new_start = (a - center).angle();
let end = start + sweep;
// The same way round, through the same part of the circle.
let shift = |x: f64, near: f64| x + ((near - x) / std::f64::consts::TAU).round() * std::f64::consts::TAU;
let s0 = shift(new_start, start);
let s1 = shift((b - center).angle(), end);
Seg2::Arc { center, radius, start: s0, sweep: s1 - s0 }
}
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 } => {
if let Some(x) = p.first_mut() {
*x = a;
}
if let Some(x) = p.get_mut(n as usize) {
*x = b;
}
Seg2::Bezier { n, p }
}
}
}
/// The exact piece of `src` from parameter `t0` to `t1` (either way round), from `a` to `b`.
fn trimmed(src: &Seg2, t0: f64, t1: f64, a: Vec2, b: Vec2) -> Seg2 {
let (lo, hi) = (t0.min(t1), t0.max(t1));
let mut s = *src;
if lo > 1e-12 {
s = s.split_at(lo).1;
}
if hi < 1.0 - 1e-12 {
// Splitting a conic renormalises its weights, which moves its parameters: the second
// split is found on the piece.
let at = s.closest_param(src.point_at(hi));
if at < 1.0 - 1e-12 {
s = s.split_at(at).0;
}
}
if t0 <= t1 { seg_with_ends(&s, a, b) } else { seg_with_ends(&s.reversed(), a, b) }
}
/// Where curve `p` near its parameter `t` meets `q` near its parameter `u`, by Newton's method:
/// their parameters there, or `None` when the crossing doesn't settle within `near` of `p(t)`.
fn meet(p: &Seg2, t: f64, q: &Seg2, u: f64, near: f64) -> Option<(f64, f64)> {
let (mut t, mut u) = (t, u);
let start = p.point_at(t);
let scale = 1.0 + start.x.abs().max(start.y.abs());
for _ in 0..50 {
let f = p.point_at(t) - q.point_at(u);
if f.len() < 1e-14 * scale {
break;
}
// P(t) - Q(u) = f; the step solves [P'(t), -Q'(u)] (dt, du) = -f.
let (dp, dq) = (p.derivative(t), q.derivative(u));
let det = -dp.cross(dq);
if !(det.abs() > 1e-300 && det.is_finite()) {
return None;
}
t = (t + f.cross(dq) / det).clamp(0.0, 1.0);
u = (u + f.cross(dp) / det).clamp(0.0, 1.0);
}
let (x, y) = (p.point_at(t), q.point_at(u));
(x.dist(y) < 1e-9 * scale && x.dist(start) < near).then_some((t, u))
}
/// The whole line or circle a line or arc lies on (as a segment reaching past it), what an exact
/// crossing with a curve is looked for on.
fn carrier(s: &Seg2) -> Option<Seg2> {
match *s {
Seg2::Line { a, b } => Some(Seg2::Line { a: a - (b - a), b: b + (b - a) }),
Seg2::Arc { center, radius, start, sweep } => {
let turn = sweep.signum() * std::f64::consts::PI;
Some(Seg2::Arc { center, radius, start: start + sweep * 0.5 - turn, sweep: 2.0 * turn })
}
_ => None,
}
}
/// The exact curves back in the result of a boolean of [`fitted`] sections: each run of arcs
/// fitted to one free-form segment becomes that segment's exact piece, and where a run ends on
/// another segment (a cut across the curve) both end at their exact crossing. Where that
/// crossing can't be found they end where the arrangement put them, within
/// [`PLANAR_FIT_TOL`] of it.
fn exact_again(out: Section, sources: &[Seg2], tol: f64) -> Section {
let near = (PLANAR_FIT_TOL + tol) * 10.0;
let mut res = Section::default();
for (r, reg) in out.regions.iter().enumerate() {
let tags = out.region_tags(r);
let mut loops = Vec::new();
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);
loops.push(Loop2 { segs });
loop_tags.push(ts);
}
let mut loops = loops.into_iter();
let Some(outer) = loops.next() else { continue };
res.regions.push(Region2 { outer, holes: loops.collect() });
res.tags.push(loop_tags);
}
res
}
/// One loop of [`exact_again`]: its segments and their tags.
fn exact_loop(lp: &Loop2, tags: &[Tag], sources: &[Seg2], near: f64) -> (Vec<Seg2>, Vec<Tag>) {
let n = lp.segs.len();
let tag = |i: usize| Tag { free: None, ..tags.get(i).copied().unwrap_or(BLANK) };
let free = |i: usize| tags.get(i).and_then(|t| t.free);
let plain = || (lp.segs.clone(), (0..n).map(tag).collect::<Vec<_>>());
// Start at the first segment of a run (a loop that is one run stays as it is).
let Some(first) = (0..n).find(|&i| free(i).is_none() || free(i) != free((i + n - 1) % n)) else { return plain() };
let mut pieces: Vec<Piece> = Vec::new();
let mut k = 0;
while k < n {
let i = (first + k) % n;
let Some(seg) = lp.segs.get(i) else { return plain() };
let Some((f, src)) = free(i).and_then(|f| sources.get(f).map(|s| (f, s))) else {
pieces.push(Piece::Plain(*seg, tag(i)));
k += 1;
continue;
};
let mut j = k;
while j + 1 < n && free((first + j + 1) % n) == Some(f) {
j += 1;
}
let Some(last) = lp.segs.get((first + j) % n) else { return plain() };
let at = |p: Vec2| {
if p.dist(src.start()) < 1e-9 {
0.0
} else if p.dist(src.end()) < 1e-9 {
1.0
} else {
src.closest_param(p)
}
};
let (t0, t1) = (at(seg.start()), at(last.end()));
if (t1 - t0).abs() < 1e-9 {
// A sliver of the curve: its arcs as they are.
for m in k..=j {
let q = (first + m) % n;
pieces.push(Piece::Plain(*lp.segs.get(q).unwrap_or(seg), tag(q)));
}
} else {
let t = Tag { end: tag((first + j) % n).end, ..tag(i) };
pieces.push(Piece::Exact { src: f, t0, t1, a: src.point_at(t0), b: src.point_at(t1), tag: t });
}
k = j + 1;
}
// Each joint at an exact curve: both sides end at the same point, the exact crossing where
// the curve is cut, its own end point where it ends.
let m = pieces.len();
let inner = |t: f64| t > 1e-9 && t < 1.0 - 1e-9;
for i in 0..m {
let (Some(p), Some(q)) = (pieces.get(i).copied(), pieces.get((i + 1) % m).copied()) else { continue };
let joint = p.end();
let src = |f: usize| sources.get(f);
let found = match (p, q) {
(Piece::Exact { src: f, t1, .. }, Piece::Exact { src: g, t0, .. }) if f != g && (inner(t1) || inner(t0)) => {
src(f).zip(src(g)).and_then(|(x, y)| meet(x, t1, y, t0, near).map(|(a, b)| (Some(a), Some(b), x.point_at(a))))
}
(Piece::Exact { src: f, t1, .. }, Piece::Plain(s, _)) if inner(t1) => {
src(f).zip(carrier(&s)).and_then(|(x, c)| meet(x, t1, &c, c.closest_param(joint), near).map(|(a, _)| (Some(a), None, x.point_at(a))))
}
(Piece::Plain(s, _), Piece::Exact { src: f, t0, .. }) if inner(t0) => {
src(f).zip(carrier(&s)).and_then(|(x, c)| meet(x, t0, &c, c.closest_param(joint), near).map(|(a, _)| (None, Some(a), x.point_at(a))))
}
_ => None,
};
// No crossing to refine (or none found): both end where the curve does.
let found = found.or(match (p, q) {
(Piece::Exact { b, .. }, _) => Some((None, None, b)),
(_, Piece::Exact { a, .. }) => Some((None, None, a)),
_ => None,
});
let Some((x, y, at)) = found else { continue };
if let Some(p) = pieces.get_mut(i) {
p.set_end(x, at);
}
if let Some(q) = pieces.get_mut((i + 1) % m) {
q.set_start(y, at);
}
}
let mut segs = Vec::with_capacity(m);
let mut ts = Vec::with_capacity(m);
for p in pieces {
match p {
Piece::Exact { src, t0, t1, a, b, tag } => match sources.get(src) {
Some(s) => {
segs.push(trimmed(s, t0, t1, a, b));
ts.push(tag);
}
None => return plain(),
},
Piece::Plain(s, t) => {
segs.push(s);
ts.push(t);
}
}
}
(segs, ts)
}
/// The boolean of sections of lines and arcs alone ([`boolean2d_tagged`]).
fn boolean2d_arcs(a: &Section, b: &Section, op: BoolOp, tol: f64) -> Option<Section> {
let count =
|rs: &[Region2]| rs.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).fold(0usize, |n, lp| n.saturating_add(lp.segs.len()));
let (na, nb) = (count(&a.regions), count(&b.regions));
@@ -1347,9 +1654,6 @@ fn simple_loops(lp: Loop2, tags: Vec<Tag>, near: f64) -> Vec<(Loop2, Vec<Tag>)>
}
fn split_seg(seg: &Seg2, pts: &[Vec2], tol: f64) -> Option<Vec<Seg2>> {
if !matches!(seg, Seg2::Line { .. } | Seg2::Arc { .. }) {
return None;
}
let mut ts: Vec<f64> = pts
.iter()
.filter_map(|p| {
@@ -1372,7 +1676,7 @@ fn split_seg(seg: &Seg2, pts: &[Vec2], tol: f64) -> Option<Vec<Seg2>> {
out.push(match *seg {
Seg2::Line { .. } => Seg2::Line { a: seg.point_at(t0), b: seg.point_at(t1) },
Seg2::Arc { center, radius, start, sweep } => Seg2::Arc { center, radius, start: start + sweep * t0, sweep: sweep * (t1 - t0) },
_ => return None,
_ => trimmed(seg, t0, t1, seg.point_at(t0), seg.point_at(t1)),
});
t0 = t1;
}
@@ -1510,6 +1814,23 @@ fn wall_key(s: &Seg2) -> Option<[u64; 6]> {
match s {
Seg2::Line { a, b } => Some([0, bits(a.x)?, bits(a.y)?, bits(b.x)?, bits(b.y)?, 0]),
Seg2::Arc { center, radius, start, sweep } => Some([1, bits(center.x)?, bits(center.y)?, bits(*radius)?, bits(*start)?, bits(*sweep)?]),
// Its ends and a point between them: free-form segments with the same ones are the same
// curve as far as a wall over it goes.
_ => {
let (a, m, b) = (s.start(), s.point_at(0.5), s.end());
let h = |x: f64, y: f64| bits(x).zip(bits(y)).map(|(x, y)| x ^ y.rotate_left(17));
Some([2, h(a.x, a.y)?, h(m.x, m.y)?, h(b.x, b.y)?, bits(m.x)?, bits(m.y)?])
}
}
}
/// A free-form segment as the control points of the cubic Bézier it is (a quadratic raised to
/// a cubic); `None` for those no cubic is exactly (rational conics, higher degrees).
fn cubic_of(s: &Seg2) -> Option<[Vec2; 4]> {
match *s {
Seg2::Cubic { p0, p1, p2, p3 } => Some([p0, p1, p2, p3]),
Seg2::Conic { a, apex, b, w } if (w - 1.0).abs() < 1e-12 => Some([a, a + (apex - a) * (2.0 / 3.0), b + (apex - b) * (2.0 / 3.0), b]),
Seg2::Bezier { n: 3, p } => Some([p[0], p[1], p[2], p[3]]),
_ => None,
}
}
@@ -1551,12 +1872,47 @@ fn sew_connected_sections(
pts.push(sg.end());
}
}
let at = |u: Vec2, z: f64| plane.to_world(u) + d * z;
let edge = |sg: &Seg2, z: f64| -> EdgeSpec {
match sg {
Seg2::Arc { .. } => EdgeSpec::Arc { a: at(sg.start(), z), b: at(sg.end(), z), mid: at(sg.point_at(0.5), z) },
_ => EdgeSpec::Line { a: at(sg.start(), z), b: at(sg.end(), z) },
// Ends within `tol` of each other are one point. The 2D boolean closes its loops within
// `tol`, not exactly: where a font's nearly round curve was read as an arc, the arc ends a
// few hundredths of a micron from the curve next to it. Sewing joins only ends at the same
// point, so such a loop would not close.
let cell = |u: Vec2| ((u.x / tol).floor() as i64, (u.y / tol).floor() as i64);
let mut ends: Vec<Vec2> = Vec::new();
let mut grid: std::collections::HashMap<(i64, i64), Vec<usize>> = std::collections::HashMap::new();
let weld = |ends: &[Vec2], grid: &std::collections::HashMap<(i64, i64), Vec<usize>>, u: Vec2| -> Option<Vec2> {
let (cx, cy) = cell(u);
(cx - 1..=cx + 1)
.flat_map(|x| (cy - 1..=cy + 1).map(move |y| (x, y)))
.filter_map(|c| grid.get(&c))
.flatten()
.filter_map(|&i| ends.get(i).copied())
.find(|e| e.dist(u) <= tol)
};
for &u in &pts {
if weld(&ends, &grid, u).is_none() {
grid.entry(cell(u)).or_default().push(ends.len());
ends.push(u);
}
}
// A piece of a free-form curve whose ends weld into one point (a font's curve cut a hair from
// its end) is no edge and has no wall: the pieces on either side meet there. (Lines and arcs
// are left as they were: such a piece of one fails the sewing, as before.)
let short = |s: &Seg2| {
!s.is_line_or_arc()
&& s.length() <= 2.0 * tol
&& matches!((weld(&ends, &grid, s.start()), weld(&ends, &grid, s.end())), (Some(p), Some(q)) if p == q)
};
let at = |u: Vec2, z: f64| plane.to_world(u) + d * z;
let welded = |u: Vec2, z: f64| at(weld(&ends, &grid, u).unwrap_or(u), z);
let edge = |sg: &Seg2, z: f64| -> Option<EdgeSpec> {
Some(match sg {
Seg2::Line { .. } => EdgeSpec::Line { a: welded(sg.start(), z), b: welded(sg.end(), z) },
Seg2::Arc { .. } => EdgeSpec::Arc { a: welded(sg.start(), z), b: welded(sg.end(), z), mid: at(sg.point_at(0.5), z) },
_ => {
let [p0, p1, p2, p3] = cubic_of(sg)?;
EdgeSpec::Chain { ctrl: vec![welded(p0, z), at(p1, z), at(p2, z), welded(p3, z)].into() }
}
})
};
// Exact oriented generated segments alone define a run. No support fitting,
// proximity matching, or old face lineage is used here.
@@ -1574,6 +1930,9 @@ fn sew_connected_sections(
let pieces = split_seg(sg0, &pts, tol)?;
let (n, tag) = (pieces.len(), slab.tag(r, l, si));
for (k, sg) in pieces.into_iter().enumerate() {
if short(&sg) {
continue;
}
remaining = remaining.checked_sub(1)?;
let key = wall_key(&sg)?;
let prior = if coalesce { last.get(&key).copied() } else { None };
@@ -1622,20 +1981,22 @@ fn sew_connected_sections(
let (z0, z1) = (levels[first], levels[end]);
let (a, b) = (sg.start(), sg.end());
let mut lp3 = Vec::with_capacity(2 + 2 * (end - first));
lp3.push(edge(&sg, z0));
lp3.push(edge(&sg, z0)?);
// Keep station vertices at both ends: neighboring stepped walls/caps
// must share complete edges rather than create T-junctions.
for k in first..end {
lp3.push(EdgeSpec::Line { a: at(b, levels[k]), b: at(b, levels[k + 1]) });
lp3.push(EdgeSpec::Line { a: welded(b, levels[k]), b: welded(b, levels[k + 1]) });
}
lp3.push(edge(&sg.reversed(), z1));
lp3.push(edge(&sg.reversed(), z1)?);
for k in (first..end).rev() {
lp3.push(EdgeSpec::Line { a: at(a, levels[k + 1]), b: at(a, levels[k]) });
lp3.push(EdgeSpec::Line { a: welded(a, levels[k + 1]), b: welded(a, levels[k]) });
}
let outward = plane.dir_to_world(sg.derivative(0.5)).cross(d).normalized()?;
let surface = match sg {
Seg2::Line { .. } => SurfSpec::Plane,
Seg2::Arc { .. } => SurfSpec::Extruded { a: at(a, z0), b: at(b, z0), mid: at(sg.point_at(0.5), z0), dir: d * (z1 - z0) },
_ => SurfSpec::Plane,
// A free-form wall: the lines along `d` joining the curve at both levels.
_ => SurfSpec::Ruled { a: edge(&sg, z0)?, b: edge(&sg, z1)? },
};
faces.push(FaceSpec { loops: vec![lp3], surface, probe: Some((at(sg.point_at(0.5), (z0 + z1) / 2.0), outward)) });
// Axis-aligned generated lines have a common oriented support frame
@@ -1668,10 +2029,10 @@ fn sew_connected_sections(
for lp in std::iter::once(&r.outer).chain(&r.holes) {
let mut segs: Vec<Seg2> = Vec::new();
for sg in &lp.segs {
segs.extend(split_seg(sg, &pts, tol)?);
segs.extend(split_seg(sg, &pts, tol)?.into_iter().filter(|s| !short(s)));
}
let segs: Vec<Seg2> = if *up { segs } else { segs.iter().rev().map(Seg2::reversed).collect() };
loops.push(segs.iter().map(|sg| edge(sg, *z)).collect::<Vec<_>>());
loops.push(segs.iter().map(|sg| edge(sg, *z)).collect::<Option<Vec<_>>>()?);
}
faces.push(FaceSpec { loops, surface: SurfSpec::Plane, probe: None });
sources.push(None);
@@ -1700,24 +2061,9 @@ fn simple_prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<
fn simple_prism_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2]) -> Option<Result<Option<Body>>> {
let size = a.size().max(b.size());
let tol = (size * 1e-7).max(1e-9) * 10.0;
// Candidate directions: a's planar face normals (each line once).
let mut dirs: Vec<Vec3> = Vec::new();
let mut remaining_direction_faces = MAX_PLANAR_SEGMENTS * 2;
for f in a.solid.face_iter() {
remaining_direction_faces = remaining_direction_faces.checked_sub(1)?;
if let mt::Surface::Plane(p) = f.oriented_surface() {
let n = p.normal();
if let Some(n) = Vec3::new(n.x, n.y, n.z).normalized()
&& !dirs.iter().any(|m| m.dot(n).abs() > 1.0 - 1e-9)
{
dirs.push(if n.dot(Vec3::new(1e-3, 1e-6, 1.0)) < 0.0 { -n } else { n });
if dirs.len() == 8 {
break;
}
}
}
}
let (d, plane, pa, pb) = dirs.into_iter().take(8).find_map(|d| {
// Candidate directions (each line once, pointing up).
let dirs: Vec<Vec3> = directions(a, b)?.into_iter().map(|n| if n.dot(Vec3::new(1e-3, 1e-6, 1.0)) < 0.0 { -n } else { n }).collect();
let (d, plane, pa, pb) = dirs.into_iter().find_map(|d| {
let plane = Plane::from_normal(Vec3::ZERO, d)?;
let pa = prism_tagged(a, d, &plane, tol, ia)?;
let pb = prism_tagged(b, d, &plane, tol, ib)?;
@@ -1743,10 +2089,16 @@ fn simple_prism_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2
if regions.is_empty() {
return Some(Ok(None));
}
let (u, x, y) = (area(regions), area(&pa.section.regions), area(&pb.section.regions));
let alike = |v: f64| (u - v).abs() <= tol * (1.0 + u);
// A tool that misses its target (or an intersection that is one operand whole) leaves it
// as it is: the B-rep boolean reuses that operand itself, where this would rebuild it.
if matches!(op, BoolOp::Cut | BoolOp::Intersect) && (same_area(u, x, tol) || (op == BoolOp::Intersect && same_area(u, y, tol))) {
return None;
}
let (lo, hi) = if stacked {
// Different sections (a boss hanging over a plate's edge): sewn slab by slab.
let (u, x, y) = (area(regions), area(&pa.section.regions), area(&pb.section.regions));
if (u - x).abs() > tol * (1.0 + u) || (u - y).abs() > tol * (1.0 + u) {
if !alike(x) || !alike(y) {
return sew_stack(&plane, d, &pa, &pb, tol, &history).map(|r| r.map(Some));
}
// One outline end to end: also slab by slab, so the history follows each operand's
@@ -1784,6 +2136,46 @@ fn simple_prism_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2
Some(Ok(Some(history.attach(last.0.clone(), &[last]))))
}
/// The directions to try the operands as prisms along: the first eight plane directions of the
/// target (in its face order), then those of the tool's caps (a pair of opposite plane faces)
/// that the target has further on. A target cut many times over (a line of text, a group of
/// letters at a time) has more plane directions than eight, its walls' among them: in face
/// order its caps' direction can come too late. `None` past the face budget.
fn directions(a: &Body, b: &Body) -> Option<Vec<Vec3>> {
if a.face_count().saturating_add(b.face_count()) > MAX_PLANAR_SEGMENTS * 2 {
return None;
}
let normals = |x: &Body| -> Vec<Vec3> {
x.solid
.face_iter()
.filter_map(|f| match f.oriented_surface() {
mt::Surface::Plane(p) => {
let n = p.normal();
Vec3::new(n.x, n.y, n.z).normalized()
}
_ => None,
})
.collect()
};
let mut target: Vec<Vec3> = Vec::new();
for n in normals(a) {
if !target.iter().any(|m| m.dot(n).abs() > 1.0 - 1e-9) {
target.push(n);
}
}
let tool = normals(b);
let capped = |d: &Vec3| tool.iter().any(|n| n.dot(*d) > 1.0 - 1e-9) && tool.iter().any(|n| n.dot(*d) < -1.0 + 1e-9);
let mut dirs: Vec<Vec3> = target.iter().take(8).copied().collect();
dirs.extend(target.iter().skip(8).filter(|d| capped(d)).take(8));
Some(dirs)
}
/// Whether a boolean of sections left one of area `v` as it was (area `u` after): the same to
/// within rounding, as the very same outline gives (a change is at least a sliver `tol` wide).
fn same_area(u: f64, v: f64, tol: f64) -> bool {
(u - v).abs() <= 1e-9 * u.abs().max(v.abs()) + tol * tol
}
/// A stepped extrusion (case floor plus walls), recovered from its cap events.
/// Downward caps add a section above their level; upward caps remove one.
#[cfg(test)]
@@ -1886,19 +2278,9 @@ fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2], comm
ps.iter().find(|p| p.lo <= lo + tol && p.hi >= hi - tol).map(|p| p.section.clone()).unwrap_or_default()
}
let tol = (a.size().max(b.size()) * 1e-7).max(1e-9) * 10.0;
let mut dirs: Vec<Vec3> = Vec::new();
for f in a.solid.face_iter() {
if let mt::Surface::Plane(p) = f.oriented_surface() {
let n = p.normal();
if let Some(n) = Vec3::new(n.x, n.y, n.z).normalized()
&& !dirs.iter().any(|m| m.dot(n).abs() > 1.0 - 1e-9)
{
dirs.push(n);
}
}
}
let dirs = directions(a, b)?;
let ends = |ps: &[Prism]| ps.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p.lo), hi.max(p.hi)));
let (d, plane, pa, pb) = dirs.into_iter().take(8).find_map(|d| {
let (d, plane, pa, pb) = dirs.into_iter().find_map(|d| {
let plane = Plane::from_normal(Vec3::ZERO, d)?;
let pa = stack_tagged(a, d, &plane, tol, ia)?;
let pb = stack_tagged(b, d, &plane, tol, ib)?;
@@ -1919,10 +2301,15 @@ fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2], comm
let mut slabs = Vec::new();
let mut work = 0usize;
let mut output_edges = 0usize;
let area =
|rs: &[Region2]| rs.iter().map(|r| r.outer.signed_area().abs() - r.holes.iter().map(|h| h.signed_area().abs()).sum::<f64>()).sum::<f64>();
// Whether some slab's section came out other than the target's (else the tool missed it).
let mut changed = false;
let edges = |regions: &[Region2]| regions.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).map(|l| l.segs.len()).sum::<usize>();
for w in levels.windows(2) {
let (lo, hi) = (*w.first()?, *w.get(1)?);
let (x, y) = (section(&pa, lo, hi, tol), section(&pb, lo, hi, tol));
let before = area(&x.regions);
if op == BoolOp::Cut {
let count = edges(&x.regions).saturating_add(edges(&y.regions));
work = work.saturating_add(count.saturating_mul(count));
@@ -1936,6 +2323,7 @@ fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2], comm
(_, true, _) => x,
_ => boolean2d_tagged(&x, &y, op, tol)?,
};
changed |= !same_area(area(&s.regions), before, tol);
if op == BoolOp::Cut {
output_edges = output_edges.saturating_add(edges(&s.regions));
if output_edges > 8192 {
@@ -1947,6 +2335,11 @@ fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2], comm
if slabs.iter().all(|s| s.regions.is_empty()) {
return Some(Ok(None));
}
// A cut that takes nothing leaves the target as it is: the B-rep boolean reuses it itself,
// where this would rebuild it.
if op == BoolOp::Cut && !changed {
return None;
}
let history = lineage::Context::new(op, [a, b], [ia.get(), ib.get()], plane, tol, None);
let coalesce = op == BoolOp::Cut
&& !a.regularization_protected