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 result.is_ok() && sign_terms_oracle_enabled() && solid.kind == crate::BodyKind::Solid
60 && !solid.shells.is_empty() && solid.shells.iter().all(|shell| !shell.faces.is_empty())
61 {
62 verify_solid_sign_terms(solid)?;
63 }
64 if let (Some(dump), Ok(properties)) = (super::profile::identity_dump(), result.as_ref()) {
65 super::profile::emit_identity(
66 dump,
67 "solid_mass_properties",
68 solid,
69 &[
70 ("volume_bits", properties.volume),
71 ("area_bits", properties.surface_area),
72 ],
73 );
74 }
75 result
76}
77
78fn solid_mass_properties_cached(solid: &BrepSolid) -> Result<MassProperties, String> {
81 let identity = face_set_identity(solid);
82 if let Some((cached_identity, properties)) = solid.mass_properties_cache.entry.get() {
83 if cached_identity == identity {
84 if super::profile::mass_profile_enabled() {
85 let measured = solid_mass_properties_inner(solid)?;
86 assert_eq!(properties.volume.to_bits(), measured.volume.to_bits(),
87 "mass cache changed volume");
88 assert_eq!(properties.surface_area.to_bits(), measured.surface_area.to_bits(),
89 "mass cache changed area");
90 eprintln!("mass.cache hit faces={} verified=true", identity.faces);
91 }
92 return Ok(properties);
93 }
94 }
95 solid.mass_properties_cache.entry.set(None);
97 let properties = solid_mass_properties_inner(solid)?;
98 solid.mass_properties_cache.entry.set(Some((identity, properties)));
99 Ok(properties)
100}
101
102fn solid_mass_properties_inner(solid: &BrepSolid) -> Result<MassProperties, String> {
103 let mut properties = MassProperties {
104 surface_area: 0.0,
105 volume: 0.0,
106 };
107 let mut volume_compensation = 0.0;
108 let face_lines = super::profile::face_lines_enabled();
109 for (shell_index, shell) in solid.shells.iter().enumerate() {
110 if shell.faces.is_empty() {
111 continue;
112 }
113 let reference = shell_volume_reference(shell)?;
117 let kinds = [Integrand::Area, Integrand::VolumeAbout(reference)];
118 for (face_index, face) in shell.faces.iter().enumerate() {
119 mass_profile(|p| p.faces += 1);
120 let line_before = face_lines.then(super::profile::snapshot);
121 let line_started = face_lines.then(Instant::now);
122 let gate_started = super::profile::profile_started();
123 let (area, volume) = if let Some(values) = biperiodic_band_integral(face, &kinds)? {
124 mass_profile(|p| {
125 p.route_biperiodic += 1;
126 p.gate_ms += super::profile::elapsed_ms(gate_started);
127 });
128 (values[0], values[1] / 3.0)
129 } else if !is_affine(&face.surface)? && !is_untrimmed(face)? {
130 mass_profile(|p| {
131 p.trimmed_faces += 1;
132 p.gate_ms += super::profile::elapsed_ms(gate_started);
133 });
134 let values = integrate_trimmed_multi(face, &kinds)?;
136 (values[0], values[1] / 3.0)
137 } else if is_affine(&face.surface)? {
138 mass_profile(|p| {
139 p.route_affine += 1;
140 p.gate_ms += super::profile::elapsed_ms(gate_started);
141 });
142 affine_area_and_volume_about(face, reference)?
146 } else {
147 mass_profile(|p| {
148 p.route_untrimmed += 1;
149 p.gate_ms += super::profile::elapsed_ms(gate_started);
150 });
151 let started = super::profile::profile_started();
154 let values = integrate_untrimmed_multi(face, &kinds)?;
155 mass_profile(|p| p.untrimmed_ms += super::profile::elapsed_ms(started));
156 (values[0], values[1] / 3.0)
157 };
158 emit_face_line(
159 "solid_mass_properties",
160 shell_index,
161 face_index,
162 face,
163 line_before,
164 line_started,
165 );
166 properties.surface_area += area;
167 let next = properties.volume + volume;
168 if properties.volume.abs() >= volume.abs() {
169 volume_compensation += (properties.volume - next) + volume;
170 } else {
171 volume_compensation += (volume - next) + properties.volume;
172 }
173 properties.volume = next;
174 }
175 }
176 properties.volume += volume_compensation;
177 Ok(properties)
178}
179
180pub fn solid_signed_volume(solid: &BrepSolid) -> Result<f64, String> {
185 let token = super::profile::begin("solid_signed_volume");
186 let result = if solid.shells.len() == 1 && !solid.shells[0].faces.is_empty() {
187 match solid_mass_properties_cached(solid) {
188 Ok(properties) => {
189 if super::profile::mass_profile_enabled() {
190 let original = solid_signed_volume_uncached(solid)
191 .expect("shared mass measurement changed a signed-volume refusal");
192 assert_eq!(properties.volume.to_bits(), original.to_bits(),
193 "shared mass measurement changed signed volume");
194 }
195 Ok(properties.volume)
196 }
197 Err(_) => solid_signed_volume_uncached(solid),
199 }
200 } else {
201 solid_signed_volume_uncached(solid)
202 };
203 super::profile::end(token);
204 if result.is_ok() && sign_terms_oracle_enabled() && solid.kind == crate::BodyKind::Solid {
205 verify_solid_sign_terms(solid)?;
206 }
207 if let (Some(dump), Ok(volume)) = (super::profile::identity_dump(), result.as_ref()) {
208 super::profile::emit_identity(dump, "solid_signed_volume", solid, &[("signed_bits", *volume)]);
209 }
210 result
211}
212
213const SIGN_READING_MARGIN: f64 = 1e-6;
219
220pub fn signed_volume_against(solid: &BrepSolid, bar: f64) -> Result<f64, String> {
227 static EXACT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
228 if *EXACT.get_or_init(|| std::env::var("BREP_SIGN_READERS_EXACT").as_deref() == Ok("1")) {
229 return solid_signed_volume(solid);
230 }
231 solid_signed_volume_for_sign(solid, bar)
232}
233
234pub fn solid_signed_volume_for_sign(solid: &BrepSolid, bar: f64) -> Result<f64, String> {
255 let enabled = match std::env::var("BREP_SPHERE_SIGN_BOUND") {
256 Ok(value) => value == "1" || value == "on",
257 Err(std::env::VarError::NotPresent) => true,
258 Err(_) => false,
259 };
260 let result = solid_signed_volume_for_sign_with_bound(solid, bar, enabled);
261 if result.is_ok() && sign_terms_oracle_enabled() {
262 verify_solid_sign_terms(solid)?;
263 }
264 result
265}
266
267pub(super) fn solid_signed_volume_for_sign_with_bound(solid: &BrepSolid, bar: f64, enabled: bool) -> Result<f64, String> {
268 if enabled {
269 if let Some(lower) = super::sphere_sign::positive_volume_bound(solid) {
270 if bar.is_finite() && lower > bar {
271 if crate::profile::profile_enabled() {
272 profile_line!("mass.sign_certificate kind=trimmed_spheres lower={lower:.9} bar={bar:.3e}");
273 }
274 return Ok(lower);
275 }
276 }
277 }
278 if sliver_bound_enabled() {
279 if let Some(lower) = sliver_bound_certificate(solid, bar) {
280 return Ok(lower);
281 }
282 }
283 let token = super::profile::begin("solid_signed_volume_for_sign");
284 let outcome = solid_sign_terms_reading(solid);
285 super::profile::end(token);
286 let (reading, magnitude) = outcome?;
287 if (reading - bar).abs() > SIGN_READING_MARGIN * magnitude {
288 return Ok(reading);
289 }
290 solid_signed_volume(solid)
291}
292
293fn sliver_bound_enabled() -> bool {
300 switch!(live "BREP_SIGN_SLIVER_BOUND", true)
301}
302
303
304pub(super) fn sliver_bound_certificate(solid: &BrepSolid, bar: f64) -> Option<f64> {
325 if !bar.is_finite() {
326 return None;
327 }
328 let token = super::profile::begin("solid_sign_sliver_bound");
329 let outcome = super::trimmed::with_straight_corrections_skipped(|| -> Option<(f64, f64, f64, f64, [usize; 3])> {
330 let (mut reading, mut magnitude, mut bound, mut scale) = (0.0, 0.0, 0.0, 0.0);
331 let mut counts = [0usize; 3]; for shell in &solid.shells {
333 let reference = shell_volume_reference(shell).ok()?;
334 let (mut volume, mut compensation) = (0.0f64, 0.0f64);
335 for face in &shell.faces {
336 let local = face_volume_reference(face).unwrap_or(reference);
337 let identity = super::identity::face_contribution_identity(face, local);
338 let offset = local.sub(reference);
339 let (terms, face_bound, face_scale) = match crate::reading_memo::peek_sign_terms(identity) {
340 Some(terms) => {
341 counts[0] += 1;
342 (terms?, 0.0, 0.0)
343 }
344 None => match crate::reading_memo::memoized_bounded_sign_terms(identity, || bounded_face_terms(face, local)) {
345 Some((terms, per_offset, fixed, rounding)) => {
346 counts[1] += 1;
347 (terms, fixed + per_offset * offset.length(), rounding)
348 }
349 None => {
350 counts[2] += 1;
351 let terms = crate::reading_memo::memoized_sign_terms(identity, || {
352 face_volume_terms_about(face, local).ok()
353 })?;
354 (terms, 0.0, 0.0)
355 }
356 },
357 };
358 let area = Vec3::new(terms[1], terms[2], terms[3]);
359 let contribution = terms[0] + offset.dot(area);
360 magnitude += contribution.abs();
361 bound += face_bound;
362 scale += terms[0].abs() + offset.length() * area.length() + face_bound + face_scale * (1.0 + offset.length());
363 let next = volume + contribution;
364 if volume.abs() >= contribution.abs() {
365 compensation += (volume - next) + contribution;
366 } else {
367 compensation += (contribution - next) + volume;
368 }
369 volume = next;
370 }
371 reading += volume + compensation;
372 }
373 Some((reading, magnitude, bound, scale, counts))
374 });
375 super::profile::end(token);
376 let (reading, magnitude, bound, scale, counts) = outcome?;
377 let slack = 1e-9 * scale;
382 let lower = reading - bound - slack;
383 let certified = [reading, magnitude, bound, scale, lower].iter().all(|v| v.is_finite())
384 && lower > bar
385 && lower - bar > SIGN_READING_MARGIN * (magnitude + bound + slack);
386 if crate::profile::profile_enabled() {
387 profile_line!(
388 "mass.sign_certificate kind=sliver_bound certified={certified} cells={reading:.9e} bound={bound:.3e} slack={slack:.3e} magnitude={magnitude:.3e} held={} bounded={} exact={}",
389 counts[0], counts[1], counts[2]
390 );
391 }
392 certified.then_some(lower)
393}
394
395pub(super) fn bounded_face_terms(face: &FaceRecord, local: Vec3) -> Option<([f64; 4], f64, f64, f64)> {
402 let (terms, measure) = super::trimmed::with_sliver_measure(|| face_volume_terms_about(face, local));
403 let terms = terms.ok()?;
404 if measure.declined || !terms.iter().all(|v| v.is_finite()) {
405 return None;
406 }
407 if measure.measure == 0.0 {
408 return Some((terms, 0.0, 0.0, measure.complement / 3.0));
409 }
410 let (n, r) = surface_flux_bounds(&face.surface, local)?;
411 let per_offset = measure.measure * n / 3.0;
412 let fixed = per_offset * r;
413 [per_offset, fixed].iter().all(|v| v.is_finite()).then_some((terms, per_offset, fixed, measure.complement / 3.0))
414}
415
416pub(super) fn surface_flux_bounds(surface: &crate::NurbsSurface, center: Vec3) -> Option<(f64, f64)> {
428 let net = &surface.control_points;
429 let rows = net.len();
430 let columns = net.first()?.len();
431 if rows == 0 || columns == 0 || net.iter().any(|row| row.len() != columns) {
432 return None;
433 }
434 let relative = |i: usize, j: usize| -> Option<[f64; 4]> {
435 let q = net[i][j];
436 let values = [q.x - center.x * q.w, q.y - center.y * q.w, q.z - center.z * q.w, q.w];
437 (values.iter().all(|v| v.is_finite()) && q.w > 0.0).then_some(values)
438 };
439 let mut w_min = f64::INFINITY;
440 let mut r = 0.0f64;
441 for i in 0..rows {
442 for j in 0..columns {
443 let q = relative(i, j)?;
444 w_min = w_min.min(q[3]);
445 r = r.max((q[0] * q[0] + q[1] * q[1] + q[2] * q[2]).sqrt() / q[3]);
446 }
447 }
448 let derivative = |along_u: bool| -> Option<(f64, f64)> {
450 let (degree, knots, count) = if along_u {
451 (surface.degree_u, &surface.knots_u, rows)
452 } else {
453 (surface.degree_v, &surface.knots_v, columns)
454 };
455 if knots.len() < count + degree + 1 {
456 return None;
457 }
458 let (mut point, mut weight) = (0.0f64, 0.0f64);
459 if degree == 0 {
460 return Some((0.0, 0.0));
461 }
462 for k in 0..count.saturating_sub(1) {
463 let span = knots[k + degree + 1] - knots[k + 1];
464 if !(span > 0.0) {
465 continue;
466 }
467 let factor = degree as f64 / span;
468 let others = if along_u { columns } else { rows };
469 for other in 0..others {
470 let (a, b) = if along_u {
471 (relative(k, other)?, relative(k + 1, other)?)
472 } else {
473 (relative(other, k)?, relative(other, k + 1)?)
474 };
475 let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2], b[3] - a[3]];
476 point = point.max(factor * (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt());
477 weight = weight.max(factor * d[3].abs());
478 }
479 }
480 Some((point, weight))
481 };
482 let (xu, wu) = derivative(true)?;
483 let (xv, wv) = derivative(false)?;
484 let su = (xu + wu * r) / w_min;
485 let sv = (xv + wv * r) / w_min;
486 let n = su * sv;
487 let (n, r) = (n * (1.0 + 1e-9), r * (1.0 + 1e-9));
489 (n.is_finite() && r.is_finite() && w_min.is_finite()).then_some((n, r))
490}
491
492fn solid_sign_terms_reading(solid: &BrepSolid) -> Result<(f64, f64), String> {
497 super::trimmed::with_straight_corrections_skipped(|| {
498 let (mut reading, mut magnitude) = (0.0, 0.0);
499 for (shell_index, shell) in solid.shells.iter().enumerate() {
500 let (volume, faces) = shell_volume_sign_reading(shell, shell_index)?;
501 reading += volume;
502 magnitude += faces;
503 }
504 Ok((reading, magnitude))
505 })
506}
507
508fn sign_terms_oracle_enabled() -> bool {
509 switch!(live "BREP_SIGN_TERMS_ORACLE", false)
510}
511
512fn verify_solid_sign_terms(solid: &BrepSolid) -> Result<(), String> {
515 let token = super::profile::begin("solid_sign_terms_oracle");
516 let reading = solid_sign_terms_reading(solid).map(|pair| pair.0);
517 super::profile::end(token);
518 check_sign_terms(solid, reading?)
519}
520
521pub(super) fn check_sign_terms(solid: &BrepSolid, reading: f64) -> Result<(), String> {
522 let exact = solid_signed_volume_uncached(solid)?;
523 let error = (reading - exact).abs();
524 let bar = 1e-9 * exact.abs();
525 if !reading.is_finite() || !exact.is_finite() || error > bar {
526 return Err(format!("sign terms oracle: retained {reading:.17e}, whole-body {exact:.17e}, error {error:.3e} exceeds {bar:.3e}"));
527 }
528 super::profile::emit_sign_oracle(solid, reading, exact)?;
529 Ok(())
530}
531
532fn shell_volume_sign_reading(shell: &ShellRecord, shell_index: usize) -> Result<(f64, f64), String> {
536 let reference = shell_volume_reference(shell)?;
537 if crate::profile::profile_enabled() {
538 profile_line!(
539 "mass.sign_reference faces={} reference=({:.9},{:.9},{:.9})",
540 shell.faces.len(), reference.x, reference.y, reference.z
541 );
542 }
543 let mut volume = 0.0;
544 let mut compensation = 0.0;
545 let mut magnitude = 0.0;
546 let face_lines = super::profile::face_lines_enabled();
547 for (face_index, face) in shell.faces.iter().enumerate() {
548 mass_profile(|p| p.faces += 1);
549 let line_before = face_lines.then(super::profile::snapshot);
550 let line_started = face_lines.then(Instant::now);
551 let local = face_volume_reference(face).unwrap_or(reference);
557 let hash_started = super::profile::profile_started();
558 let identity = super::identity::face_contribution_identity(face, local);
559 let hash_ms = super::profile::elapsed_ms(hash_started);
560 let mut reread = false;
561 let terms = match crate::reading_memo::memoized_sign_terms(identity, || {
562 reread = true;
563 face_volume_terms_about(face, local).ok()
564 }) {
565 Some(terms) => terms,
566 None => face_volume_terms_about(face, local)?,
567 };
568 let contribution = terms[0] + local.sub(reference).dot(Vec3::new(terms[1], terms[2], terms[3]));
569 super::profile::sign_face(shell_index, face, reread, hash_ms);
570 emit_face_line("shell_volume_sign_reading", shell_index, face_index, face, line_before, line_started);
571 magnitude += contribution.abs();
572 let next = volume + contribution;
573 if volume.abs() >= contribution.abs() {
574 compensation += (volume - next) + contribution;
575 } else {
576 compensation += (contribution - next) + volume;
577 }
578 volume = next;
579 }
580 Ok((volume + compensation, magnitude))
581}
582
583fn solid_signed_volume_uncached(solid: &BrepSolid) -> Result<f64, String> {
584 let mut volume = 0.0;
585 for shell in &solid.shells {
586 volume += shell_signed_volume(shell)?;
587 }
588 Ok(volume)
589}
590
591pub(super) fn shell_volume_reference(shell: &ShellRecord) -> Result<Vec3, String> {
592 shell.faces.iter().find_map(face_volume_reference)
593 .ok_or_else(|| "mass_properties: shell has no finite geometric reference".to_string())
594}
595
596pub(super) fn face_volume_reference(face: &FaceRecord) -> Option<Vec3> {
598 let finite = |point: Vec3| point.x.is_finite() && point.y.is_finite() && point.z.is_finite();
599 if let Some(coedge) = face
600 .loops
601 .first()
602 .and_then(|loop_record| loop_record.coedges.first())
603 {
604 if let Ok(domain) = coedge.pcurve.domain() {
605 if let Ok(uv) = coedge.pcurve.evaluate(domain[0]) {
606 if let Ok(point) = face.surface.evaluate(uv.x, uv.y) {
607 if finite(point) {
608 return Some(point);
609 }
610 }
611 }
612 }
613 }
614 if let Ok((u_breaks, v_breaks)) = surface_breaks(&face.surface) {
615 let u = 0.5 * (u_breaks[0] + u_breaks[u_breaks.len() - 1]);
616 let v = 0.5 * (v_breaks[0] + v_breaks[v_breaks.len() - 1]);
617 if let Ok(point) = face.surface.evaluate(u, v) {
618 if finite(point) {
619 return Some(point);
620 }
621 }
622 }
623 for row in &face.surface.control_points {
624 for control in row {
625 if let Ok(point) = control.point() {
626 if finite(point) {
627 return Some(point);
628 }
629 }
630 }
631 }
632 None
633}
634
635pub(crate) fn shell_signed_volume(shell: &ShellRecord) -> Result<f64, String> {
639 let reference = shell_volume_reference(shell)?;
640
641 let mut volume = 0.0;
647 let mut compensation = 0.0;
648 let face_lines = super::profile::face_lines_enabled();
649 for (face_index, face) in shell.faces.iter().enumerate() {
650 mass_profile(|p| p.faces += 1);
651 let line_before = face_lines.then(super::profile::snapshot);
652 let line_started = face_lines.then(Instant::now);
653 let contribution = face_volume_contribution_about(face, reference)?;
654 emit_face_line(
655 "shell_signed_volume",
656 0,
657 face_index,
658 face,
659 line_before,
660 line_started,
661 );
662 let next = volume + contribution;
663 if volume.abs() >= contribution.abs() {
664 compensation += (volume - next) + contribution;
665 } else {
666 compensation += (contribution - next) + volume;
667 }
668 volume = next;
669 }
670 Ok(volume + compensation)
671}
672
673pub(super) fn affine_moment(face: &FaceRecord, kind: Integrand) -> Result<f64, String> {
680 let ku = crate::KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
681 let kv = crate::KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
682 let [u0, u1] = ku.domain();
683 let [v0, v1] = kv.domain();
684 let points = &face.surface.control_points;
685 let p00 = points[0][0].point()?;
686 let p10 = points[1][0].point()?;
687 let p01 = points[0][1].point()?;
688 let du = p10.sub(p00).scale(1.0 / (u1 - u0));
689 let dv = p01.sub(p00).scale(1.0 / (v1 - v0));
690 let weighted_normal = du.cross(dv);
691 let g = |u: f64, v: f64| {
692 let point = p00.add(du.scale(u - u0)).add(dv.scale(v - v0));
693 integrand_value(kind, point, weighted_normal)
694 };
695 let inner = |u: f64, v: f64| {
696 let half = (u - u0) * 0.5;
697 let middle = (u + u0) * 0.5;
698 let mut sum = 0.0;
699 for index in 0..GAUSS_X.len() {
700 sum += GAUSS_W[index] * g(middle + half * GAUSS_X[index], v);
701 }
702 sum * half
703 };
704 let mut total = 0.0;
705 for loop_record in &face.loops {
706 for coedge in &loop_record.coedges {
707 for pair in curve_breaks(&coedge.pcurve)?.windows(2) {
708 for panel in rule::curve_panels(&coedge.pcurve, pair[0], pair[1])? {
712 for (parameter, weight) in panel.stations() {
713 let (point, tangent) = coedge.pcurve.deriv1(parameter)?;
714 total += weight * inner(point.x, point.y) * tangent.y;
715 }
716 }
717 }
718 }
719 }
720 for (end, start) in loop_closing_chords(face)? {
725 let step = start.y - end.y;
726 if step == 0.0 {
727 continue;
728 }
729 for index in 0..GAUSS_X.len() {
730 let fraction = 0.5 * (GAUSS_X[index] + 1.0);
731 let u = end.x + (start.x - end.x) * fraction;
732 let v = end.y + step * fraction;
733 total += 0.5 * GAUSS_W[index] * inner(u, v) * step;
734 }
735 }
736 Ok(total)
737}
738
739pub(super) fn face_moment(face: &FaceRecord, kind: Integrand) -> Result<f64, String> {
740 if is_affine(&face.surface)? {
741 return affine_moment(face, kind);
742 }
743 if let Some(values) = biperiodic_band_integral(face, &[kind])? {
744 return Ok(values[0]);
745 }
746 if is_untrimmed(face)? {
747 integrate_untrimmed(face, kind)
748 } else {
749 integrate_trimmed(face, kind)
750 }
751}
752
753pub(super) fn mat3_mul(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
756 let mut r = [[0.0f64; 3]; 3];
757 for i in 0..3 {
758 for j in 0..3 {
759 for k in 0..3 {
760 r[i][j] += a[i][k] * b[k][j];
761 }
762 }
763 }
764 r
765}
766
767pub(super) fn mat3_transpose(a: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
768 let mut r = [[0.0f64; 3]; 3];
769 for i in 0..3 {
770 for j in 0..3 {
771 r[i][j] = a[j][i];
772 }
773 }
774 r
775}
776
777pub(super) fn jacobi_eigen_symmetric_3x3(matrix: [[f64; 3]; 3]) -> ([f64; 3], [[f64; 3]; 3]) {
786 let mut a = [
788 [matrix[0][0], 0.0, 0.0],
789 [0.0, matrix[1][1], 0.0],
790 [0.0, 0.0, matrix[2][2]],
791 ];
792 a[0][1] = 0.5 * (matrix[0][1] + matrix[1][0]);
793 a[1][0] = a[0][1];
794 a[0][2] = 0.5 * (matrix[0][2] + matrix[2][0]);
795 a[2][0] = a[0][2];
796 a[1][2] = 0.5 * (matrix[1][2] + matrix[2][1]);
797 a[2][1] = a[1][2];
798
799 let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
800 let scale = a[0][0].abs() + a[1][1].abs() + a[2][2].abs() + 1.0;
801 for _sweep in 0..64 {
802 let pairs = [(0usize, 1usize), (0, 2), (1, 2)];
804 let (mut p, mut q, mut best) = (0usize, 1usize, 0.0f64);
805 for &(i, j) in &pairs {
806 if a[i][j].abs() > best {
807 best = a[i][j].abs();
808 p = i;
809 q = j;
810 }
811 }
812 if best <= 1e-18 * scale {
813 break;
814 }
815 let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
818 let t = if theta == 0.0 {
819 1.0
820 } else {
821 theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt())
822 };
823 let c = 1.0 / (t * t + 1.0).sqrt();
824 let s = t * c;
825 let mut j = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
827 j[p][p] = c;
828 j[q][q] = c;
829 j[p][q] = s;
830 j[q][p] = -s;
831 a = mat3_mul(mat3_transpose(j), mat3_mul(a, j));
833 v = mat3_mul(v, j);
834 }
835 ([a[0][0], a[1][1], a[2][2]], v)
836}
837
838pub(super) fn principal_frame(inertia: [[f64; 3]; 3]) -> ([f64; 3], [[f64; 3]; 3]) {
844 let (values, vectors) = jacobi_eigen_symmetric_3x3(inertia);
845 let mut order = [0usize, 1, 2];
847 order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
848 let mut moments = [0.0f64; 3];
849 let mut axes = [[0.0f64; 3]; 3];
850 for (slot, &k) in order.iter().enumerate() {
851 moments[slot] = values[k];
852 let mut axis = [vectors[0][k], vectors[1][k], vectors[2][k]];
853 let length = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
854 if length > 0.0 {
855 axis = [axis[0] / length, axis[1] / length, axis[2] / length];
856 }
857 axes[slot] = axis;
858 }
859 let cross = [
861 axes[0][1] * axes[1][2] - axes[0][2] * axes[1][1],
862 axes[0][2] * axes[1][0] - axes[0][0] * axes[1][2],
863 axes[0][0] * axes[1][1] - axes[0][1] * axes[1][0],
864 ];
865 let det = cross[0] * axes[2][0] + cross[1] * axes[2][1] + cross[2] * axes[2][2];
866 if det < 0.0 {
867 axes[2] = [-axes[2][0], -axes[2][1], -axes[2][2]];
868 }
869 (moments, axes)
870}
871
872pub fn solid_mass_properties_full(solid: &BrepSolid) -> Result<FullMassProperties, String> {
877 let base = solid_mass_properties(solid)?;
878 let token = super::profile::begin("solid_mass_properties_full.moments");
879 let result = solid_mass_properties_full_moments(solid, base);
880 super::profile::end(token);
881 result
882}
883
884fn solid_mass_properties_full_moments(
885 solid: &BrepSolid,
886 base: MassProperties,
887) -> Result<FullMassProperties, String> {
888 const MOMENT_KINDS: [Integrand; 9] = [
889 Integrand::MomentX,
890 Integrand::MomentY,
891 Integrand::MomentZ,
892 Integrand::SecondXX,
893 Integrand::SecondYY,
894 Integrand::SecondZZ,
895 Integrand::ProductXY,
896 Integrand::ProductXZ,
897 Integrand::ProductYZ,
898 ];
899 let mut moments = [0.0f64; 3];
900 let mut seconds = [0.0f64; 3];
901 let mut products = [0.0f64; 3];
902 for shell in &solid.shells {
903 for face in &shell.faces {
904 mass_profile(|p| p.faces += 1);
905 let results = if !is_affine(&face.surface)? && !is_untrimmed(face)? {
906 mass_profile(|p| p.trimmed_faces += 1);
907 integrate_trimmed_multi(face, &MOMENT_KINDS)?
910 } else {
911 MOMENT_KINDS
912 .iter()
913 .map(|kind| face_moment(face, *kind))
914 .collect::<Result<Vec<_>, _>>()?
915 };
916 moments[0] += results[0];
917 moments[1] += results[1];
918 moments[2] += results[2];
919 seconds[0] += results[3];
920 seconds[1] += results[4];
921 seconds[2] += results[5];
922 products[0] += results[6];
923 products[1] += results[7];
924 products[2] += results[8];
925 }
926 }
927 let volume = base.volume;
928 if volume.abs() <= 1e-30 {
929 return Err("solid_mass_properties_full: non-positive volume".into());
930 }
931 let centroid = Vec3::new(
932 moments[0] / volume,
933 moments[1] / volume,
934 moments[2] / volume,
935 );
936 let ixx =
938 seconds[1] + seconds[2] - volume * (centroid.y * centroid.y + centroid.z * centroid.z);
939 let iyy =
940 seconds[0] + seconds[2] - volume * (centroid.x * centroid.x + centroid.z * centroid.z);
941 let izz =
942 seconds[0] + seconds[1] - volume * (centroid.x * centroid.x + centroid.y * centroid.y);
943 let ixy = -(products[0] - volume * centroid.x * centroid.y);
944 let ixz = -(products[1] - volume * centroid.x * centroid.z);
945 let iyz = -(products[2] - volume * centroid.y * centroid.z);
946 let inertia = [[ixx, ixy, ixz], [ixy, iyy, iyz], [ixz, iyz, izz]];
947 let (principal_moments, principal_axes) = principal_frame(inertia);
948 Ok(FullMassProperties {
949 surface_area: base.surface_area,
950 volume,
951 centroid,
952 inertia,
953 principal_moments,
954 principal_axes,
955 })
956}
957
958