1use super::*;
2use super::mass_profile;
3use web_time::Instant;
4
5fn face_line_meta(face: &FaceRecord) -> (&'static str, usize, usize, bool, bool, usize, usize) {
9 let kind = match face.surface.analytic().map(|analytic| analytic.kind_label()) {
12 Some("Surface of revolution") => "Revolution",
13 Some(label) => label,
14 None => "Fitted",
15 };
16 let (closed_u, closed_v) = face.surface.closed_directions().unwrap_or((false, false));
17 let coedges = face
18 .loops
19 .iter()
20 .map(|loop_record| loop_record.coedges.len())
21 .sum();
22 (
23 kind,
24 face.surface.degree_u,
25 face.surface.degree_v,
26 closed_u,
27 closed_v,
28 face.loops.len(),
29 coedges,
30 )
31}
32
33fn emit_face_line(
37 call: &str,
38 shell: usize,
39 face_index: usize,
40 face: &FaceRecord,
41 before: Option<super::profile::MassProfile>,
42 started: Option<Instant>,
43) {
44 let (Some(before), Some(started)) = (before, started) else {
45 return;
46 };
47 let wall_ms = started.elapsed().as_secs_f64() * 1_000.0;
48 let (kind, degree_u, degree_v, closed_u, closed_v, loops, coedges) = face_line_meta(face);
49 super::profile::emit_face(
50 call, shell, face_index, kind, degree_u, degree_v, closed_u, closed_v, loops, coedges,
51 before, wall_ms,
52 );
53}
54
55pub fn solid_mass_properties(solid: &BrepSolid) -> Result<MassProperties, String> {
56 let token = super::profile::begin("solid_mass_properties");
57 let result = solid_mass_properties_inner(solid);
58 super::profile::end(token);
59 if let (Some(dump), Ok(properties)) = (super::profile::identity_dump(), result.as_ref()) {
60 super::profile::emit_identity(
61 dump,
62 "solid_mass_properties",
63 solid,
64 &[
65 ("volume_bits", properties.volume),
66 ("area_bits", properties.surface_area),
67 ],
68 );
69 }
70 result
71}
72
73fn solid_mass_properties_inner(solid: &BrepSolid) -> Result<MassProperties, String> {
74 let mut properties = MassProperties {
75 surface_area: 0.0,
76 volume: 0.0,
77 };
78 let mut volume_compensation = 0.0;
79 let face_lines = super::profile::face_lines_enabled();
80 for (shell_index, shell) in solid.shells.iter().enumerate() {
81 if shell.faces.is_empty() {
82 continue;
83 }
84 let reference = shell_volume_reference(shell)?;
88 let kinds = [Integrand::Area, Integrand::VolumeAbout(reference)];
89 for (face_index, face) in shell.faces.iter().enumerate() {
90 mass_profile(|p| p.faces += 1);
91 let line_before = face_lines.then(super::profile::snapshot);
92 let line_started = face_lines.then(Instant::now);
93 let gate_started = super::profile::profile_started();
94 let (area, volume) = if let Some(values) = biperiodic_band_integral(face, &kinds)? {
95 mass_profile(|p| {
96 p.route_biperiodic += 1;
97 p.gate_ms += super::profile::elapsed_ms(gate_started);
98 });
99 (values[0], values[1] / 3.0)
100 } else if !is_affine(&face.surface)? && !is_untrimmed(face)? {
101 mass_profile(|p| {
102 p.trimmed_faces += 1;
103 p.gate_ms += super::profile::elapsed_ms(gate_started);
104 });
105 let values = integrate_trimmed_multi(face, &kinds)?;
107 (values[0], values[1] / 3.0)
108 } else if is_affine(&face.surface)? {
109 mass_profile(|p| {
110 p.route_affine += 1;
111 p.gate_ms += super::profile::elapsed_ms(gate_started);
112 });
113 affine_area_and_volume_about(face, reference)?
117 } else {
118 mass_profile(|p| {
119 p.route_untrimmed += 1;
120 p.gate_ms += super::profile::elapsed_ms(gate_started);
121 });
122 let started = super::profile::profile_started();
125 let values = integrate_untrimmed_multi(face, &kinds)?;
126 mass_profile(|p| p.untrimmed_ms += super::profile::elapsed_ms(started));
127 (values[0], values[1] / 3.0)
128 };
129 emit_face_line(
130 "solid_mass_properties",
131 shell_index,
132 face_index,
133 face,
134 line_before,
135 line_started,
136 );
137 properties.surface_area += area;
138 let next = properties.volume + volume;
139 if properties.volume.abs() >= volume.abs() {
140 volume_compensation += (properties.volume - next) + volume;
141 } else {
142 volume_compensation += (volume - next) + properties.volume;
143 }
144 properties.volume = next;
145 }
146 }
147 properties.volume += volume_compensation;
148 Ok(properties)
149}
150
151pub fn solid_signed_volume(solid: &BrepSolid) -> Result<f64, String> {
156 let token = super::profile::begin("solid_signed_volume");
157 let mut volume = 0.0;
158 for shell in &solid.shells {
159 volume += match shell_signed_volume(shell) {
160 Ok(value) => value,
161 Err(error) => {
162 super::profile::end(token);
163 return Err(error);
164 }
165 };
166 }
167 super::profile::end(token);
168 if let Some(dump) = super::profile::identity_dump() {
169 super::profile::emit_identity(dump, "solid_signed_volume", solid, &[("signed_bits", volume)]);
170 }
171 Ok(volume)
172}
173
174pub(super) fn shell_volume_reference(shell: &ShellRecord) -> Result<Vec3, String> {
175 let finite = |point: Vec3| point.x.is_finite() && point.y.is_finite() && point.z.is_finite();
176 for face in &shell.faces {
177 if let Some(coedge) = face
178 .loops
179 .first()
180 .and_then(|loop_record| loop_record.coedges.first())
181 {
182 if let Ok(domain) = coedge.pcurve.domain() {
183 if let Ok(uv) = coedge.pcurve.evaluate(domain[0]) {
184 if let Ok(point) = face.surface.evaluate(uv.x, uv.y) {
185 if finite(point) {
186 return Ok(point);
187 }
188 }
189 }
190 }
191 }
192 if let Ok((u_breaks, v_breaks)) = surface_breaks(&face.surface) {
193 let u = 0.5 * (u_breaks[0] + u_breaks[u_breaks.len() - 1]);
194 let v = 0.5 * (v_breaks[0] + v_breaks[v_breaks.len() - 1]);
195 if let Ok(point) = face.surface.evaluate(u, v) {
196 if finite(point) {
197 return Ok(point);
198 }
199 }
200 }
201 for row in &face.surface.control_points {
202 for control in row {
203 if let Ok(point) = control.point() {
204 if finite(point) {
205 return Ok(point);
206 }
207 }
208 }
209 }
210 }
211 Err("mass_properties: shell has no finite geometric reference".to_string())
212}
213
214pub(crate) fn shell_signed_volume(shell: &ShellRecord) -> Result<f64, String> {
218 let reference = shell_volume_reference(shell)?;
219
220 let mut volume = 0.0;
226 let mut compensation = 0.0;
227 let face_lines = super::profile::face_lines_enabled();
228 for (face_index, face) in shell.faces.iter().enumerate() {
229 mass_profile(|p| p.faces += 1);
230 let line_before = face_lines.then(super::profile::snapshot);
231 let line_started = face_lines.then(Instant::now);
232 let contribution = face_volume_contribution_about(face, reference)?;
233 emit_face_line(
234 "shell_signed_volume",
235 0,
236 face_index,
237 face,
238 line_before,
239 line_started,
240 );
241 let next = volume + contribution;
242 if volume.abs() >= contribution.abs() {
243 compensation += (volume - next) + contribution;
244 } else {
245 compensation += (contribution - next) + volume;
246 }
247 volume = next;
248 }
249 Ok(volume + compensation)
250}
251
252pub(super) fn affine_moment(face: &FaceRecord, kind: Integrand) -> Result<f64, String> {
259 let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
260 let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
261 let [u0, u1] = ku.domain();
262 let [v0, v1] = kv.domain();
263 let points = &face.surface.control_points;
264 let p00 = points[0][0].point()?;
265 let p10 = points[1][0].point()?;
266 let p01 = points[0][1].point()?;
267 let du = p10.sub(p00).scale(1.0 / (u1 - u0));
268 let dv = p01.sub(p00).scale(1.0 / (v1 - v0));
269 let weighted_normal = du.cross(dv);
270 let g = |u: f64, v: f64| {
271 let point = p00.add(du.scale(u - u0)).add(dv.scale(v - v0));
272 integrand_value(kind, point, weighted_normal)
273 };
274 let inner = |u: f64, v: f64| {
275 let half = (u - u0) * 0.5;
276 let middle = (u + u0) * 0.5;
277 let mut sum = 0.0;
278 for index in 0..GAUSS_X.len() {
279 sum += GAUSS_W[index] * g(middle + half * GAUSS_X[index], v);
280 }
281 sum * half
282 };
283 let mut total = 0.0;
284 for loop_record in &face.loops {
285 for coedge in &loop_record.coedges {
286 for pair in curve_breaks(&coedge.pcurve)?.windows(2) {
287 for panel in rule::curve_panels(&coedge.pcurve, pair[0], pair[1])? {
291 for (parameter, weight) in panel.stations() {
292 let (point, tangent) = coedge.pcurve.deriv1(parameter)?;
293 total += weight * inner(point.x, point.y) * tangent.y;
294 }
295 }
296 }
297 }
298 }
299 for (end, start) in loop_closing_chords(face)? {
304 let step = start.y - end.y;
305 if step == 0.0 {
306 continue;
307 }
308 for index in 0..GAUSS_X.len() {
309 let fraction = 0.5 * (GAUSS_X[index] + 1.0);
310 let u = end.x + (start.x - end.x) * fraction;
311 let v = end.y + step * fraction;
312 total += 0.5 * GAUSS_W[index] * inner(u, v) * step;
313 }
314 }
315 Ok(total)
316}
317
318pub(super) fn face_moment(face: &FaceRecord, kind: Integrand) -> Result<f64, String> {
319 if is_affine(&face.surface)? {
320 return affine_moment(face, kind);
321 }
322 if let Some(values) = biperiodic_band_integral(face, &[kind])? {
323 return Ok(values[0]);
324 }
325 if is_untrimmed(face)? {
326 integrate_untrimmed(face, kind)
327 } else {
328 integrate_trimmed(face, kind)
329 }
330}
331
332pub(super) fn mat3_mul(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
335 let mut r = [[0.0f64; 3]; 3];
336 for i in 0..3 {
337 for j in 0..3 {
338 for k in 0..3 {
339 r[i][j] += a[i][k] * b[k][j];
340 }
341 }
342 }
343 r
344}
345
346pub(super) fn mat3_transpose(a: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
347 let mut r = [[0.0f64; 3]; 3];
348 for i in 0..3 {
349 for j in 0..3 {
350 r[i][j] = a[j][i];
351 }
352 }
353 r
354}
355
356pub(super) fn jacobi_eigen_symmetric_3x3(matrix: [[f64; 3]; 3]) -> ([f64; 3], [[f64; 3]; 3]) {
365 let mut a = [
367 [matrix[0][0], 0.0, 0.0],
368 [0.0, matrix[1][1], 0.0],
369 [0.0, 0.0, matrix[2][2]],
370 ];
371 a[0][1] = 0.5 * (matrix[0][1] + matrix[1][0]);
372 a[1][0] = a[0][1];
373 a[0][2] = 0.5 * (matrix[0][2] + matrix[2][0]);
374 a[2][0] = a[0][2];
375 a[1][2] = 0.5 * (matrix[1][2] + matrix[2][1]);
376 a[2][1] = a[1][2];
377
378 let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
379 let scale = a[0][0].abs() + a[1][1].abs() + a[2][2].abs() + 1.0;
380 for _sweep in 0..64 {
381 let pairs = [(0usize, 1usize), (0, 2), (1, 2)];
383 let (mut p, mut q, mut best) = (0usize, 1usize, 0.0f64);
384 for &(i, j) in &pairs {
385 if a[i][j].abs() > best {
386 best = a[i][j].abs();
387 p = i;
388 q = j;
389 }
390 }
391 if best <= 1e-18 * scale {
392 break;
393 }
394 let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
397 let t = if theta == 0.0 {
398 1.0
399 } else {
400 theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt())
401 };
402 let c = 1.0 / (t * t + 1.0).sqrt();
403 let s = t * c;
404 let mut j = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
406 j[p][p] = c;
407 j[q][q] = c;
408 j[p][q] = s;
409 j[q][p] = -s;
410 a = mat3_mul(mat3_transpose(j), mat3_mul(a, j));
412 v = mat3_mul(v, j);
413 }
414 ([a[0][0], a[1][1], a[2][2]], v)
415}
416
417pub(super) fn principal_frame(inertia: [[f64; 3]; 3]) -> ([f64; 3], [[f64; 3]; 3]) {
423 let (values, vectors) = jacobi_eigen_symmetric_3x3(inertia);
424 let mut order = [0usize, 1, 2];
426 order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
427 let mut moments = [0.0f64; 3];
428 let mut axes = [[0.0f64; 3]; 3];
429 for (slot, &k) in order.iter().enumerate() {
430 moments[slot] = values[k];
431 let mut axis = [vectors[0][k], vectors[1][k], vectors[2][k]];
432 let length = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
433 if length > 0.0 {
434 axis = [axis[0] / length, axis[1] / length, axis[2] / length];
435 }
436 axes[slot] = axis;
437 }
438 let cross = [
440 axes[0][1] * axes[1][2] - axes[0][2] * axes[1][1],
441 axes[0][2] * axes[1][0] - axes[0][0] * axes[1][2],
442 axes[0][0] * axes[1][1] - axes[0][1] * axes[1][0],
443 ];
444 let det = cross[0] * axes[2][0] + cross[1] * axes[2][1] + cross[2] * axes[2][2];
445 if det < 0.0 {
446 axes[2] = [-axes[2][0], -axes[2][1], -axes[2][2]];
447 }
448 (moments, axes)
449}
450
451pub fn solid_mass_properties_full(solid: &BrepSolid) -> Result<FullMassProperties, String> {
456 let base = solid_mass_properties(solid)?;
457 let token = super::profile::begin("solid_mass_properties_full.moments");
458 let result = solid_mass_properties_full_moments(solid, base);
459 super::profile::end(token);
460 result
461}
462
463fn solid_mass_properties_full_moments(
464 solid: &BrepSolid,
465 base: MassProperties,
466) -> Result<FullMassProperties, String> {
467 const MOMENT_KINDS: [Integrand; 9] = [
468 Integrand::MomentX,
469 Integrand::MomentY,
470 Integrand::MomentZ,
471 Integrand::SecondXX,
472 Integrand::SecondYY,
473 Integrand::SecondZZ,
474 Integrand::ProductXY,
475 Integrand::ProductXZ,
476 Integrand::ProductYZ,
477 ];
478 let mut moments = [0.0f64; 3];
479 let mut seconds = [0.0f64; 3];
480 let mut products = [0.0f64; 3];
481 for shell in &solid.shells {
482 for face in &shell.faces {
483 mass_profile(|p| p.faces += 1);
484 let results = if !is_affine(&face.surface)? && !is_untrimmed(face)? {
485 mass_profile(|p| p.trimmed_faces += 1);
486 integrate_trimmed_multi(face, &MOMENT_KINDS)?
489 } else {
490 MOMENT_KINDS
491 .iter()
492 .map(|kind| face_moment(face, *kind))
493 .collect::<Result<Vec<_>, _>>()?
494 };
495 moments[0] += results[0];
496 moments[1] += results[1];
497 moments[2] += results[2];
498 seconds[0] += results[3];
499 seconds[1] += results[4];
500 seconds[2] += results[5];
501 products[0] += results[6];
502 products[1] += results[7];
503 products[2] += results[8];
504 }
505 }
506 let volume = base.volume;
507 if volume.abs() <= 1e-30 {
508 return Err("solid_mass_properties_full: non-positive volume".into());
509 }
510 let centroid = Vec3::new(
511 moments[0] / volume,
512 moments[1] / volume,
513 moments[2] / volume,
514 );
515 let ixx =
517 seconds[1] + seconds[2] - volume * (centroid.y * centroid.y + centroid.z * centroid.z);
518 let iyy =
519 seconds[0] + seconds[2] - volume * (centroid.x * centroid.x + centroid.z * centroid.z);
520 let izz =
521 seconds[0] + seconds[1] - volume * (centroid.x * centroid.x + centroid.y * centroid.y);
522 let ixy = -(products[0] - volume * centroid.x * centroid.y);
523 let ixz = -(products[1] - volume * centroid.x * centroid.z);
524 let iyz = -(products[2] - volume * centroid.y * centroid.z);
525 let inertia = [[ixx, ixy, ixz], [ixy, iyy, iyz], [ixz, iyz, izz]];
526 let (principal_moments, principal_axes) = principal_frame(inertia);
527 Ok(FullMassProperties {
528 surface_area: base.surface_area,
529 volume,
530 centroid,
531 inertia,
532 principal_moments,
533 principal_axes,
534 })
535}