Skip to main content

brep_kernel/props/mass_properties/
polygons.rs

1use super::*;
2
3pub fn trim_polygons(face: &FaceRecord) -> Result<Vec<Vec<[f64; 2]>>, String> {
4    let mut polygons = Vec::new();
5    for loop_record in &face.loops {
6        let mut polygon = Vec::new();
7        for coedge in &loop_record.coedges {
8            sample_coedge(coedge, [0.0, 0.0], &mut polygon)?;
9        }
10        polygons.push(polygon);
11    }
12    Ok(polygons)
13}
14
15/// Rebuild a sphere cap bounded by a varying full-period contact rim and the
16/// collapsed opposite-winding rim at one parameter pole.  In the flat UV
17/// plane both individual loops have zero enclosed area; joined across one cut
18/// of the periodic cover they form the actual cap polygon.
19pub(super) fn singly_periodic_sphere_cap_polygons(
20    face: &FaceRecord,
21    closed_u: bool,
22    closed_v: bool,
23    domain: [f64; 4],
24) -> Result<Option<Vec<Vec<[f64; 2]>>>, String> {
25    if !matches!(
26        face.surface.analytic(),
27        Some(crate::AnalyticSurface::Sphere { .. })
28    ) || (closed_u, closed_v) != (true, false)
29        || face.loops.len() != 2
30    {
31        return Ok(None);
32    }
33    let [u0, u1, v0, v1] = domain;
34    let period = u1 - u0;
35    let v_span = v1 - v0;
36    struct Rim {
37        points: Vec<[f64; 2]>,
38        winding: f64,
39        vmin: f64,
40        vmax: f64,
41    }
42    let mut rims = Vec::with_capacity(2);
43    for loop_record in &face.loops {
44        let mut points = Vec::new();
45        for coedge in &loop_record.coedges {
46            let [start, end] = coedge.pcurve.domain()?;
47            for index in 0..=64 {
48                let parameter = start + (end - start) * index as f64 / 64.0;
49                let point = coedge.pcurve.evaluate(parameter)?;
50                points.push([point.x, point.y]);
51            }
52        }
53        if points.len() < 2 {
54            return Ok(None);
55        }
56        let winding = points.last().unwrap()[0] - points[0][0];
57        let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
58        for point in &points {
59            vmin = vmin.min(point[1]);
60            vmax = vmax.max(point[1]);
61        }
62        rims.push(Rim {
63            points,
64            winding,
65            vmin,
66            vmax,
67        });
68    }
69    if rims
70        .iter()
71        .any(|rim| (rim.winding.abs() - period).abs() > 0.05 * period)
72        || rims[0].winding * rims[1].winding >= 0.0
73    {
74        return Ok(None);
75    }
76    let flat = |rim: &Rim| rim.vmax - rim.vmin <= 1e-6 * v_span;
77    let pole = match (flat(&rims[0]), flat(&rims[1])) {
78        (true, false) => &rims[0],
79        (false, true) => &rims[1],
80        _ => return Ok(None),
81    };
82    let pole_v = 0.5 * (pole.vmin + pole.vmax);
83    if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
84        return Ok(None);
85    }
86
87    let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
88        (&rims[0], &rims[1])
89    } else if rims[1].vmax <= rims[0].vmin {
90        (&rims[1], &rims[0])
91    } else {
92        return Ok(None);
93    };
94    let mut lower_points = lower.points.clone();
95    if lower.winding < 0.0 {
96        lower_points.reverse();
97    }
98    let mut upper_points = upper.points.clone();
99    if upper.winding > 0.0 {
100        upper_points.reverse();
101    }
102    let shift = ((lower_points[0][0] - upper_points.last().unwrap()[0]) / period).round() * period;
103    for point in &mut upper_points {
104        point[0] += shift;
105    }
106    lower_points.extend(upper_points);
107    Ok(Some(vec![lower_points]))
108}
109
110/// Trim polygons with implicit torus seams unwrapped onto the covering plane.
111/// Returns `(polygons, unwrapped)`; `unwrapped` is true iff any loop crossed an
112/// implicit seam (and therefore carries out-of-domain parameters). Only attempts
113/// the unwrap on a doubly-periodic surface — single-periodic seams the raw
114/// integrator already handles are left in-domain.
115pub(super) fn trim_polygons_unwrapped(
116    face: &FaceRecord,
117    closed_u: bool,
118    closed_v: bool,
119    u_period: f64,
120    v_period: f64,
121) -> Result<(Vec<Vec<[f64; 2]>>, bool), String> {
122    let mut polygons = Vec::new();
123    let mut unwrapped = false;
124    for loop_record in &face.loops {
125        let offsets = crate::topology::loop_seam_offsets(
126            &loop_record.coedges,
127            closed_u,
128            closed_v,
129            u_period,
130            v_period,
131        )?;
132        let mut polygon = Vec::new();
133        for (index, coedge) in loop_record.coedges.iter().enumerate() {
134            let offset = offsets[index];
135            if offset[0] != 0.0 || offset[1] != 0.0 {
136                unwrapped = true;
137            }
138            sample_coedge(coedge, offset, &mut polygon)?;
139        }
140        polygons.push(polygon);
141    }
142    Ok((polygons, unwrapped))
143}
144
145/// A singly-periodic (cylinder/cone) wall whose seam is IMPLICIT: the closed
146/// wall is bounded by two full-wrap rim loops (iso-parametric lines at constant
147/// cross-level) with NO seam edge connecting them — the two-rim-circles form
148/// OCC/STEP emit for a full lateral face. This is the single-periodic analogue
149/// of [`biperiodic_band_range`]: each rim is a degenerate iso line in parameter
150/// space, so the raw trim integrates the band between them to ~zero (only the
151/// in-band holes survive — a full cylinder wall collapses to a ~1000x-too-small
152/// sliver, ABC 00000056 solid 2). We restrict to the unambiguous case where the
153/// two rims sit at the cross-domain EXTREMES: the material region is then the
154/// WHOLE surface (full periodic span × full cross span) minus any hole/notch
155/// loops, with no between-rims-vs-complement choice to get wrong. Replacing the
156/// two rim lines with a single full-domain band rectangle lets the existing
157/// winding integrator fill the wall while the remaining loops subtract as holes.
158///
159/// Returns the rebuilt polygon set (band rectangle first, then the unwrapped
160/// non-rim loops), or None when `face` is not a clean two-extreme-rim band —
161/// every other periodic face keeps its existing integration path unchanged.
162pub(super) fn singly_periodic_wall_polygons(
163    face: &FaceRecord,
164    closed_u: bool,
165    closed_v: bool,
166    domain: [f64; 4],
167) -> Result<Option<Vec<Vec<[f64; 2]>>>, String> {
168    // Exactly one periodic direction (the doubly-periodic case is handled by
169    // `biperiodic_band_integral` upstream).
170    if closed_u == closed_v {
171        return Ok(None);
172    }
173    let [u0, u1, v0, v1] = domain;
174    let p_is_u = closed_u;
175    let period = if p_is_u { u1 - u0 } else { v1 - v0 };
176    if !(period > 0.0) {
177        return Ok(None);
178    }
179    let (q_dom_lo, q_dom_hi) = if p_is_u { (v0, v1) } else { (u0, u1) };
180    let q_extent = (q_dom_hi - q_dom_lo).abs();
181    if !(q_extent > 0.0) {
182        return Ok(None);
183    }
184    // (periodic, cross) coordinate split.
185    let coord = |p: [f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
186
187    let mut rim_indices: Vec<usize> = Vec::new();
188    let mut rim_levels: Vec<f64> = Vec::new();
189    for (index, loop_record) in face.loops.iter().enumerate() {
190        // Sample the loop's pcurves in traversal order.
191        let mut points: Vec<[f64; 2]> = Vec::new();
192        for coedge in &loop_record.coedges {
193            let [d0, d1] = coedge.pcurve.domain()?;
194            let samples = 16;
195            for k in 0..=samples {
196                let t = d0 + (d1 - d0) * k as f64 / samples as f64;
197                let p = coedge.pcurve.evaluate(t)?;
198                points.push([p.x, p.y]);
199            }
200        }
201        if points.len() < 2 {
202            return Ok(None);
203        }
204        let (mut qmin, mut qmax) = (f64::INFINITY, f64::NEG_INFINITY);
205        let mut net = 0.0;
206        for pair in points.windows(2) {
207            let (p0, q0) = coord(pair[0]);
208            let (p1, _q1) = coord(pair[1]);
209            qmin = qmin.min(q0);
210            qmax = qmax.max(q0);
211            let mut delta = p1 - p0;
212            if delta > 0.5 * period {
213                delta -= period;
214            } else if delta < -0.5 * period {
215                delta += period;
216            }
217            net += delta;
218        }
219        if let Some(last) = points.last() {
220            let (_p, q) = coord(*last);
221            qmin = qmin.min(q);
222            qmax = qmax.max(q);
223        }
224        // A rim: nets a full period in the periodic direction and stays on a
225        // near-constant cross-level (a true iso line).
226        let is_rim = net.abs() > 0.75 * period && (qmax - qmin) <= 0.02 * q_extent;
227        if is_rim {
228            rim_indices.push(index);
229            rim_levels.push(0.5 * (qmin + qmax));
230        }
231    }
232    if rim_levels.len() != 2 {
233        return Ok(None);
234    }
235    let (q_lo, q_hi) = if rim_levels[0] <= rim_levels[1] {
236        (rim_levels[0], rim_levels[1])
237    } else {
238        (rim_levels[1], rim_levels[0])
239    };
240    // Require the rims at the cross-domain extremes: material is then the whole
241    // wall (no between-rims/complement ambiguity). A sub-band (rims interior to
242    // the cross domain) is declined here — the existing path keeps handling it.
243    let tol = 0.02 * q_extent;
244    if (q_lo - q_dom_lo).abs() > tol || (q_hi - q_dom_hi).abs() > tol {
245        return Ok(None);
246    }
247    if (q_hi - q_lo) <= 0.5 * q_extent {
248        return Ok(None);
249    }
250
251    // Band rectangle over the full periodic span × the rim cross-levels.
252    let (r_ulo, r_uhi, r_vlo, r_vhi) = if p_is_u {
253        (u0, u1, q_lo, q_hi)
254    } else {
255        (q_lo, q_hi, v0, v1)
256    };
257    let rectangle = vec![
258        [r_ulo, r_vlo],
259        [r_uhi, r_vlo],
260        [r_uhi, r_vhi],
261        [r_ulo, r_vhi],
262    ];
263    let mut polygons: Vec<Vec<[f64; 2]>> = vec![rectangle];
264    // Keep the non-rim loops (holes / notches), unwrapping any implicit-seam hop
265    // so a seam-straddling hole subtracts as one region.
266    for (index, loop_record) in face.loops.iter().enumerate() {
267        if rim_indices.contains(&index) {
268            continue;
269        }
270        let offsets = crate::topology::loop_seam_offsets(
271            &loop_record.coedges,
272            closed_u,
273            closed_v,
274            u1 - u0,
275            v1 - v0,
276        )?;
277        let mut polygon = Vec::new();
278        for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
279            sample_coedge(coedge, offsets[coedge_index], &mut polygon)?;
280        }
281        polygons.push(polygon);
282    }
283    Ok(Some(polygons))
284}
285
286/// A DOUBLY-periodic (torus) face whose trim is a full-wrap BAND bounded by a
287/// constant-cross-level seam RIM (at a cross-domain extreme) and a single-valued
288/// WAVY CUT. The raw winding integrator collapses it to a sliver (the rim is a
289/// zero-area iso line and the cut's own polygon encloses only the thin strip it
290/// bounds), so — mirroring [`singly_periodic_wall_polygons`] for the singly-
291/// periodic wall — rebuild the material region as ONE simple (u,v) polygon:
292/// the wavy cut as one boundary, the flat rim iso-line (at the OPPOSITE cross
293/// extreme, which is the same physical seam circle) as the other, closed by the
294/// two private wrap-seam edges. The winding integrator then fills the band.
295///
296/// Returns None for anything that is not this exact signature (see
297/// [`crate::topology::analyze_doubly_periodic_seam_band`]) — every other face
298/// keeps its existing integration path bit-for-bit.
299pub(super) fn doubly_periodic_seam_band_polygons(
300    face: &FaceRecord,
301    domain: [f64; 4],
302) -> Result<Option<Vec<Vec<[f64; 2]>>>, String> {
303    let (closed_u, closed_v) = face.surface.closed_directions()?;
304    if !(closed_u && closed_v) {
305        return Ok(None);
306    }
307    // Sample each loop's pcurves in traversal order.
308    let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
309    for loop_record in &face.loops {
310        let mut points: Vec<[f64; 2]> = Vec::new();
311        for coedge in &loop_record.coedges {
312            let [d0, d1] = coedge.pcurve.domain()?;
313            let samples = 24;
314            for k in 0..=samples {
315                let t = d0 + (d1 - d0) * k as f64 / samples as f64;
316                let p = coedge.pcurve.evaluate(t)?;
317                points.push([p.x, p.y]);
318            }
319        }
320        loops_uv.push(points);
321    }
322    let Some(band) =
323        crate::topology::analyze_doubly_periodic_seam_band(&loops_uv, domain, face.same_sense)
324    else {
325        return Ok(None);
326    };
327    let polygon = crate::topology::seam_band_uv_polygon(&loops_uv, domain, &band);
328    Ok(Some(vec![polygon]))
329}
330
331pub(super) fn is_untrimmed(face: &FaceRecord) -> Result<bool, String> {
332    if face.loops.len() != 1 {
333        return Ok(false);
334    }
335    let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
336    let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
337    let [u0, u1] = ku.domain();
338    let [v0, v1] = kv.domain();
339    let domain_area = (u1 - u0) * (v1 - v0);
340    Ok((parameter_space_area(face)?.abs() - domain_area).abs() <= 1e-6 * domain_area)
341}
342
343/// Symmetric degree-5 triangle cubature (barycentric points and weights,
344/// normalized to unit area) — Golovanov §8.10 Table 8.10.2.
345pub(super) const TRIANGLE_CUBATURE: [([f64; 3], f64); 7] = [
346    ([1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], 0.225),
347    (
348        [0.101286507323456, 0.101286507323456, 0.797426985353087],
349        0.125939180544827,
350    ),
351    (
352        [0.101286507323456, 0.797426985353087, 0.101286507323456],
353        0.125939180544827,
354    ),
355    (
356        [0.797426985353087, 0.101286507323456, 0.101286507323456],
357        0.125939180544827,
358    ),
359    (
360        [0.470142064105115, 0.470142064105115, 0.059715871789770],
361        0.132394152788506,
362    ),
363    (
364        [0.470142064105115, 0.059715871789770, 0.470142064105115],
365        0.132394152788506,
366    ),
367    (
368        [0.059715871789770, 0.470142064105115, 0.470142064105115],
369        0.132394152788506,
370    ),
371];
372
373pub(super) fn polygon_signed_area(polygon: &[[f64; 2]]) -> f64 {
374    let mut area = 0.0;
375    for index in 0..polygon.len() {
376        let a = polygon[index];
377        let b = polygon[(index + 1) % polygon.len()];
378        area += a[0] * b[1] - b[0] * a[1];
379    }
380    area * 0.5
381}
382
383/// Sutherland–Hodgman clip of one loop against an axis-aligned cell.  A
384/// loop that fully encloses the cell clips to the whole cell; a disjoint
385/// loop clips to nothing — so clipping every loop handles containment,
386/// holes, and partial coverage uniformly, with the loop's winding as the
387/// contribution sign.
388pub(super) fn clip_polygon_to_cell(polygon: &[[f64; 2]], cell: [f64; 4]) -> Vec<[f64; 2]> {
389    let [u_low, u_high, v_low, v_high] = cell;
390    let mut current = polygon.to_vec();
391    // (axis, bound, keep_below)
392    for (axis, bound, keep_below) in [
393        (0, u_low, false),
394        (0, u_high, true),
395        (1, v_low, false),
396        (1, v_high, true),
397    ] {
398        if current.len() < 3 {
399            return Vec::new();
400        }
401        let inside = |point: &[f64; 2]| {
402            if keep_below {
403                point[axis] <= bound
404            } else {
405                point[axis] >= bound
406            }
407        };
408        let mut next = Vec::with_capacity(current.len() + 4);
409        for index in 0..current.len() {
410            let a = current[index];
411            let b = current[(index + 1) % current.len()];
412            let a_in = inside(&a);
413            let b_in = inside(&b);
414            if a_in {
415                next.push(a);
416            }
417            if a_in != b_in {
418                let t = (bound - a[axis]) / (b[axis] - a[axis]);
419                let mut crossing = [0.0; 2];
420                crossing[axis] = bound;
421                crossing[1 - axis] = a[1 - axis] + t * (b[1 - axis] - a[1 - axis]);
422                next.push(crossing);
423            }
424        }
425        current = next;
426    }
427    current
428}
429
430pub(super) fn cell_breaks(knots: &[f64], degree: usize, low: f64, high: f64) -> Vec<f64> {
431    let mut breaks = vec![low, high];
432    breaks.extend(interior_knots(knots, degree));
433    breaks.sort_by(f64::total_cmp);
434    breaks.dedup_by(|a, b| (*a - *b).abs() <= (high - low) * 1e-12);
435    // Subdivide spans so the base grid has at least MINIMUM_CELLS cells
436    // across the domain; boundary cells then refine adaptively.
437    const MINIMUM_CELLS: usize = 8;
438    let target = (high - low) / MINIMUM_CELLS as f64;
439    let mut refined = Vec::with_capacity(breaks.len() * 2);
440    for pair in breaks.windows(2) {
441        refined.push(pair[0]);
442        let span = pair[1] - pair[0];
443        let pieces = (span / target).ceil().max(1.0) as usize;
444        for piece in 1..pieces {
445            refined.push(pair[0] + span * piece as f64 / pieces as f64);
446        }
447    }
448    refined.push(high);
449    refined
450}
451
452/// Tile one period's `cell_breaks` across `[lo, hi]` for a periodic direction
453/// whose unwrapped trim ran past the domain end. Shifted copies of the base
454/// breaks keep the knot-conforming grid on every covered period; a direction
455/// that stayed in-domain yields exactly the base breaks.
456pub(super) fn tiled_breaks(base: &[f64], lo: f64, hi: f64, period: f64) -> Vec<f64> {
457    if period <= 0.0 || base.len() < 2 {
458        return base.to_vec();
459    }
460    let origin = base[0];
461    let first = ((lo - origin) / period).floor() as i64;
462    let last = ((hi - origin) / period).ceil() as i64;
463    let mut breaks = Vec::new();
464    for tile in first..=last {
465        let shift = tile as f64 * period;
466        for &value in base {
467            let shifted = value + shift;
468            if breaks.last().map_or(true, |&previous: &f64| {
469                (shifted - previous).abs() > 1e-12 * period
470            }) {
471                breaks.push(shifted);
472            }
473        }
474    }
475    breaks
476}