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