Skip to main content

brep_kernel/meshing/watertight_tessellation/
chord_budget.rs

1use super::*;
2use crate::Vec4;
3
4const HARD_SPANS: usize = 65_536;
5
6/// Failure to certify an edge's requested chord tolerance.
7#[derive(Clone, Debug, PartialEq)]
8pub struct EdgeChordError {
9    pub edge_id: u64,
10    pub requested: f64,
11    pub reason: EdgeChordFailure,
12}
13
14#[derive(Clone, Debug, PartialEq)]
15pub enum EdgeChordFailure {
16    InvalidTolerance,
17    UnsupportedCurve,
18    EndpointMismatch,
19    DegenerateEdge,
20    SafetyCap,
21    ParameterResolution,
22    Evaluation(String),
23}
24
25impl std::fmt::Display for EdgeChordError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "edge chord tolerance: edge {} requested {:e}: {:?}", self.edge_id, self.requested, self.reason)
28    }
29}
30impl std::error::Error for EdgeChordError {}
31
32/// A checked tessellation either meets its edge chord budget or names the
33/// failing edge. Errors from face triangulation retain their original detail.
34#[derive(Clone, Debug, PartialEq)]
35pub enum TessellationError {
36    EdgeChord(EdgeChordError),
37    Geometry(String),
38}
39impl From<String> for TessellationError {
40    fn from(value: String) -> Self { Self::Geometry(value) }
41}
42impl From<EdgeChordError> for TessellationError {
43    fn from(value: EdgeChordError) -> Self { Self::EdgeChord(value) }
44}
45impl std::fmt::Display for TessellationError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self { Self::EdgeChord(e) => e.fmt(f), Self::Geometry(e) => f.write_str(e) }
48    }
49}
50impl std::error::Error for TessellationError {}
51
52pub(super) fn enabled() -> bool {
53    crate::switch!("BREP_TESS_CHORD_BUDGET", true)
54}
55
56fn certificate_spans(curve: &crate::NurbsCurve) -> Option<Vec<Vec<Vec4>>> {
57    let p = curve.degree;
58    let points = &curve.control_points;
59    let knots = &curve.knots;
60    if points.len() < p + 1 || knots.len() != points.len() + p + 1 { return None; }
61    let (first, last) = (*knots.first()?, *knots.last()?);
62    // Match the evaluator's first/last non-empty span on over-clamped
63    // vectors. Extra end knots have corresponding inactive control points;
64    // removing those pairs changes no basis function on the curve's domain.
65    let head = knots.iter().take_while(|&&k| k == first).count().saturating_sub(p + 1);
66    let tail = knots.iter().rev().take_while(|&&k| k == last).count().saturating_sub(p + 1);
67    if head + tail > points.len() - p - 1 { return None; }
68    crate::curve::bezier_decompose_raw(&knots[head..knots.len() - tail],
69        &points[head..points.len() - tail], p,
70        Vec4 { x: 0., y: 0., z: 0., w: 0. },
71        |a, b, alpha| a.scale(alpha).add(b.scale(1. - alpha)))
72        .or_else(|| separated_spans(&knots[head..knots.len() - tail],
73            &points[head..points.len() - tail], p))
74}
75
76// A full-multiplicity interior knot separates independent clamped pieces.
77// Preserve both endpoint poles: the bound must include both one-sided limits,
78// rather than averaging away a discontinuity that the requested chord cannot meet.
79#[cold]
80#[inline(never)]
81fn separated_spans(knots: &[f64], points: &[Vec4], p: usize) -> Option<Vec<Vec<Vec4>>> {
82    if p == 0 || points.len() < p + 1 || knots.len() != points.len() + p + 1
83        || knots.iter().any(|k| !k.is_finite()) || knots.windows(2).any(|k| k[1] < k[0]) {
84        return None;
85    }
86    let decompose = |knots: &[f64], points: &[Vec4]| crate::curve::bezier_decompose_raw(
87        knots, points, p, Vec4 { x: 0., y: 0., z: 0., w: 0. },
88        |a, b, alpha| a.scale(alpha).add(b.scale(1. - alpha)));
89    let mut spans = Vec::new();
90    let (mut knot_begin, mut point_begin) = (0, 0);
91    let mut i = p + 1;
92    while i < knots.len() - p - 1 {
93        let mut end = i + 1;
94        while end < knots.len() && knots[end] == knots[i] { end += 1; }
95        let multiplicity = end - i;
96        if multiplicity > p + 1 { return None; }
97        if multiplicity == p + 1 {
98            let point_end = point_begin + end - knot_begin - p - 1;
99            spans.extend(decompose(&knots[knot_begin..end], points.get(point_begin..point_end)?)?);
100            knot_begin = i;
101            point_begin = point_end;
102        }
103        i = end;
104    }
105    if knot_begin == 0 { return None; }
106    spans.extend(decompose(&knots[knot_begin..], points.get(point_begin..)?)?);
107    Some(spans)
108}
109
110fn control_bound<const N: usize>(points: &[Vec4], lo: f64, hi: f64, a: Vec3, b: Vec3, tolerance: f64) -> f64 {
111    let zero = Vec4 { x: 0., y: 0., z: 0., w: 0. };
112    let mut work = [zero; N];
113    let mut net = [zero; N];
114    let count = points.len();
115    work[..count].copy_from_slice(points);
116    net[0] = work[0];
117    for n in (1..count).rev() {
118        for i in 0..n { work[i] = work[i].scale(1. - hi).add(work[i + 1].scale(hi)); }
119        net[count - n] = work[0];
120    }
121    if lo > 0. {
122        let t = lo / hi;
123        work[..count].copy_from_slice(&net[..count]);
124        for n in (1..count).rev() {
125            for i in 0..n { work[i] = work[i].scale(1. - t).add(work[i + 1].scale(t)); }
126            net[n - 1] = work[n - 1];
127        }
128    }
129    tight_bound::<N>(&net[..count], a, b, tolerance, 3)
130}
131
132fn tight_bound<const N: usize>(points: &[Vec4], a: Vec3, b: Vec3, tolerance: f64, depth: usize) -> f64 {
133    let distance = |p: Vec4| deflection_to_chord(Vec3::new(p.x / p.w, p.y / p.w, p.z / p.w), a, b);
134    let mut bound = 0.0f64;
135    for &p in points {
136        let d = distance(p);
137        if !d.is_finite() { return f64::INFINITY; }
138        bound = bound.max(d);
139    }
140    if bound <= tolerance || depth == 0 { return bound; }
141    // Tighten the certificate against the SAME chord before adding mesh
142    // vertices. A quadratic arc's tangent pole can be twice as far from the
143    // chord as the arc; refining the mesh for that loose hull wastes samples.
144    let zero = Vec4 { x: 0., y: 0., z: 0., w: 0. };
145    let mut work = [zero; N];
146    let mut left = [zero; N];
147    let mut right = [zero; N];
148    let count = points.len();
149    work[..count].copy_from_slice(points);
150    left[0] = work[0];
151    right[count - 1] = work[count - 1];
152    for n in (1..count).rev() {
153        for i in 0..n { work[i] = work[i].scale(0.5).add(work[i + 1].scale(0.5)); }
154        left[count - n] = work[0];
155        right[n - 1] = work[n - 1];
156    }
157    // This midpoint is on the curve, so a measured miss already proves that
158    // the chord must split; further certificate work cannot rescue it.
159    if distance(work[0]) > tolerance { return bound; }
160    tight_bound::<N>(&left[..count], a, b, tolerance, depth - 1)
161        .max(tight_bound::<N>(&right[..count], a, b, tolerance, depth - 1))
162}
163
164/// Certify the full rational curve, rather than a finite set of sag probes.
165/// Positive homogeneous weights place each Bezier arc inside its control hull;
166/// distance to a segment is convex, so the largest control-point distance bounds
167/// the entire arc. Subdivision tightens that bound without omitting knot spans.
168/// Endpoints are the shared topology vertices, including their displacement from
169/// the curve. Unsupported representations fail closed.
170pub fn sample_edge_polyline_checked(
171    edge: &EdgeRecord,
172    start: Vec3,
173    end: Vec3,
174    chord_tolerance: f64,
175) -> Result<Vec<(f64, Vec3)>, EdgeChordError> {
176    refine(edge, start, end, chord_tolerance, &[0.0, 1.0], HARD_SPANS)
177}
178
179// A positive-weight linear curve lies on its two Cartesian poles' segment.
180// Pairing that segment with the topology chord bounds every point by the
181// larger endpoint displacement, without allocating a Bezier decomposition.
182// Other representations and additional chart stations use the general path.
183fn full_line_certified(edge: &EdgeRecord, start: Vec3, end: Vec3, tolerance: f64, seeds: &[f64]) -> bool {
184    let curve = &edge.curve;
185    if curve.degree != 1 || curve.control_points.len() != 2 || curve.knots.len() != 4
186        || !curve.is_validated() || seeds != [0., 1.] { return false; }
187    let k = &curve.knots;
188    if k.iter().any(|v| !v.is_finite()) || k[0] != k[1] || k[2] != k[3]
189        || k[2] - k[0] <= crate::tolerance::FP_FLOOR
190        || !(edge.t1 - edge.t0).is_finite() { return false; }
191    let forward = edge.t0 == k[0] && edge.t1 == k[2];
192    if !forward && !(edge.t0 == k[2] && edge.t1 == k[0]) { return false; }
193    if curve.control_points.iter().any(|p| p.w <= crate::tolerance::FP_FLOOR
194        || ![p.x, p.y, p.z, p.w].iter().all(|v| v.is_finite()))
195        || ![start.x, start.y, start.z, end.x, end.y, end.z].iter().all(|v| v.is_finite()) {
196        return false;
197    }
198    let poles = &curve.control_points;
199    let a = poles[usize::from(!forward)].point().unwrap();
200    let b = poles[usize::from(forward)].point().unwrap();
201    let scale = [a.x.abs(), a.y.abs(), a.z.abs(), b.x.abs(), b.y.abs(), b.z.abs(),
202        start.x.abs(), start.y.abs(), start.z.abs(), end.x.abs(), end.y.abs(), end.z.abs()]
203        .into_iter().fold(0.0f64, f64::max);
204    let roundoff = 128. * f64::EPSILON * scale;
205    if !roundoff.is_finite() || tolerance <= roundoff || tolerance < 16. * f64::MIN_POSITIVE.sqrt() {
206        return false;
207    }
208    let budget = tolerance - roundoff;
209    a.sub(start).length() <= budget && b.sub(end).length() <= budget
210}
211
212pub(super) fn refine(
213    edge: &EdgeRecord,
214    start: Vec3,
215    end: Vec3,
216    tolerance: f64,
217    seeds: &[f64],
218    cap: usize,
219) -> Result<Vec<(f64, Vec3)>, EdgeChordError> {
220    let fail = |reason| EdgeChordError { edge_id: edge.id, requested: tolerance, reason };
221    if !tolerance.is_finite() || tolerance <= 0.0 {
222        return Err(fail(EdgeChordFailure::InvalidTolerance));
223    }
224    if seeds.len().saturating_sub(1) > cap {
225        return Err(fail(EdgeChordFailure::SafetyCap));
226    }
227    if edge.curve.degree > 64 || edge.curve.control_points.len() > HARD_SPANS {
228        return Err(fail(EdgeChordFailure::SafetyCap));
229    }
230    if full_line_certified(edge, start, end, tolerance, seeds) {
231        return Ok(vec![(0., start), (1., end)]);
232    }
233    let spans = certificate_spans(&edge.curve)
234        .ok_or_else(|| fail(EdgeChordFailure::UnsupportedCurve))?;
235    let knots: Vec<_> = edge.curve.knots.windows(2).filter(|p| p[1] > p[0])
236        .map(|p| (p[0], p[1])).collect();
237    if spans.len() != knots.len() || spans.is_empty()
238        || !edge.t0.is_finite() || !edge.t1.is_finite()
239        || !(edge.t1 - edge.t0).is_finite()
240        || spans.iter().flatten().any(|p| p.w <= 0.0 || ![p.x, p.y, p.z, p.w].iter().all(|v| v.is_finite()))
241        || ![start.x, start.y, start.z, end.x, end.y, end.z].iter().all(|v| v.is_finite())
242    {
243        return Err(fail(EdgeChordFailure::UnsupportedCurve));
244    }
245    let parameter = |f: f64| {
246        if f == 0. { edge.t0 } else if f == 1. { edge.t1 }
247        else { edge.t0 + f * (edge.t1 - edge.t0) }
248    };
249    let evaluate = |f: f64| edge.curve.evaluate(parameter(f))
250        .map_err(|e| fail(EdgeChordFailure::Evaluation(e)));
251    let coordinate_scale = spans.iter().flatten()
252        .flat_map(|p| [p.x.abs() / p.w, p.y.abs() / p.w, p.z.abs() / p.w])
253        .chain([start.x.abs(), start.y.abs(), start.z.abs(), end.x.abs(), end.y.abs(), end.z.abs()])
254        .fold(0.0f64, f64::max);
255    // Reserve arithmetic slack for homogeneous subdivision and projection.
256    // A request below representable coordinate accuracy must fail explicitly,
257    // rather than treating rounded zero distances as a tolerance certificate.
258    let roundoff = 64. * f64::EPSILON * (edge.curve.degree + 1) as f64 * coordinate_scale;
259    if !roundoff.is_finite() || tolerance <= roundoff || tolerance < 16. * f64::MIN_POSITIVE.sqrt() {
260        return Err(fail(EdgeChordFailure::ParameterResolution));
261    }
262    let budget = tolerance - roundoff;
263    // A validated clamped curve interpolates its active end poles, which
264    // decomposition already supplied (including over-clamped ends). Reuse
265    // those values for endpoint checks without running the basis engine.
266    // Keep first-use validation and the evaluator's tiny-span refusal path.
267    let endpoint = |t: f64| {
268        let last = knots.len() - 1;
269        let pole = if !edge.curve.is_validated() { None }
270            else if t == knots[0].0 && knots[0].1 - knots[0].0 > crate::tolerance::FP_FLOOR {
271                spans[0].first()
272            } else if t == knots[last].1 && knots[last].1 - knots[last].0 > crate::tolerance::FP_FLOOR {
273                spans.last().unwrap().last()
274            } else { None };
275        pole.map_or_else(|| edge.curve.evaluate(t), |p| p.point())
276            .map_err(|e| fail(EdgeChordFailure::Evaluation(e)))
277    };
278    if endpoint(edge.t0)?.sub(start).length() > tolerance || endpoint(edge.t1)?.sub(end).length() > tolerance {
279        return Err(fail(EdgeChordFailure::EndpointMismatch));
280    }
281    let mut output = vec![(0.0, start)];
282    let mut pending = Vec::new();
283    let mut b = end;
284    for pair in seeds.windows(2).rev() {
285        let a = if pair[0] == 0.0 { start } else { evaluate(pair[0])? };
286        pending.push((pair[0], pair[1], a, b));
287        b = a;
288    }
289    while let Some((f0, f1, a, b)) = pending.pop() {
290        // Match the curve evaluator's clamped domain, including trims a few
291        // ulps beyond an end knot after topology operations.
292        let domain = (knots[0].0, knots.last().unwrap().1);
293        let t0 = parameter(f0).clamp(domain.0, domain.1);
294        let t1 = parameter(f1).clamp(domain.0, domain.1);
295        let mut bound = 0.0f64;
296        let first = knots.partition_point(|(_, hi)| *hi < t0.min(t1));
297        for ((lo, hi), points) in knots[first..].iter().zip(&spans[first..]) {
298            if *lo > t0.max(t1) { break; }
299            let from = t0.min(t1).max(*lo);
300            let to = t0.max(t1).min(*hi);
301            if to < from { continue; }
302            let u = ((from - lo) / (hi - lo)).clamp(0.0, 1.0);
303            let v = ((to - lo) / (hi - lo)).clamp(0.0, 1.0);
304            let distance = if points.len() <= 4 {
305                control_bound::<4>(points, u, v, a, b, budget)
306            } else {
307                control_bound::<65>(points, u, v, a, b, budget)
308            };
309            if !distance.is_finite() { return Err(fail(EdgeChordFailure::UnsupportedCurve)); }
310            bound = bound.max(distance);
311        }
312        if bound <= budget {
313            output.push((f1, b));
314            continue;
315        }
316        if edge.degenerate { return Err(fail(EdgeChordFailure::DegenerateEdge)); }
317        if output.len() + pending.len() >= cap {
318            return Err(fail(EdgeChordFailure::SafetyCap));
319        }
320        let middle = f0 + (f1 - f0) * 0.5;
321        if middle <= f0 || middle >= f1 || parameter(middle) == t0
322            || parameter(middle) == t1 {
323            return Err(fail(EdgeChordFailure::ParameterResolution));
324        }
325        let m = evaluate(middle)?;
326        pending.push((middle, f1, m, b));
327        pending.push((f0, middle, a, m));
328    }
329    Ok(output)
330}
331
332pub(super) fn refine_shared(edge: &EdgeRecord, start: Vec3, end: Vec3, tolerance: f64, seeds: &[f64])
333    -> Result<Vec<(f64, Vec3)>, EdgeChordError>
334{
335    refine(edge, start, end, tolerance, seeds, HARD_SPANS)
336}