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_cached(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_cached(solid: &BrepSolid) -> Result<MassProperties, String> {
76 let identity = face_set_identity(solid);
77 if let Some((cached_identity, properties)) = solid.mass_properties_cache.entry.get() {
78 if cached_identity == identity {
79 if super::profile::mass_profile_enabled() {
80 let measured = solid_mass_properties_inner(solid)?;
81 assert_eq!(properties.volume.to_bits(), measured.volume.to_bits(),
82 "mass cache changed volume");
83 assert_eq!(properties.surface_area.to_bits(), measured.surface_area.to_bits(),
84 "mass cache changed area");
85 eprintln!("mass.cache hit faces={} verified=true", identity.faces);
86 }
87 return Ok(properties);
88 }
89 }
90 solid.mass_properties_cache.entry.set(None);
92 let properties = solid_mass_properties_inner(solid)?;
93 solid.mass_properties_cache.entry.set(Some((identity, properties)));
94 Ok(properties)
95}
96
97fn solid_mass_properties_inner(solid: &BrepSolid) -> Result<MassProperties, String> {
98 let mut properties = MassProperties {
99 surface_area: 0.0,
100 volume: 0.0,
101 };
102 let mut volume_compensation = 0.0;
103 let face_lines = super::profile::face_lines_enabled();
104 for (shell_index, shell) in solid.shells.iter().enumerate() {
105 if shell.faces.is_empty() {
106 continue;
107 }
108 let reference = shell_volume_reference(shell)?;
112 let kinds = [Integrand::Area, Integrand::VolumeAbout(reference)];
113 for (face_index, face) in shell.faces.iter().enumerate() {
114 mass_profile(|p| p.faces += 1);
115 let line_before = face_lines.then(super::profile::snapshot);
116 let line_started = face_lines.then(Instant::now);
117 let gate_started = super::profile::profile_started();
118 let (area, volume) = if let Some(values) = biperiodic_band_integral(face, &kinds)? {
119 mass_profile(|p| {
120 p.route_biperiodic += 1;
121 p.gate_ms += super::profile::elapsed_ms(gate_started);
122 });
123 (values[0], values[1] / 3.0)
124 } else if !is_affine(&face.surface)? && !is_untrimmed(face)? {
125 mass_profile(|p| {
126 p.trimmed_faces += 1;
127 p.gate_ms += super::profile::elapsed_ms(gate_started);
128 });
129 let values = integrate_trimmed_multi(face, &kinds)?;
131 (values[0], values[1] / 3.0)
132 } else if is_affine(&face.surface)? {
133 mass_profile(|p| {
134 p.route_affine += 1;
135 p.gate_ms += super::profile::elapsed_ms(gate_started);
136 });
137 affine_area_and_volume_about(face, reference)?
141 } else {
142 mass_profile(|p| {
143 p.route_untrimmed += 1;
144 p.gate_ms += super::profile::elapsed_ms(gate_started);
145 });
146 let started = super::profile::profile_started();
149 let values = integrate_untrimmed_multi(face, &kinds)?;
150 mass_profile(|p| p.untrimmed_ms += super::profile::elapsed_ms(started));
151 (values[0], values[1] / 3.0)
152 };
153 emit_face_line(
154 "solid_mass_properties",
155 shell_index,
156 face_index,
157 face,
158 line_before,
159 line_started,
160 );
161 properties.surface_area += area;
162 let next = properties.volume + volume;
163 if properties.volume.abs() >= volume.abs() {
164 volume_compensation += (properties.volume - next) + volume;
165 } else {
166 volume_compensation += (volume - next) + properties.volume;
167 }
168 properties.volume = next;
169 }
170 }
171 properties.volume += volume_compensation;
172 Ok(properties)
173}
174
175pub fn solid_signed_volume(solid: &BrepSolid) -> Result<f64, String> {
180 let token = super::profile::begin("solid_signed_volume");
181 let result = if solid.shells.len() == 1 && !solid.shells[0].faces.is_empty() {
182 match solid_mass_properties_cached(solid) {
183 Ok(properties) => {
184 if super::profile::mass_profile_enabled() {
185 let original = solid_signed_volume_uncached(solid)
186 .expect("shared mass measurement changed a signed-volume refusal");
187 assert_eq!(properties.volume.to_bits(), original.to_bits(),
188 "shared mass measurement changed signed volume");
189 }
190 Ok(properties.volume)
191 }
192 Err(_) => solid_signed_volume_uncached(solid),
194 }
195 } else {
196 solid_signed_volume_uncached(solid)
197 };
198 super::profile::end(token);
199 if let (Some(dump), Ok(volume)) = (super::profile::identity_dump(), result.as_ref()) {
200 super::profile::emit_identity(dump, "solid_signed_volume", solid, &[("signed_bits", *volume)]);
201 }
202 result
203}
204
205const SIGN_READING_MARGIN: f64 = 1e-6;
211
212pub fn solid_signed_volume_for_sign(solid: &BrepSolid, bar: f64) -> Result<f64, String> {
232 let enabled = match std::env::var("BREP_SPHERE_SIGN_BOUND") {
233 Ok(value) => value == "1" || value == "on",
234 Err(std::env::VarError::NotPresent) => true,
235 Err(_) => false,
236 };
237 solid_signed_volume_for_sign_with_bound(solid, bar, enabled)
238}
239
240pub(super) fn solid_signed_volume_for_sign_with_bound(solid: &BrepSolid, bar: f64, enabled: bool) -> Result<f64, String> {
241 if enabled {
242 if let Some(lower) = super::sphere_sign::positive_volume_bound(solid) {
243 if bar.is_finite() && lower > bar {
244 if std::env::var("BREP_PROFILE").is_ok() {
245 eprintln!("mass.sign_certificate kind=trimmed_spheres lower={lower:.9} bar={bar:.3e}");
246 }
247 return Ok(lower);
248 }
249 }
250 }
251 let token = super::profile::begin("solid_signed_volume_for_sign");
252 let outcome = super::trimmed::with_straight_corrections_skipped(|| {
253 let (mut reading, mut magnitude) = (0.0, 0.0);
254 for shell in &solid.shells {
255 let (volume, faces) = shell_volume_sign_reading(shell)?;
256 reading += volume;
257 magnitude += faces;
258 }
259 Ok::<(f64, f64), String>((reading, magnitude))
260 });
261 super::profile::end(token);
262 let (reading, magnitude) = outcome?;
263 if (reading - bar).abs() > SIGN_READING_MARGIN * magnitude {
264 return Ok(reading);
265 }
266 solid_signed_volume(solid)
267}
268
269fn shell_volume_sign_reading(shell: &ShellRecord) -> Result<(f64, f64), String> {
273 let reference = shell_volume_reference(shell)?;
274 if std::env::var_os("BREP_PROFILE").is_some() {
275 eprintln!(
276 "mass.sign_reference faces={} reference=({:.9},{:.9},{:.9})",
277 shell.faces.len(), reference.x, reference.y, reference.z
278 );
279 }
280 let mut volume = 0.0;
281 let mut compensation = 0.0;
282 let mut magnitude = 0.0;
283 let face_lines = super::profile::face_lines_enabled();
284 for (face_index, face) in shell.faces.iter().enumerate() {
285 mass_profile(|p| p.faces += 1);
286 let line_before = face_lines.then(super::profile::snapshot);
287 let line_started = face_lines.then(Instant::now);
288 let contribution = match crate::reading_memo::memoized(
297 crate::reading_memo::Reading::SignContribution,
298 super::identity::face_contribution_identity(face, reference),
299 || face_volume_contribution_about(face, reference).ok(),
300 ) {
301 Some(contribution) => contribution,
302 None => face_volume_contribution_about(face, reference)?,
303 };
304 emit_face_line("shell_volume_sign_reading", 0, face_index, face, line_before, line_started);
305 magnitude += contribution.abs();
306 let next = volume + contribution;
307 if volume.abs() >= contribution.abs() {
308 compensation += (volume - next) + contribution;
309 } else {
310 compensation += (contribution - next) + volume;
311 }
312 volume = next;
313 }
314 Ok((volume + compensation, magnitude))
315}
316
317fn solid_signed_volume_uncached(solid: &BrepSolid) -> Result<f64, String> {
318 let mut volume = 0.0;
319 for shell in &solid.shells {
320 volume += shell_signed_volume(shell)?;
321 }
322 Ok(volume)
323}
324
325pub(super) fn shell_volume_reference(shell: &ShellRecord) -> Result<Vec3, String> {
326 let finite = |point: Vec3| point.x.is_finite() && point.y.is_finite() && point.z.is_finite();
327 for face in &shell.faces {
328 if let Some(coedge) = face
329 .loops
330 .first()
331 .and_then(|loop_record| loop_record.coedges.first())
332 {
333 if let Ok(domain) = coedge.pcurve.domain() {
334 if let Ok(uv) = coedge.pcurve.evaluate(domain[0]) {
335 if let Ok(point) = face.surface.evaluate(uv.x, uv.y) {
336 if finite(point) {
337 return Ok(point);
338 }
339 }
340 }
341 }
342 }
343 if let Ok((u_breaks, v_breaks)) = surface_breaks(&face.surface) {
344 let u = 0.5 * (u_breaks[0] + u_breaks[u_breaks.len() - 1]);
345 let v = 0.5 * (v_breaks[0] + v_breaks[v_breaks.len() - 1]);
346 if let Ok(point) = face.surface.evaluate(u, v) {
347 if finite(point) {
348 return Ok(point);
349 }
350 }
351 }
352 for row in &face.surface.control_points {
353 for control in row {
354 if let Ok(point) = control.point() {
355 if finite(point) {
356 return Ok(point);
357 }
358 }
359 }
360 }
361 }
362 Err("mass_properties: shell has no finite geometric reference".to_string())
363}
364
365pub(crate) fn shell_signed_volume(shell: &ShellRecord) -> Result<f64, String> {
369 let reference = shell_volume_reference(shell)?;
370
371 let mut volume = 0.0;
377 let mut compensation = 0.0;
378 let face_lines = super::profile::face_lines_enabled();
379 for (face_index, face) in shell.faces.iter().enumerate() {
380 mass_profile(|p| p.faces += 1);
381 let line_before = face_lines.then(super::profile::snapshot);
382 let line_started = face_lines.then(Instant::now);
383 let contribution = face_volume_contribution_about(face, reference)?;
384 emit_face_line(
385 "shell_signed_volume",
386 0,
387 face_index,
388 face,
389 line_before,
390 line_started,
391 );
392 let next = volume + contribution;
393 if volume.abs() >= contribution.abs() {
394 compensation += (volume - next) + contribution;
395 } else {
396 compensation += (contribution - next) + volume;
397 }
398 volume = next;
399 }
400 Ok(volume + compensation)
401}
402
403pub(super) fn affine_moment(face: &FaceRecord, kind: Integrand) -> Result<f64, String> {
410 let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
411 let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
412 let [u0, u1] = ku.domain();
413 let [v0, v1] = kv.domain();
414 let points = &face.surface.control_points;
415 let p00 = points[0][0].point()?;
416 let p10 = points[1][0].point()?;
417 let p01 = points[0][1].point()?;
418 let du = p10.sub(p00).scale(1.0 / (u1 - u0));
419 let dv = p01.sub(p00).scale(1.0 / (v1 - v0));
420 let weighted_normal = du.cross(dv);
421 let g = |u: f64, v: f64| {
422 let point = p00.add(du.scale(u - u0)).add(dv.scale(v - v0));
423 integrand_value(kind, point, weighted_normal)
424 };
425 let inner = |u: f64, v: f64| {
426 let half = (u - u0) * 0.5;
427 let middle = (u + u0) * 0.5;
428 let mut sum = 0.0;
429 for index in 0..GAUSS_X.len() {
430 sum += GAUSS_W[index] * g(middle + half * GAUSS_X[index], v);
431 }
432 sum * half
433 };
434 let mut total = 0.0;
435 for loop_record in &face.loops {
436 for coedge in &loop_record.coedges {
437 for pair in curve_breaks(&coedge.pcurve)?.windows(2) {
438 for panel in rule::curve_panels(&coedge.pcurve, pair[0], pair[1])? {
442 for (parameter, weight) in panel.stations() {
443 let (point, tangent) = coedge.pcurve.deriv1(parameter)?;
444 total += weight * inner(point.x, point.y) * tangent.y;
445 }
446 }
447 }
448 }
449 }
450 for (end, start) in loop_closing_chords(face)? {
455 let step = start.y - end.y;
456 if step == 0.0 {
457 continue;
458 }
459 for index in 0..GAUSS_X.len() {
460 let fraction = 0.5 * (GAUSS_X[index] + 1.0);
461 let u = end.x + (start.x - end.x) * fraction;
462 let v = end.y + step * fraction;
463 total += 0.5 * GAUSS_W[index] * inner(u, v) * step;
464 }
465 }
466 Ok(total)
467}
468
469pub(super) fn face_moment(face: &FaceRecord, kind: Integrand) -> Result<f64, String> {
470 if is_affine(&face.surface)? {
471 return affine_moment(face, kind);
472 }
473 if let Some(values) = biperiodic_band_integral(face, &[kind])? {
474 return Ok(values[0]);
475 }
476 if is_untrimmed(face)? {
477 integrate_untrimmed(face, kind)
478 } else {
479 integrate_trimmed(face, kind)
480 }
481}
482
483pub(super) fn mat3_mul(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
486 let mut r = [[0.0f64; 3]; 3];
487 for i in 0..3 {
488 for j in 0..3 {
489 for k in 0..3 {
490 r[i][j] += a[i][k] * b[k][j];
491 }
492 }
493 }
494 r
495}
496
497pub(super) fn mat3_transpose(a: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
498 let mut r = [[0.0f64; 3]; 3];
499 for i in 0..3 {
500 for j in 0..3 {
501 r[i][j] = a[j][i];
502 }
503 }
504 r
505}
506
507pub(super) fn jacobi_eigen_symmetric_3x3(matrix: [[f64; 3]; 3]) -> ([f64; 3], [[f64; 3]; 3]) {
516 let mut a = [
518 [matrix[0][0], 0.0, 0.0],
519 [0.0, matrix[1][1], 0.0],
520 [0.0, 0.0, matrix[2][2]],
521 ];
522 a[0][1] = 0.5 * (matrix[0][1] + matrix[1][0]);
523 a[1][0] = a[0][1];
524 a[0][2] = 0.5 * (matrix[0][2] + matrix[2][0]);
525 a[2][0] = a[0][2];
526 a[1][2] = 0.5 * (matrix[1][2] + matrix[2][1]);
527 a[2][1] = a[1][2];
528
529 let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
530 let scale = a[0][0].abs() + a[1][1].abs() + a[2][2].abs() + 1.0;
531 for _sweep in 0..64 {
532 let pairs = [(0usize, 1usize), (0, 2), (1, 2)];
534 let (mut p, mut q, mut best) = (0usize, 1usize, 0.0f64);
535 for &(i, j) in &pairs {
536 if a[i][j].abs() > best {
537 best = a[i][j].abs();
538 p = i;
539 q = j;
540 }
541 }
542 if best <= 1e-18 * scale {
543 break;
544 }
545 let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
548 let t = if theta == 0.0 {
549 1.0
550 } else {
551 theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt())
552 };
553 let c = 1.0 / (t * t + 1.0).sqrt();
554 let s = t * c;
555 let mut j = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
557 j[p][p] = c;
558 j[q][q] = c;
559 j[p][q] = s;
560 j[q][p] = -s;
561 a = mat3_mul(mat3_transpose(j), mat3_mul(a, j));
563 v = mat3_mul(v, j);
564 }
565 ([a[0][0], a[1][1], a[2][2]], v)
566}
567
568pub(super) fn principal_frame(inertia: [[f64; 3]; 3]) -> ([f64; 3], [[f64; 3]; 3]) {
574 let (values, vectors) = jacobi_eigen_symmetric_3x3(inertia);
575 let mut order = [0usize, 1, 2];
577 order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
578 let mut moments = [0.0f64; 3];
579 let mut axes = [[0.0f64; 3]; 3];
580 for (slot, &k) in order.iter().enumerate() {
581 moments[slot] = values[k];
582 let mut axis = [vectors[0][k], vectors[1][k], vectors[2][k]];
583 let length = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
584 if length > 0.0 {
585 axis = [axis[0] / length, axis[1] / length, axis[2] / length];
586 }
587 axes[slot] = axis;
588 }
589 let cross = [
591 axes[0][1] * axes[1][2] - axes[0][2] * axes[1][1],
592 axes[0][2] * axes[1][0] - axes[0][0] * axes[1][2],
593 axes[0][0] * axes[1][1] - axes[0][1] * axes[1][0],
594 ];
595 let det = cross[0] * axes[2][0] + cross[1] * axes[2][1] + cross[2] * axes[2][2];
596 if det < 0.0 {
597 axes[2] = [-axes[2][0], -axes[2][1], -axes[2][2]];
598 }
599 (moments, axes)
600}
601
602pub fn solid_mass_properties_full(solid: &BrepSolid) -> Result<FullMassProperties, String> {
607 let base = solid_mass_properties(solid)?;
608 let token = super::profile::begin("solid_mass_properties_full.moments");
609 let result = solid_mass_properties_full_moments(solid, base);
610 super::profile::end(token);
611 result
612}
613
614fn solid_mass_properties_full_moments(
615 solid: &BrepSolid,
616 base: MassProperties,
617) -> Result<FullMassProperties, String> {
618 const MOMENT_KINDS: [Integrand; 9] = [
619 Integrand::MomentX,
620 Integrand::MomentY,
621 Integrand::MomentZ,
622 Integrand::SecondXX,
623 Integrand::SecondYY,
624 Integrand::SecondZZ,
625 Integrand::ProductXY,
626 Integrand::ProductXZ,
627 Integrand::ProductYZ,
628 ];
629 let mut moments = [0.0f64; 3];
630 let mut seconds = [0.0f64; 3];
631 let mut products = [0.0f64; 3];
632 for shell in &solid.shells {
633 for face in &shell.faces {
634 mass_profile(|p| p.faces += 1);
635 let results = if !is_affine(&face.surface)? && !is_untrimmed(face)? {
636 mass_profile(|p| p.trimmed_faces += 1);
637 integrate_trimmed_multi(face, &MOMENT_KINDS)?
640 } else {
641 MOMENT_KINDS
642 .iter()
643 .map(|kind| face_moment(face, *kind))
644 .collect::<Result<Vec<_>, _>>()?
645 };
646 moments[0] += results[0];
647 moments[1] += results[1];
648 moments[2] += results[2];
649 seconds[0] += results[3];
650 seconds[1] += results[4];
651 seconds[2] += results[5];
652 products[0] += results[6];
653 products[1] += results[7];
654 products[2] += results[8];
655 }
656 }
657 let volume = base.volume;
658 if volume.abs() <= 1e-30 {
659 return Err("solid_mass_properties_full: non-positive volume".into());
660 }
661 let centroid = Vec3::new(
662 moments[0] / volume,
663 moments[1] / volume,
664 moments[2] / volume,
665 );
666 let ixx =
668 seconds[1] + seconds[2] - volume * (centroid.y * centroid.y + centroid.z * centroid.z);
669 let iyy =
670 seconds[0] + seconds[2] - volume * (centroid.x * centroid.x + centroid.z * centroid.z);
671 let izz =
672 seconds[0] + seconds[1] - volume * (centroid.x * centroid.x + centroid.y * centroid.y);
673 let ixy = -(products[0] - volume * centroid.x * centroid.y);
674 let ixz = -(products[1] - volume * centroid.x * centroid.z);
675 let iyz = -(products[2] - volume * centroid.y * centroid.z);
676 let inertia = [[ixx, ixy, ixz], [ixy, iyy, iyz], [ixz, iyz, izz]];
677 let (principal_moments, principal_axes) = principal_frame(inertia);
678 Ok(FullMassProperties {
679 surface_area: base.surface_area,
680 volume,
681 centroid,
682 inertia,
683 principal_moments,
684 principal_axes,
685 })
686}
687