Skip to main content

brep_kernel/props/mass_properties/
measures.rs

1use super::*;
2use super::mass_profile;
3
4pub fn face_area(face: &FaceRecord) -> Result<f64, String> {
5    if is_affine(&face.surface)? {
6        return Ok(closed_parameter_space_area(face)?.abs() * planar_metric(face)?);
7    }
8    if let Some(values) = biperiodic_band_integral(face, &[Integrand::Area])? {
9        return Ok(values[0]);
10    }
11    if is_untrimmed(face)? {
12        integrate_untrimmed(face, Integrand::Area)
13    } else {
14        integrate_trimmed(face, Integrand::Area)
15    }
16}
17
18/// Exact 3D arc length of a NURBS curve over the parameter range `[t0, t1]` —
19/// ∫|C'(t)| dt evaluated with a composite 8-point Gauss–Legendre rule paneled at
20/// the curve's interior knots (each knot span further halved for headroom). The
21/// speed integrand `|C'(t)|` is polynomial-exact for the line/circle-arc curves
22/// the kernel emits and converges tightly for general NURBS. Returns `0.0` for a
23/// zero-width (or inverted) range.
24pub fn curve_arc_length(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<f64, String> {
25    let (lo, hi) = (t0.min(t1), t0.max(t1));
26    if !(hi > lo) || !hi.is_finite() || !lo.is_finite() {
27        return Ok(0.0);
28    }
29    // Break the range at the curve's interior knots so a panel never straddles a
30    // C0 kink; then halve each span for extra Gauss headroom.
31    let mut breaks: Vec<f64> = vec![lo, hi];
32    for &knot in &curve.knots {
33        if knot > lo + 1e-12 && knot < hi - 1e-12 {
34            breaks.push(knot);
35        }
36    }
37    breaks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
38    breaks.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
39
40    let mut length = 0.0;
41    for window in breaks.windows(2) {
42        let (a, b) = (window[0], window[1]);
43        if !(b > a) {
44            continue;
45        }
46        // Two sub-panels per knot span, each read at the order its own
47        // weights ask for: `|C'|` carries the weight in the denominator, so a
48        // re-weighted copy of the same curve is the same length only if the
49        // rule outruns the weight's nearest root (`rule`). A unit-weight span
50        // keeps the two 8-point panels, bit for bit.
51        for sub in 0..2 {
52            let sa = a + (b - a) * sub as f64 / 2.0;
53            let sb = a + (b - a) * (sub + 1) as f64 / 2.0;
54            for panel in rule::curve_panels(curve, sa, sb)? {
55                let half = 0.5 * (panel.hi - panel.lo);
56                let middle = 0.5 * (panel.lo + panel.hi);
57                let (nodes, weights) = panel.nodes();
58                for index in 0..nodes.len() {
59                    let t = middle + half * nodes[index];
60                    let speed = curve.derivatives(t, 1)?[1].length();
61                    length += weights[index] * half * speed;
62                }
63            }
64        }
65    }
66    Ok(length)
67}
68
69/// Exact 3D arc length of one edge over its active parameter range `[t0, t1]`
70/// (the same range the display sampler walks). Degenerate edges measure `0`.
71pub fn edge_arc_length(edge: &EdgeRecord) -> Result<f64, String> {
72    if edge.degenerate {
73        return Ok(0.0);
74    }
75    curve_arc_length(&edge.curve, edge.t0, edge.t1)
76}
77
78/// Total 3D arc length of every non-degenerate edge of a solid (each shared edge
79/// counted ONCE — the solid's edge table is already edge-unique). This is the
80/// solid's "total edge length" measurement.
81pub fn solid_edge_length_total(solid: &BrepSolid) -> Result<f64, String> {
82    let mut total = 0.0;
83    for edge in &solid.edges {
84        total += edge_arc_length(edge)?;
85    }
86    Ok(total)
87}
88
89/// Total 3D arc length of a face's boundary edges: the unique edges referenced by
90/// the face's loop coedges, resolved against the owning `solid`'s edge table and
91/// summed once each. An edge referenced twice by the same face (e.g. a seam) is
92/// still counted once.
93pub fn face_boundary_length(solid: &BrepSolid, face: &FaceRecord) -> Result<f64, String> {
94    let mut edge_ids: Vec<u64> = Vec::new();
95    for loop_record in &face.loops {
96        for coedge in &loop_record.coedges {
97            if !edge_ids.contains(&coedge.edge_id) {
98                edge_ids.push(coedge.edge_id);
99            }
100        }
101    }
102    let mut total = 0.0;
103    for id in edge_ids {
104        if let Some(edge) = solid.edges.iter().find(|edge| edge.id == id) {
105            total += edge_arc_length(edge)?;
106        }
107    }
108    Ok(total)
109}
110
111pub fn face_volume_contribution(face: &FaceRecord) -> Result<f64, String> {
112    if is_affine(&face.surface)? {
113        let signed_area = closed_parameter_space_area(face)? * planar_metric(face)?;
114        let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
115        let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
116        let u = ku.domain()[0];
117        let v = kv.domain()[0];
118        let point = face.surface.evaluate(u, v)?;
119        let normal = face.surface.normal(u, v)?;
120        return Ok(point.dot(normal) * signed_area / 3.0);
121    }
122    if let Some(values) = biperiodic_band_integral(face, &[Integrand::Volume])? {
123        return Ok(values[0] / 3.0);
124    }
125    let integral = if is_untrimmed(face)? {
126        integrate_untrimmed(face, Integrand::Volume)?
127    } else {
128        integrate_trimmed(face, Integrand::Volume)?
129    };
130    Ok(integral / 3.0)
131}
132
133/// Area AND volume flux of an AFFINE face from ONE parameter-space area and
134/// ONE planar metric. [`face_area`] and [`face_volume_contribution_about`]
135/// each compute both quantities from scratch, so asking a plane for its area
136/// and its volume walked every trim pcurve twice; the two are pure functions
137/// of the face, so sharing them is bit-for-bit the same pair of numbers.
138pub(super) fn affine_area_and_volume_about(
139    face: &FaceRecord,
140    reference: Vec3,
141) -> Result<(f64, f64), String> {
142    let parameter_area = closed_parameter_space_area(face)?;
143    let metric = planar_metric(face)?;
144    let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
145    let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
146    let u = ku.domain()[0];
147    let v = kv.domain()[0];
148    let point = face.surface.evaluate(u, v)?;
149    let normal = face.surface.normal(u, v)?;
150    let signed_area = parameter_area * metric;
151    Ok((
152        parameter_area.abs() * metric,
153        point.sub(reference).dot(normal) * signed_area / 3.0,
154    ))
155}
156
157pub(super) fn face_volume_contribution_about(face: &FaceRecord, reference: Vec3) -> Result<f64, String> {
158    if is_affine(&face.surface)? {
159        let signed_area = closed_parameter_space_area(face)? * planar_metric(face)?;
160        let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
161        let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
162        let u = ku.domain()[0];
163        let v = kv.domain()[0];
164        let point = face.surface.evaluate(u, v)?;
165        let normal = face.surface.normal(u, v)?;
166        return Ok(point.sub(reference).dot(normal) * signed_area / 3.0);
167    }
168    let kind = Integrand::VolumeAbout(reference);
169    let gate_started = super::profile::profile_started();
170    if let Some(values) = biperiodic_band_integral(face, &[kind])? {
171        mass_profile(|p| {
172            p.route_biperiodic += 1;
173            p.gate_ms += super::profile::elapsed_ms(gate_started);
174        });
175        return Ok(values[0] / 3.0);
176    }
177    let integral = if is_untrimmed(face)? {
178        mass_profile(|p| {
179            p.route_untrimmed += 1;
180            p.gate_ms += super::profile::elapsed_ms(gate_started);
181        });
182        let started = super::profile::profile_started();
183        let value = integrate_untrimmed(face, kind)?;
184        mass_profile(|p| p.untrimmed_ms += super::profile::elapsed_ms(started));
185        value
186    } else {
187        mass_profile(|p| {
188            p.trimmed_faces += 1;
189            p.gate_ms += super::profile::elapsed_ms(gate_started);
190        });
191        integrate_trimmed(face, kind)?
192    };
193    Ok(integral / 3.0)
194}
195
196/// Volume about a local anchor and the three vector-area components, all
197/// divided by three. Their affine combination reads this face about ANY
198/// reference without integrating it again. The multi-integrand paths share
199/// surface evaluations, panels, polygons, winding and correction policy.
200pub(super) fn face_volume_terms_about(face: &FaceRecord, reference: Vec3) -> Result<[f64; 4], String> {
201    if is_affine(&face.surface)? {
202        let signed_area = closed_parameter_space_area(face)? * planar_metric(face)?;
203        let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
204        let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
205        let (u, v) = (ku.domain()[0], kv.domain()[0]);
206        let point = face.surface.evaluate(u, v)?;
207        let normal = face.surface.normal(u, v)?;
208        return Ok([
209            point.sub(reference).dot(normal) * signed_area / 3.0,
210            normal.x * signed_area / 3.0,
211            normal.y * signed_area / 3.0,
212            normal.z * signed_area / 3.0,
213        ]);
214    }
215    let kinds = [Integrand::VolumeAbout(reference), Integrand::NormalX, Integrand::NormalY, Integrand::NormalZ];
216    let gate_started = super::profile::profile_started();
217    let values = if let Some(values) = biperiodic_band_integral(face, &kinds)? {
218        mass_profile(|p| {
219            p.route_biperiodic += 1;
220            p.gate_ms += super::profile::elapsed_ms(gate_started);
221        });
222        values
223    } else if is_untrimmed(face)? {
224        mass_profile(|p| {
225            p.route_untrimmed += 1;
226            p.gate_ms += super::profile::elapsed_ms(gate_started);
227        });
228        let started = super::profile::profile_started();
229        let values = integrate_untrimmed_multi(face, &kinds)?;
230        mass_profile(|p| p.untrimmed_ms += super::profile::elapsed_ms(started));
231        values
232    } else if let Some(rectangle) = iso_rectangle_route_on().then(|| iso_rectangle_trim(face)).transpose()?.flatten() {
233        // The sign reading's iso-rectangle route (lane O, 2026-10-28): a
234        // single loop of four axis-parallel line pcurves is a sub-rectangle
235        // of the knot grid, read with the untrimmed sweep's rules on the
236        // clipped cells instead of the trimmed route's chord polygons and
237        // cell quadtree. `BREP_MASS_ISO_RECT=0` keeps the trimmed route.
238        mass_profile(|p| {
239            p.route_iso_rectangle += 1;
240            p.gate_ms += super::profile::elapsed_ms(gate_started);
241        });
242        let started = super::profile::profile_started();
243        let values = integrate_iso_rectangle_multi(face, rectangle, &kinds)?;
244        mass_profile(|p| p.untrimmed_ms += super::profile::elapsed_ms(started));
245        values
246    } else {
247        mass_profile(|p| {
248            p.trimmed_faces += 1;
249            p.gate_ms += super::profile::elapsed_ms(gate_started);
250        });
251        integrate_trimmed_multi(face, &kinds)?
252    };
253    Ok([values[0] / 3.0, values[1] / 3.0, values[2] / 3.0, values[3] / 3.0])
254}
255
256/// The iso-rectangle route of [`face_volume_terms_about`], on unless
257/// `BREP_MASS_ISO_RECT` is `0`/`off`. Read once.
258fn iso_rectangle_route_on() -> bool {
259    switch!("BREP_MASS_ISO_RECT", true)
260}
261