1pub(super) use crate::curve::interior_knots;
2use super::*;
3
4pub(crate) fn curve_breaks(curve: &NurbsCurve) -> Result<Vec<f64>, String> {
5 let [start, end] = curve.domain()?;
6 let mut result = vec![start];
7 result.extend(interior_knots(&curve.knots, curve.degree));
8 result.push(end);
9 Ok(result)
10}
11
12pub fn parameter_space_area(face: &FaceRecord) -> Result<f64, String> {
13 let mut area = 0.0;
14 for loop_record in &face.loops {
15 for coedge in &loop_record.coedges {
16 for pair in curve_breaks(&coedge.pcurve)?.windows(2) {
17 for panel in rule::curve_panels(&coedge.pcurve, pair[0], pair[1])? {
18 for (parameter, weight) in panel.stations() {
19 let (point, tangent) = coedge.pcurve.deriv1(parameter)?;
20 area += weight * 0.5 * (point.x * tangent.y - point.y * tangent.x);
21 }
22 }
23 }
24 }
25 }
26 Ok(area)
27}
28
29fn open_planar_loops() -> bool {
33 static OPEN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
34 *OPEN.get_or_init(|| std::env::var_os("BREP_OPEN_PLANAR_LOOPS").is_some())
35}
36
37pub(super) fn loop_closing_chords(face: &FaceRecord) -> Result<Vec<(Vec3, Vec3)>, String> {
41 let mut chords = Vec::new();
42 if open_planar_loops() {
43 return Ok(chords);
44 }
45 for loop_record in &face.loops {
46 let count = loop_record.coedges.len();
47 for index in 0..count {
48 let [_, end] = loop_record.coedges[index].pcurve.domain()?;
49 let [start, _] = loop_record.coedges[(index + 1) % count].pcurve.domain()?;
50 chords.push((
51 loop_record.coedges[index].pcurve.evaluate(end)?,
52 loop_record.coedges[(index + 1) % count].pcurve.evaluate(start)?,
53 ));
54 }
55 }
56 Ok(chords)
57}
58
59pub(crate) fn closed_parameter_space_area(face: &FaceRecord) -> Result<f64, String> {
79 let mut area = parameter_space_area(face)?;
80 for (end, start) in loop_closing_chords(face)? {
81 area += 0.5 * (end.x * start.y - start.x * end.y);
82 }
83 Ok(area)
84}
85
86const SEAM_COINCIDENCE_REL: f64 = 1e-12;
96
97pub(crate) fn closed_parameter_space_area_on_carrier(face: &FaceRecord) -> Result<f64, String> {
113 let mut area = parameter_space_area(face)?;
114 let mut coincidence: Option<f64> = None;
115 for loop_record in &face.loops {
116 let count = loop_record.coedges.len();
117 for index in 0..count {
118 let [_, end] = loop_record.coedges[index].pcurve.domain()?;
119 let [start, _] = loop_record.coedges[(index + 1) % count].pcurve.domain()?;
120 let here = loop_record.coedges[index].pcurve.evaluate(end)?;
121 let next = loop_record.coedges[(index + 1) % count].pcurve.evaluate(start)?;
122 if here.x == next.x && here.y == next.y {
123 continue;
124 }
125 let bar = *coincidence.get_or_insert_with(|| {
126 SEAM_COINCIDENCE_REL
127 * crate::tolerance::model_scale(
128 face.surface
129 .control_points
130 .iter()
131 .flatten()
132 .filter_map(|control| control.point().ok()),
133 )
134 });
135 let (image_here, image_next) = (
136 face.surface.evaluate_extended(here.x, here.y)?,
137 face.surface.evaluate_extended(next.x, next.y)?,
138 );
139 if image_here.sub(image_next).length() <= bar {
140 continue;
141 }
142 area += 0.5 * (here.x * next.y - next.x * here.y);
143 }
144 }
145 Ok(area)
146}
147
148pub(super) fn is_affine(surface: &NurbsSurface) -> Result<bool, String> {
149 surface.is_affine()
151}
152
153pub(crate) fn surface_breaks(surface: &NurbsSurface) -> Result<(Vec<f64>, Vec<f64>), String> {
154 let ku = crate::KnotVector::new(surface.knots_u.clone(), surface.degree_u)?;
155 let kv = crate::KnotVector::new(surface.knots_v.clone(), surface.degree_v)?;
156 let [u0, u1] = ku.domain();
157 let [v0, v1] = kv.domain();
158 let mut u = vec![u0];
159 u.extend(interior_knots(&surface.knots_u, surface.degree_u));
160 u.push(u1);
161 let mut v = vec![v0];
162 v.extend(interior_knots(&surface.knots_v, surface.degree_v));
163 v.push(v1);
164 Ok((u, v))
165}
166
167pub(super) fn integrand_value(kind: Integrand, point: Vec3, weighted_normal: Vec3) -> f64 {
168 let (x, y, z) = (point.x, point.y, point.z);
169 match kind {
170 Integrand::Area => weighted_normal.length(),
171 Integrand::Volume => point.dot(weighted_normal),
172 Integrand::VolumeAbout(reference) => point.sub(reference).dot(weighted_normal),
173 Integrand::NormalX => weighted_normal.x,
174 Integrand::NormalY => weighted_normal.y,
175 Integrand::NormalZ => weighted_normal.z,
176 Integrand::MomentX => 0.5 * x * x * weighted_normal.x,
177 Integrand::MomentY => 0.5 * y * y * weighted_normal.y,
178 Integrand::MomentZ => 0.5 * z * z * weighted_normal.z,
179 Integrand::SecondXX => x * x * x / 3.0 * weighted_normal.x,
180 Integrand::SecondYY => y * y * y / 3.0 * weighted_normal.y,
181 Integrand::SecondZZ => z * z * z / 3.0 * weighted_normal.z,
182 Integrand::ProductXY => 0.5 * x * x * y * weighted_normal.x,
183 Integrand::ProductXZ => 0.5 * x * x * z * weighted_normal.x,
184 Integrand::ProductYZ => 0.5 * y * y * z * weighted_normal.y,
185 }
186}
187
188
189pub(super) const GAUSS_COUNT: usize = GAUSS_X.len();
191
192pub(super) fn tensor_blocks_on() -> bool {
198 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
199 *ON.get_or_init(|| std::env::var("BREP_MASS_TENSOR_BLOCK").as_deref() != Ok("0"))
200}
201
202pub(super) fn integrate_cell_scaled(
216 face: &FaceRecord,
217 cell: [f64; 4],
218 scale: f64,
219 kinds: &[Integrand],
220 totals: &mut [f64],
221 rules: &mut rule::SurfaceRules,
222) -> Result<usize, String> {
223 let (u_panels, v_panels) = rules.panels(&face.surface, cell)?;
227 let mut stations = 0;
228 for u_panel in &u_panels {
229 for v_panel in &v_panels {
230 integrate_block(face, *u_panel, *v_panel, scale, kinds, totals)?;
231 stations += u_panel.order * v_panel.order;
232 }
233 }
234 Ok(stations)
235}
236
237fn integrate_block(
239 face: &FaceRecord,
240 u_panel: rule::Panel,
241 v_panel: rule::Panel,
242 scale: f64,
243 kinds: &[Integrand],
244 totals: &mut [f64],
245) -> Result<(), String> {
246 let half_u = (u_panel.hi - u_panel.lo) * 0.5;
247 let middle_u = (u_panel.hi + u_panel.lo) * 0.5;
248 let half_v = (v_panel.hi - v_panel.lo) * 0.5;
249 let middle_v = (v_panel.hi + v_panel.lo) * 0.5;
250 let (u_x, u_w) = u_panel.nodes();
251 let (v_x, v_w) = v_panel.nodes();
252 let sign = if face.same_sense { 1.0 } else { -1.0 };
253 let base_rule = u_x.len() == GAUSS_COUNT && v_x.len() == GAUSS_COUNT;
259 if base_rule && tensor_blocks_on() && face.surface.deriv1_tensor_supported() {
260 let mut u_stations = [0.0f64; GAUSS_COUNT];
261 let mut v_stations = [0.0f64; GAUSS_COUNT];
262 for index in 0..GAUSS_COUNT {
263 u_stations[index] = middle_u + half_u * GAUSS_X[index];
264 v_stations[index] = middle_v + half_v * GAUSS_X[index];
265 }
266 return face
267 .surface
268 .deriv1_tensor_each(&u_stations, &v_stations, |i, j, point, su, sv| {
269 let weighted_normal = su.cross(sv).scale(sign);
270 for (slot, kind) in kinds.iter().enumerate() {
271 totals[slot] += scale
272 * GAUSS_W[i]
273 * GAUSS_W[j]
274 * half_u
275 * half_v
276 * integrand_value(*kind, point, weighted_normal);
277 }
278 Ok(())
279 });
280 }
281 if base_rule && !face.surface.deriv1_tensor_supported() {
282 for i in 0..GAUSS_COUNT {
283 for j in 0..GAUSS_COUNT {
284 let (point, su, sv) = face
285 .surface
286 .deriv1(middle_u + half_u * GAUSS_X[i], middle_v + half_v * GAUSS_X[j])?;
287 let weighted_normal = su.cross(sv).scale(sign);
288 for (slot, kind) in kinds.iter().enumerate() {
289 totals[slot] += scale
290 * GAUSS_W[i]
291 * GAUSS_W[j]
292 * half_u
293 * half_v
294 * integrand_value(*kind, point, weighted_normal);
295 }
296 }
297 }
298 return Ok(());
299 }
300 if tensor_blocks_on() && face.surface.deriv1_tensor_supported() {
301 let mut u_stations = [0.0f64; rule::MAX_ORDER];
302 let mut v_stations = [0.0f64; rule::MAX_ORDER];
303 for index in 0..u_x.len() {
304 u_stations[index] = middle_u + half_u * u_x[index];
305 }
306 for index in 0..v_x.len() {
307 v_stations[index] = middle_v + half_v * v_x[index];
308 }
309 return face.surface.deriv1_tensor_each(
310 &u_stations[..u_x.len()],
311 &v_stations[..v_x.len()],
312 |i, j, point, su, sv| {
313 let weighted_normal = su.cross(sv).scale(sign);
314 for (slot, kind) in kinds.iter().enumerate() {
315 totals[slot] += scale
316 * u_w[i]
317 * v_w[j]
318 * half_u
319 * half_v
320 * integrand_value(*kind, point, weighted_normal);
321 }
322 Ok(())
323 },
324 );
325 }
326 for i in 0..u_x.len() {
327 for j in 0..v_x.len() {
328 let (point, su, sv) = face
329 .surface
330 .deriv1(middle_u + half_u * u_x[i], middle_v + half_v * v_x[j])?;
331 let weighted_normal = su.cross(sv).scale(sign);
332 for (slot, kind) in kinds.iter().enumerate() {
333 totals[slot] += scale
334 * u_w[i]
335 * v_w[j]
336 * half_u
337 * half_v
338 * integrand_value(*kind, point, weighted_normal);
339 }
340 }
341 }
342 Ok(())
343}
344
345pub(super) fn integrate_untrimmed_multi(
350 face: &FaceRecord,
351 kinds: &[Integrand],
352) -> Result<Vec<f64>, String> {
353 let (u_breaks, v_breaks) = surface_breaks(&face.surface)?;
354 let mut totals = vec![0.0f64; kinds.len()];
355 let mut rules = rule::SurfaceRules::new(&face.surface)?;
356 for upair in u_breaks.windows(2) {
357 for vpair in v_breaks.windows(2) {
358 integrate_cell_scaled(
359 face,
360 [upair[0], upair[1], vpair[0], vpair[1]],
361 1.0,
362 kinds,
363 &mut totals,
364 &mut rules,
365 )?;
366 }
367 }
368 Ok(totals)
369}
370
371pub(super) fn integrate_untrimmed(face: &FaceRecord, kind: Integrand) -> Result<f64, String> {
372 Ok(integrate_untrimmed_multi(face, &[kind])?[0])
373}
374
375#[derive(Clone, Copy)]
382pub(super) struct BiBand {
383 pub(super) p_is_u: bool,
385 pub(super) q_lo: f64,
387 pub(super) q_hi: f64,
388 pub(super) complement: bool,
392 pub(super) rim_wobble: f64,
399}
400
401pub(super) const BAND_RIM_LEVEL_TOLERANCE_REL: f64 = 1e-9;
415
416pub(super) fn biperiodic_band_range(face: &FaceRecord) -> Result<Option<BiBand>, String> {
423 let surface = &face.surface;
424 let (closed_u, closed_v) = surface.closed_directions()?;
425 if !(closed_u && closed_v) {
426 return Ok(None);
427 }
428 let [u0, u1] = surface.domain_u()?;
429 let [v0, v1] = surface.domain_v()?;
430 let u_span = (u1 - u0).abs().max(1e-30);
431 let v_span = (v1 - v0).abs().max(1e-30);
432 let mut loop_points: Vec<Vec<[f64; 2]>> = Vec::with_capacity(2);
438 for loop_record in &face.loops {
439 let mut points = Vec::new();
440 for coedge in &loop_record.coedges {
441 let [d0, d1] = coedge.pcurve.domain()?;
442 let samples = 32;
445 for k in 0..=samples {
446 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
447 let p = coedge.pcurve.evaluate(t)?;
448 points.push([p.x, p.y]);
449 }
450 }
451 if points.len() < 2 {
452 return Ok(None);
453 }
454 let (mut umin, mut umax, mut vmin, mut vmax) = (
455 f64::INFINITY,
456 f64::NEG_INFINITY,
457 f64::INFINITY,
458 f64::NEG_INFINITY,
459 );
460 for pt in &points {
461 umin = umin.min(pt[0]);
462 umax = umax.max(pt[0]);
463 vmin = vmin.min(pt[1]);
464 vmax = vmax.max(pt[1]);
465 }
466 if (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span {
467 continue; }
469 loop_points.push(points);
470 }
471 if loop_points.len() != 2 {
472 return Ok(None);
473 }
474 for p_is_u in [true, false] {
475 let (period, _p0, _p1) = if p_is_u {
476 (u1 - u0, u0, u1)
477 } else {
478 (v1 - v0, v0, v1)
479 };
480 let (q_dom_lo, q_dom_hi) = if p_is_u { (v0, v1) } else { (u0, u1) };
481 let q_extent = (q_dom_hi - q_dom_lo).abs().max(1e-30);
482 if !(period > 0.0) {
483 continue;
484 }
485 let coord = |pt: &[f64; 2]| -> (f64, f64) {
486 if p_is_u {
487 (pt[0], pt[1])
488 } else {
489 (pt[1], pt[0])
490 }
491 };
492 let mut rings: Vec<(f64, i32)> = Vec::with_capacity(2);
495 let mut clean = true;
496 let mut rim_wobble = 0.0f64;
497 for points in &loop_points {
498 let (mut pmin, mut pmax, mut qmin, mut qmax) = (
499 f64::INFINITY,
500 f64::NEG_INFINITY,
501 f64::INFINITY,
502 f64::NEG_INFINITY,
503 );
504 for pt in points {
505 let (p, q) = coord(pt);
506 pmin = pmin.min(p);
507 pmax = pmax.max(p);
508 qmin = qmin.min(q);
509 qmax = qmax.max(q);
510 }
511 if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
512 clean = false;
513 break;
514 }
515 rim_wobble = rim_wobble.max((qmax - qmin) / q_extent);
516 let mut net = 0.0;
517 for pair in points.windows(2) {
518 let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
519 if delta > 0.5 * period {
520 delta -= period;
521 } else if delta < -0.5 * period {
522 delta += period;
523 }
524 net += delta;
525 }
526 let direction = if net > 0.25 * period {
527 1
528 } else if net < -0.25 * period {
529 -1
530 } else {
531 0
532 };
533 rings.push((0.5 * (qmin + qmax), direction));
534 }
535 if !clean || rings.len() != 2 {
536 continue;
537 }
538 rings.sort_by(|a, b| a.0.total_cmp(&b.0));
539 let (q_lo, lower_dir) = rings[0];
540 let (q_hi, upper_dir) = rings[1];
541 let inconclusive = lower_dir == 0 || upper_dir == 0 || lower_dir == upper_dir;
542 let between_rims_is_ccw_uv = if p_is_u { lower_dir > 0 } else { lower_dir < 0 };
543 let complement = !inconclusive && between_rims_is_ccw_uv != face.same_sense;
544 return Ok(Some(BiBand {
545 p_is_u,
546 q_lo,
547 q_hi,
548 complement,
549 rim_wobble,
550 }));
551 }
552 Ok(None)
553}
554
555pub(super) fn integrate_rectangle(
558 face: &FaceRecord,
559 u_lo: f64,
560 u_hi: f64,
561 v_lo: f64,
562 v_hi: f64,
563 kind: Integrand,
564) -> Result<f64, String> {
565 let (u_full, v_full) = surface_breaks(&face.surface)?;
566 let clamp = |breaks: &[f64], lo: f64, hi: f64| -> Vec<f64> {
567 let eps = 1e-9 * (hi - lo).abs().max(1e-30);
568 let mut out = vec![lo];
569 for &b in breaks {
570 if b > lo + eps && b < hi - eps {
571 out.push(b);
572 }
573 }
574 out.push(hi);
575 out
576 };
577 let u_breaks = clamp(&u_full, u_lo, u_hi);
578 let v_breaks = clamp(&v_full, v_lo, v_hi);
579 let mut totals = [0.0f64];
580 let mut rules = rule::SurfaceRules::new(&face.surface)?;
581 for upair in u_breaks.windows(2) {
582 for vpair in v_breaks.windows(2) {
583 integrate_cell_scaled(
584 face,
585 [upair[0], upair[1], vpair[0], vpair[1]],
586 1.0,
587 &[kind],
588 &mut totals,
589 &mut rules,
590 )?;
591 }
592 }
593 Ok(totals[0])
594}
595
596pub(super) fn biperiodic_band_integral(
613 face: &FaceRecord,
614 kinds: &[Integrand],
615) -> Result<Option<Vec<f64>>, String> {
616 let Some(band) = biperiodic_band_range(face)? else {
617 return Ok(None);
618 };
619 let [u0, u1] = face.surface.domain_u()?;
620 let [v0, v1] = face.surface.domain_v()?;
621 let (u_lo, u_hi, v_lo, v_hi) = if band.p_is_u {
622 (u0, u1, band.q_lo, band.q_hi)
623 } else {
624 (band.q_lo, band.q_hi, v0, v1)
625 };
626 let strips: Vec<f64> = if band.rim_wobble <= BAND_RIM_LEVEL_TOLERANCE_REL {
627 kinds
628 .iter()
629 .map(|&kind| integrate_rectangle(face, u_lo, u_hi, v_lo, v_hi, kind))
630 .collect::<Result<_, _>>()?
631 } else {
632 super::winding::winding_loops_integral(face, kinds, band.p_is_u, 1)?
633 };
634 let mut out = Vec::with_capacity(kinds.len());
635 for (&kind, strip) in kinds.iter().zip(strips) {
636 let value = if band.complement {
637 integrate_untrimmed(face, kind)? - strip
638 } else {
639 strip
640 };
641 out.push(value);
642 }
643 Ok(Some(out))
644}