Skip to main content

brep_kernel/props/mass_properties/
integration.rs

1pub(super) use crate::curve::interior_knots;
2use super::*;
3
4pub(super) fn curve_breaks(curve: &NurbsCurve) -> Result<Vec<f64>, String> {
5    let [start, end] = curve.domain()?;
6    let mut result = vec![start];
7    result.extend(interior_knots(&curve.knots, curve.degree));
8    result.push(end);
9    Ok(result)
10}
11
12pub fn parameter_space_area(face: &FaceRecord) -> Result<f64, String> {
13    let mut area = 0.0;
14    for loop_record in &face.loops {
15        for coedge in &loop_record.coedges {
16            for pair in curve_breaks(&coedge.pcurve)?.windows(2) {
17                let half = (pair[1] - pair[0]) * 0.5;
18                let middle = (pair[1] + pair[0]) * 0.5;
19                for index in 0..GAUSS_X.len() {
20                    let parameter = middle + half * GAUSS_X[index];
21                    let (point, tangent) = coedge.pcurve.deriv1(parameter)?;
22                    area +=
23                        GAUSS_W[index] * half * 0.5 * (point.x * tangent.y - point.y * tangent.x);
24                }
25            }
26        }
27    }
28    Ok(area)
29}
30
31pub(super) fn is_affine(surface: &NurbsSurface) -> Result<bool, String> {
32    // Keep exact metric shortcuts consistent with geometry recognition.
33    surface.is_affine()
34}
35
36pub(super) fn surface_breaks(surface: &NurbsSurface) -> Result<(Vec<f64>, Vec<f64>), String> {
37    let ku = crate::KnotVector::new(surface.knots_u.clone(), surface.degree_u)?;
38    let kv = crate::KnotVector::new(surface.knots_v.clone(), surface.degree_v)?;
39    let [u0, u1] = ku.domain();
40    let [v0, v1] = kv.domain();
41    let mut u = vec![u0];
42    u.extend(interior_knots(&surface.knots_u, surface.degree_u));
43    u.push(u1);
44    let mut v = vec![v0];
45    v.extend(interior_knots(&surface.knots_v, surface.degree_v));
46    v.push(v1);
47    Ok((u, v))
48}
49
50pub(super) fn integrand_value(kind: Integrand, point: Vec3, weighted_normal: Vec3) -> f64 {
51    let (x, y, z) = (point.x, point.y, point.z);
52    match kind {
53        Integrand::Area => weighted_normal.length(),
54        Integrand::Volume => point.dot(weighted_normal),
55        Integrand::VolumeAbout(reference) => point.sub(reference).dot(weighted_normal),
56        Integrand::MomentX => 0.5 * x * x * weighted_normal.x,
57        Integrand::MomentY => 0.5 * y * y * weighted_normal.y,
58        Integrand::MomentZ => 0.5 * z * z * weighted_normal.z,
59        Integrand::SecondXX => x * x * x / 3.0 * weighted_normal.x,
60        Integrand::SecondYY => y * y * y / 3.0 * weighted_normal.y,
61        Integrand::SecondZZ => z * z * z / 3.0 * weighted_normal.z,
62        Integrand::ProductXY => 0.5 * x * x * y * weighted_normal.x,
63        Integrand::ProductXZ => 0.5 * x * x * z * weighted_normal.x,
64        Integrand::ProductYZ => 0.5 * y * y * z * weighted_normal.y,
65    }
66}
67
68pub(super) fn evaluate_integrand(face: &FaceRecord, u: f64, v: f64, kind: Integrand) -> Result<f64, String> {
69    let (point, su, sv) = face.surface.deriv1(u, v)?;
70    let sign = if face.same_sense { 1.0 } else { -1.0 };
71    let weighted_normal = su.cross(sv).scale(sign);
72    Ok(integrand_value(kind, point, weighted_normal))
73}
74
75pub(super) fn integrate_untrimmed(face: &FaceRecord, kind: Integrand) -> Result<f64, String> {
76    let (u_breaks, v_breaks) = surface_breaks(&face.surface)?;
77    let mut total = 0.0;
78    for upair in u_breaks.windows(2) {
79        let half_u = (upair[1] - upair[0]) * 0.5;
80        let middle_u = (upair[1] + upair[0]) * 0.5;
81        for vpair in v_breaks.windows(2) {
82            let half_v = (vpair[1] - vpair[0]) * 0.5;
83            let middle_v = (vpair[1] + vpair[0]) * 0.5;
84            for i in 0..GAUSS_X.len() {
85                for j in 0..GAUSS_X.len() {
86                    total += GAUSS_W[i]
87                        * GAUSS_W[j]
88                        * half_u
89                        * half_v
90                        * evaluate_integrand(
91                            face,
92                            middle_u + half_u * GAUSS_X[i],
93                            middle_v + half_v * GAUSS_X[j],
94                            kind,
95                        )?;
96                }
97            }
98        }
99    }
100    Ok(total)
101}
102
103/// A bi-periodic band face (surface closed in both u and v, exactly two loops
104/// each a full-wrap constant-cross-level rim) whose trim is NOT expressed with a
105/// seam ruling — the fillet-torus bands OCC/STEP emit as two rim circles. Such a
106/// face integrates to ZERO on the trimmed path (both loops are degenerate iso
107/// lines in parameter space), so it is handled analytically over the seam-cut
108/// rectangle instead. See [`biperiodic_band_range`].
109#[derive(Clone, Copy)]
110pub(super) struct BiBand {
111    /// Which parameter is the periodic (full-wrap) one.
112    p_is_u: bool,
113    /// The two rim cross-levels, ascending (q_lo <= q_hi) in the cross param.
114    q_lo: f64,
115    q_hi: f64,
116    /// True when the material band is the CROSS-SEAM COMPLEMENT of [q_lo, q_hi]
117    /// rather than the between-rims strip (mirrors the tessellator's
118    /// `close_periodic_trim_dir` orientation test).
119    complement: bool,
120}
121
122/// Detect the bi-periodic band configuration of `face` and choose which of the
123/// two regions its rims bound is material. The choice replicates the watertight
124/// tessellator (`watertight_tessellation::close_periodic_trim_dir`): the
125/// material is to the LEFT of the boundary traversal, so the between-rims strip
126/// is kept unless the cross direction wraps AND the rim senses conclusively name
127/// the complement. Returns None for anything that is not a clean two-rim band.
128pub(super) fn biperiodic_band_range(face: &FaceRecord) -> Result<Option<BiBand>, String> {
129    let surface = &face.surface;
130    let (closed_u, closed_v) = surface.closed_directions()?;
131    if !(closed_u && closed_v) {
132        return Ok(None);
133    }
134    let [u0, u1] = surface.domain_u()?;
135    let [v0, v1] = surface.domain_v()?;
136    let u_span = (u1 - u0).abs().max(1e-30);
137    let v_span = (v1 - v0).abs().max(1e-30);
138    // Sample each loop's pcurves (in coedge/traversal order) into (u,v) points.
139    // A DEGENERATE loop — a pole/apex vertex whose whole trace collapses to one
140    // parameter point (a fat fillet torus touches its outer equator at a single
141    // seam point: ABC 00000039 faces 68/70) — is not a band boundary; drop it
142    // and keep the two real rims. Anything else leaves the band ambiguous.
143    let mut loop_points: Vec<Vec<[f64; 2]>> = Vec::with_capacity(2);
144    for loop_record in &face.loops {
145        let mut points = Vec::new();
146        for coedge in &loop_record.coedges {
147            let [d0, d1] = coedge.pcurve.domain()?;
148            let samples = 12;
149            for k in 0..=samples {
150                let t = d0 + (d1 - d0) * k as f64 / samples as f64;
151                let p = coedge.pcurve.evaluate(t)?;
152                points.push([p.x, p.y]);
153            }
154        }
155        if points.len() < 2 {
156            return Ok(None);
157        }
158        let (mut umin, mut umax, mut vmin, mut vmax) = (
159            f64::INFINITY,
160            f64::NEG_INFINITY,
161            f64::INFINITY,
162            f64::NEG_INFINITY,
163        );
164        for pt in &points {
165            umin = umin.min(pt[0]);
166            umax = umax.max(pt[0]);
167            vmin = vmin.min(pt[1]);
168            vmax = vmax.max(pt[1]);
169        }
170        if (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span {
171            continue; // degenerate pole/apex loop
172        }
173        loop_points.push(points);
174    }
175    if loop_points.len() != 2 {
176        return Ok(None);
177    }
178    for p_is_u in [true, false] {
179        let (period, _p0, _p1) = if p_is_u {
180            (u1 - u0, u0, u1)
181        } else {
182            (v1 - v0, v0, v1)
183        };
184        let (q_dom_lo, q_dom_hi) = if p_is_u { (v0, v1) } else { (u0, u1) };
185        let q_extent = (q_dom_hi - q_dom_lo).abs().max(1e-30);
186        if !(period > 0.0) {
187            continue;
188        }
189        let coord = |pt: &[f64; 2]| -> (f64, f64) {
190            if p_is_u {
191                (pt[0], pt[1])
192            } else {
193                (pt[1], pt[0])
194            }
195        };
196        // (level, winding-direction) for each rim; None if any loop is not a
197        // clean full-wrap constant-cross-level rim in this p direction.
198        let mut rings: Vec<(f64, i32)> = Vec::with_capacity(2);
199        let mut clean = true;
200        for points in &loop_points {
201            let (mut pmin, mut pmax, mut qmin, mut qmax) = (
202                f64::INFINITY,
203                f64::NEG_INFINITY,
204                f64::INFINITY,
205                f64::NEG_INFINITY,
206            );
207            for pt in points {
208                let (p, q) = coord(pt);
209                pmin = pmin.min(p);
210                pmax = pmax.max(p);
211                qmin = qmin.min(q);
212                qmax = qmax.max(q);
213            }
214            if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
215                clean = false;
216                break;
217            }
218            let mut net = 0.0;
219            for pair in points.windows(2) {
220                let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
221                if delta > 0.5 * period {
222                    delta -= period;
223                } else if delta < -0.5 * period {
224                    delta += period;
225                }
226                net += delta;
227            }
228            let direction = if net > 0.25 * period {
229                1
230            } else if net < -0.25 * period {
231                -1
232            } else {
233                0
234            };
235            rings.push((0.5 * (qmin + qmax), direction));
236        }
237        if !clean || rings.len() != 2 {
238            continue;
239        }
240        rings.sort_by(|a, b| a.0.total_cmp(&b.0));
241        let (q_lo, lower_dir) = rings[0];
242        let (q_hi, upper_dir) = rings[1];
243        let inconclusive = lower_dir == 0 || upper_dir == 0 || lower_dir == upper_dir;
244        let between_rims_is_ccw_uv = if p_is_u { lower_dir > 0 } else { lower_dir < 0 };
245        let complement = !inconclusive && between_rims_is_ccw_uv != face.same_sense;
246        return Ok(Some(BiBand {
247            p_is_u,
248            q_lo,
249            q_hi,
250            complement,
251        }));
252    }
253    Ok(None)
254}
255
256/// Gauss–Legendre integral of `kind` over the axis-aligned parameter rectangle
257/// [u_lo,u_hi]×[v_lo,v_hi], subdividing at knot spans clipped to the rectangle.
258pub(super) fn integrate_rectangle(
259    face: &FaceRecord,
260    u_lo: f64,
261    u_hi: f64,
262    v_lo: f64,
263    v_hi: f64,
264    kind: Integrand,
265) -> Result<f64, String> {
266    let (u_full, v_full) = surface_breaks(&face.surface)?;
267    let clamp = |breaks: &[f64], lo: f64, hi: f64| -> Vec<f64> {
268        let eps = 1e-9 * (hi - lo).abs().max(1e-30);
269        let mut out = vec![lo];
270        for &b in breaks {
271            if b > lo + eps && b < hi - eps {
272                out.push(b);
273            }
274        }
275        out.push(hi);
276        out
277    };
278    let u_breaks = clamp(&u_full, u_lo, u_hi);
279    let v_breaks = clamp(&v_full, v_lo, v_hi);
280    let mut total = 0.0;
281    for upair in u_breaks.windows(2) {
282        let half_u = (upair[1] - upair[0]) * 0.5;
283        let middle_u = (upair[1] + upair[0]) * 0.5;
284        for vpair in v_breaks.windows(2) {
285            let half_v = (vpair[1] - vpair[0]) * 0.5;
286            let middle_v = (vpair[1] + vpair[0]) * 0.5;
287            for i in 0..GAUSS_X.len() {
288                for j in 0..GAUSS_X.len() {
289                    total += GAUSS_W[i]
290                        * GAUSS_W[j]
291                        * half_u
292                        * half_v
293                        * evaluate_integrand(
294                            face,
295                            middle_u + half_u * GAUSS_X[i],
296                            middle_v + half_v * GAUSS_X[j],
297                            kind,
298                        )?;
299                }
300            }
301        }
302    }
303    Ok(total)
304}
305
306/// Integrate each of `kinds` over a bi-periodic band face, or return None when
307/// `face` is not such a band. The between-rims strip integrates over the full
308/// periodic span × the cross-level strip; the complement is the full-domain
309/// integral MINUS that strip (the two tile the closed cross period, so no
310/// domain-extended evaluation is needed).
311pub(super) fn biperiodic_band_integral(
312    face: &FaceRecord,
313    kinds: &[Integrand],
314) -> Result<Option<Vec<f64>>, String> {
315    let Some(band) = biperiodic_band_range(face)? else {
316        return Ok(None);
317    };
318    let [u0, u1] = face.surface.domain_u()?;
319    let [v0, v1] = face.surface.domain_v()?;
320    let (u_lo, u_hi, v_lo, v_hi) = if band.p_is_u {
321        (u0, u1, band.q_lo, band.q_hi)
322    } else {
323        (band.q_lo, band.q_hi, v0, v1)
324    };
325    let mut out = Vec::with_capacity(kinds.len());
326    for &kind in kinds {
327        let strip = integrate_rectangle(face, u_lo, u_hi, v_lo, v_hi, kind)?;
328        let value = if band.complement {
329            integrate_untrimmed(face, kind)? - strip
330        } else {
331            strip
332        };
333        out.push(value);
334    }
335    Ok(Some(out))
336}