Skip to main content

brep_kernel/props/
mass_properties.rs

1use crate::topology::{BrepSolid, EdgeRecord, FaceRecord, ShellRecord};
2use crate::{NurbsCurve, NurbsSurface, Vec3};
3use serde::Serialize;
4
5/// Eight-station Gauss–Legendre on `[-1, 1]`. `pub(crate)` because
6/// `brep/soundness.rs`'s vector-area closure integrates the trims with the
7/// same rule the mass integrals use — one table, not a third copy of it.
8pub(crate) const GAUSS_X: [f64; 8] = [
9    -0.9602898564975363,
10    -0.7966664774136267,
11    -0.525532409916329,
12    -0.18343464249564978,
13    0.18343464249564978,
14    0.525532409916329,
15    0.7966664774136267,
16    0.9602898564975363,
17];
18pub(crate) const GAUSS_W: [f64; 8] = [
19    0.10122853629037669,
20    0.22238103445337445,
21    0.31370664587788727,
22    0.362683783378362,
23    0.362683783378362,
24    0.31370664587788727,
25    0.22238103445337445,
26    0.10122853629037669,
27];
28
29#[derive(Clone, Copy)]
30enum Integrand {
31    Area,
32    Volume,
33    VolumeAbout(Vec3),
34    /// Divergence-theorem first moments: ∫x dV = ∮ (x²/2)·nx dA, etc.
35    MomentX,
36    MomentY,
37    MomentZ,
38    /// Second moments: ∫x² dV = ∮ (x³/3)·nx dA, etc.
39    SecondXX,
40    SecondYY,
41    SecondZZ,
42    /// Products: ∫xy dV = ∮ (x²y/2)·nx dA, ∫xz, ∫yz analogously.
43    ProductXY,
44    ProductXZ,
45    ProductYZ,
46}
47
48#[derive(Clone, Copy, Debug, Serialize)]
49pub struct MassProperties {
50    pub surface_area: f64,
51    pub volume: f64,
52}
53
54#[derive(Clone, Copy, Debug, Serialize)]
55pub struct FullMassProperties {
56    pub surface_area: f64,
57    pub volume: f64,
58    /// Volume centroid (unit density center of mass).
59    pub centroid: Vec3,
60    /// Inertia tensor about the centroid for unit density, row-major
61    /// [[Ixx, Ixy, Ixz], [Ixy, Iyy, Iyz], [Ixz, Iyz, Izz]].
62    pub inertia: [[f64; 3]; 3],
63    /// Principal moments of inertia (eigenvalues of `inertia`) at unit
64    /// density, sorted ASCENDING: `principal_moments[0] <= [1] <= [2]`.
65    /// Golovanov §8.11.
66    pub principal_moments: [f64; 3],
67    /// Principal axes of inertia (eigenvectors of `inertia`), one per row:
68    /// `principal_axes[i]` is the unit axis whose moment is
69    /// `principal_moments[i]`. The three axes are orthonormal and arranged as
70    /// a RIGHT-HANDED frame (determinant +1); the sign of the third axis is
71    /// fixed to enforce handedness. Density-independent.
72    pub principal_axes: [[f64; 3]; 3],
73}
74
75/// Mass properties scaled to a physical density (Golovanov §8.11). The
76/// underlying `FullMassProperties` is a UNIT-density geometric result; here
77/// `mass = density * volume` and every inertia quantity scales linearly with
78/// density (`inertia = density * geometric_inertia`). The centroid and the
79/// principal AXES are density-independent; only the principal MOMENTS and the
80/// mass scale with density.
81#[derive(Clone, Copy, Debug, Serialize)]
82pub struct DensityMassProperties {
83    /// Density used to scale the geometric (unit-density) properties.
84    pub density: f64,
85    /// `mass = density * volume`.
86    pub mass: f64,
87    pub surface_area: f64,
88    pub volume: f64,
89    /// Center of mass (identical to the unit-density centroid).
90    pub centroid: Vec3,
91    /// Centroidal inertia tensor `= density * geometric_inertia`, row-major.
92    pub inertia: [[f64; 3]; 3],
93    /// Principal moments `= density * geometric_principal_moments`, ascending.
94    pub principal_moments: [f64; 3],
95    /// Principal axes (identical to the unit-density axes; density-invariant).
96    pub principal_axes: [[f64; 3]; 3],
97}
98
99impl FullMassProperties {
100    /// Scale the unit-density geometric result to a physical `density`
101    /// (Golovanov §8.11): `mass = density * volume`, `inertia` and the
102    /// principal moments scale by `density`, while the centroid and principal
103    /// axes are unchanged. Panics only on a non-finite density is avoided —
104    /// callers should pass a finite, positive density; a zero density yields a
105    /// massless (all-zero inertia) result, which is well defined.
106    pub fn with_density(&self, density: f64) -> DensityMassProperties {
107        let scale = |matrix: [[f64; 3]; 3]| {
108            let mut out = [[0.0f64; 3]; 3];
109            for row in 0..3 {
110                for column in 0..3 {
111                    out[row][column] = density * matrix[row][column];
112                }
113            }
114            out
115        };
116        DensityMassProperties {
117            density,
118            mass: density * self.volume,
119            surface_area: self.surface_area,
120            volume: self.volume,
121            centroid: self.centroid,
122            inertia: scale(self.inertia),
123            principal_moments: [
124                density * self.principal_moments[0],
125                density * self.principal_moments[1],
126                density * self.principal_moments[2],
127            ],
128            principal_axes: self.principal_axes,
129        }
130    }
131}
132
133
134/// Per-call counters for the trimmed-face integrator, printed to stderr when
135/// `BREP_PROFILE` is set (the switch `boolean.<stage>_ms` and `imprint.*`
136/// use). One line per `solid_mass_properties` / `solid_signed_volume` /
137/// `solid_mass_properties_full` call: which route each trimmed face took,
138/// how many quadrature stations the cells, the boundary triangles and the
139/// chord-arc correction cost, and the wall time of each part. Native only in
140/// practice (no env vars in wasm); when the switch is off every counter is a
141/// thread-local flag test.
142#[path = "mass_properties/profile.rs"]
143mod profile;
144pub(crate) use profile::mass_profile;
145pub use profile::{mass_caller, MassCaller};
146
147/// The content identity of the face set an integrator call reads — the
148/// instrument behind the sign-check pairing, and the key a served value is
149/// checked against.
150#[path = "mass_properties/identity.rs"]
151mod identity;
152
153#[path = "mass_properties/integration.rs"]
154mod integration;
155#[path = "mass_properties/measures.rs"]
156mod measures;
157#[path = "mass_properties/polygons.rs"]
158mod polygons;
159/// Which Gauss–Legendre rule reads one span, and where it is cut: the answer
160/// is the span's own WEIGHTS, because a fixed-order rule is exact for a
161/// polynomial integrand and a rational one is not.
162#[path = "mass_properties/rule.rs"]
163mod rule;
164#[path = "mass_properties/solid_props.rs"]
165mod solid_props;
166#[path = "mass_properties/trimmed.rs"]
167mod trimmed;
168#[path = "mass_properties/winding.rs"]
169mod winding;
170
171use integration::*;
172use measures::*;
173use polygons::*;
174use trimmed::*;
175
176pub use integration::parameter_space_area;
177// The two span helpers `brep/soundness.rs`'s vector-area closure shares with
178// the mass integrals: the surface's knot levels, and a pcurve's spans cut
179// wherever it crosses one. A boundary integral read across a knot line is the
180// same 0.3%-off reading `winding_loops_integral` documents.
181pub(crate) use integration::surface_breaks;
182pub(crate) use winding::split_at_surface_breaks;
183pub use measures::{
184    curve_arc_length, edge_arc_length, face_area, face_boundary_length,
185    face_volume_contribution, solid_edge_length_total,
186};
187pub use solid_props::{solid_mass_properties, solid_mass_properties_full, solid_signed_volume};
188pub(crate) use solid_props::shell_signed_volume;
189pub use polygons::trim_polygons;
190pub use identity::{face_set_identity, FaceSetIdentity};