1use crate::fit::solve_dense;
2use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, VertexRecord};
3use crate::{
4 interpolate_curve, measure_edge_against_pcurve_image,
5 measure_surface_fit_against_pointwise_offset, offset_construction_band, solid_model_scale,
6 vertex_endpoint_gap, vertex_tolerance_from_edges, KnotVector, MeasuredTolerance, NurbsCurve,
7 NurbsSurface, OffsetEvaluator, OffsetNormal, Vec2, Vec3, Vec4,
8};
9use rustc_hash::FxHashMap as HashMap;
10use serde::Serialize;
11
12fn domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), String> {
13 Ok((
14 KnotVector::new(surface.knots_u.clone(), surface.degree_u)?.domain(),
15 KnotVector::new(surface.knots_v.clone(), surface.degree_v)?.domain(),
16 ))
17}
18
19fn stable_face_normal(face: &FaceRecord, u: f64, v: f64) -> Result<Vec3, String> {
23 OffsetEvaluator::new(
24 "offset_surface",
25 &face.surface,
26 OffsetNormal::FaceStable {
27 same_sense: face.same_sense,
28 },
29 )
30 .normal(u, v)
31}
32
33fn greville_parameters(knots: &KnotVector) -> Vec<f64> {
34 let mut parameters = (0..knots.control_point_count())
35 .map(|index| {
36 knots.knots[index + 1..=index + knots.degree]
37 .iter()
38 .sum::<f64>()
39 / knots.degree as f64
40 })
41 .collect::<Vec<_>>();
42 let domain = knots.domain();
43 parameters[0] = domain[0];
44 *parameters.last_mut().unwrap() = domain[1];
45 parameters
46}
47
48fn collocation_matrix(knots: &KnotVector, parameters: &[f64], weights: &[f64]) -> Vec<Vec<f64>> {
52 parameters
53 .iter()
54 .map(|parameter| {
55 let mut row = vec![0.0; knots.control_point_count()];
56 let span = knots.find_span(*parameter);
57 for (offset, value) in knots
58 .basis_functions(span, *parameter)
59 .into_iter()
60 .enumerate()
61 {
62 let index = span - knots.degree + offset;
63 row[index] = value * weights[index];
64 }
65 let denominator: f64 = row.iter().sum();
66 if denominator.abs() > 0.0 {
67 for value in &mut row {
68 *value /= denominator;
69 }
70 }
71 row
72 })
73 .collect()
74}
75
76fn separable_weights(weights: &[Vec<f64>]) -> Option<(Vec<f64>, Vec<f64>)> {
80 let first_row = weights.first()?;
81 let anchor = *first_row.first()?;
82 if anchor.abs() <= 1e-12 {
83 return None;
84 }
85 let a: Vec<f64> = weights.iter().map(|row| row[0]).collect();
86 let b: Vec<f64> = first_row.iter().map(|w| w / anchor).collect();
87 for (i, row) in weights.iter().enumerate() {
88 for (j, &w) in row.iter().enumerate() {
89 if (w - a[i] * b[j]).abs() > 1e-10 * (1.0 + w.abs()) {
90 return None;
91 }
92 }
93 }
94 Some((a, b))
95}
96
97fn interpolate_tensor(
103 knot_u: &KnotVector,
104 knot_v: &KnotVector,
105 parameters_u: &[f64],
106 parameters_v: &[f64],
107 samples: &[Vec<Vec3>],
108 weights: &[Vec<f64>],
109) -> Result<Vec<Vec<Vec4>>, String> {
110 let count_u = parameters_u.len();
111 let count_v = parameters_v.len();
112 if let Some((weights_u, weights_v)) = separable_weights(weights) {
113 let matrix_u = collocation_matrix(knot_u, parameters_u, &weights_u);
114 let matrix_v = collocation_matrix(knot_v, parameters_v, &weights_v);
115 let mut intermediate = vec![vec![Vec3::default(); count_v]; count_u];
116 for column in 0..count_v {
117 let solve_axis = |axis: fn(Vec3) -> f64| {
118 solve_dense(
119 matrix_u.clone(),
120 samples.iter().map(|row| axis(row[column])).collect(),
121 )
122 };
123 let x = solve_axis(|point| point.x)?;
124 let y = solve_axis(|point| point.y)?;
125 let z = solve_axis(|point| point.z)?;
126 for row in 0..count_u {
127 intermediate[row][column] = Vec3::new(x[row], y[row], z[row]);
128 }
129 }
130 let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
131 for row in 0..count_u {
132 let solve_axis = |axis: fn(Vec3) -> f64| {
133 solve_dense(
134 matrix_v.clone(),
135 intermediate[row].iter().copied().map(axis).collect(),
136 )
137 };
138 let x = solve_axis(|point| point.x)?;
139 let y = solve_axis(|point| point.y)?;
140 let z = solve_axis(|point| point.z)?;
141 for column in 0..count_v {
142 controls[row][column] = Vec4::from_point(
143 Vec3::new(x[column], y[column], z[column]),
144 weights[row][column],
145 );
146 }
147 }
148 return Ok(controls);
149 }
150
151 let unknowns = count_u * count_v;
154 let mut matrix = vec![vec![0.0; unknowns]; unknowns];
155 for (k, &u) in parameters_u.iter().enumerate() {
156 let span_u = knot_u.find_span(u);
157 let basis_u = knot_u.basis_functions(span_u, u);
158 for (l, &v) in parameters_v.iter().enumerate() {
159 let span_v = knot_v.find_span(v);
160 let basis_v = knot_v.basis_functions(span_v, v);
161 let row = &mut matrix[k * count_v + l];
162 let mut denominator = 0.0;
163 for (du, value_u) in basis_u.iter().enumerate() {
164 let i = span_u - knot_u.degree + du;
165 for (dv, value_v) in basis_v.iter().enumerate() {
166 let j = span_v - knot_v.degree + dv;
167 let entry = value_u * value_v * weights[i][j];
168 row[i * count_v + j] = entry;
169 denominator += entry;
170 }
171 }
172 if denominator.abs() > 0.0 {
173 for value in row.iter_mut() {
174 *value /= denominator;
175 }
176 }
177 }
178 }
179 let solve_axis = |axis: fn(Vec3) -> f64| {
180 solve_dense(
181 matrix.clone(),
182 samples
183 .iter()
184 .flat_map(|row| row.iter().copied().map(axis))
185 .collect(),
186 )
187 };
188 let x = solve_axis(|point| point.x)?;
189 let y = solve_axis(|point| point.y)?;
190 let z = solve_axis(|point| point.z)?;
191 let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
192 for row in 0..count_u {
193 for column in 0..count_v {
194 let index = row * count_v + column;
195 controls[row][column] = Vec4::from_point(
196 Vec3::new(x[index], y[index], z[index]),
197 weights[row][column],
198 );
199 }
200 }
201 Ok(controls)
202}
203
204#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
214pub enum OffsetSurfaceLane {
215 Affine,
219 Analytic,
225 #[default]
229 Fit,
230 Reparameterised,
236}
237
238#[derive(Clone, Debug)]
241pub struct MeasuredOffsetSurface {
242 pub surface: NurbsSurface,
243 pub lane: OffsetSurfaceLane,
244 pub fit: Option<MeasuredTolerance>,
252}
253
254#[derive(Clone, Copy, Debug, PartialEq)]
266pub struct CarrierExtension {
267 pub u_min: f64,
268 pub u_max: f64,
269 pub v_min: f64,
270 pub v_max: f64,
271}
272
273impl CarrierExtension {
274 pub const NONE: CarrierExtension = CarrierExtension {
275 u_min: 0.0,
276 u_max: 0.0,
277 v_min: 0.0,
278 v_max: 0.0,
279 };
280
281 pub fn uniform(amount: f64) -> CarrierExtension {
282 CarrierExtension {
283 u_min: amount,
284 u_max: amount,
285 v_min: amount,
286 v_max: amount,
287 }
288 }
289
290 pub fn is_active(&self) -> bool {
291 self.u_min > 0.0 || self.u_max > 0.0 || self.v_min > 0.0 || self.v_max > 0.0
292 }
293
294 fn extends_v(&self) -> bool {
295 self.v_min > 0.0 || self.v_max > 0.0
296 }
297}
298
299fn trim_domain_fractions(
303 face: &FaceRecord,
304 [u0, u1]: [f64; 2],
305 [v0, v1]: [f64; 2],
306) -> Result<([f64; 2], [f64; 2]), String> {
307 let mut low = Vec2 {
308 x: f64::INFINITY,
309 y: f64::INFINITY,
310 };
311 let mut high = Vec2 {
312 x: f64::NEG_INFINITY,
313 y: f64::NEG_INFINITY,
314 };
315 for coedge in face
316 .loops
317 .iter()
318 .flat_map(|loop_record| &loop_record.coedges)
319 {
320 let [p0, p1] = coedge.pcurve.domain()?;
321 for sample in 0..=24 {
322 let uv = coedge
323 .pcurve
324 .evaluate(p0 + (p1 - p0) * sample as f64 / 24.0)?;
325 low.x = low.x.min(uv.x);
326 low.y = low.y.min(uv.y);
327 high.x = high.x.max(uv.x);
328 high.y = high.y.max(uv.y);
329 }
330 }
331 let fraction = |value: f64, start: f64, end: f64| {
332 let span = end - start;
333 if span.abs() <= f64::EPSILON || !value.is_finite() {
334 return None;
335 }
336 Some(((value - start) / span).clamp(0.0, 1.0))
337 };
338 let u = match (fraction(low.x, u0, u1), fraction(high.x, u0, u1)) {
339 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
340 _ => [0.0, 1.0],
341 };
342 let v = match (fraction(low.y, v0, v1), fraction(high.y, v0, v1)) {
343 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
344 _ => [0.0, 1.0],
345 };
346 Ok((u, v))
347}
348
349fn domain_end_moves(grow_min: f64, grow_max: f64, [ta, tb]: [f64; 2]) -> (f64, f64) {
359 let span = tb - ta;
360 if span <= 1e-6 {
361 return (grow_min, grow_max);
362 }
363 let rate = (grow_min + grow_max) / span;
364 let back = grow_min + ta * rate;
365 let forward = (1.0 - ta) * rate - grow_min;
366 (back.max(0.0), forward.max(0.0))
367}
368
369pub fn offset_surface(
373 face: &FaceRecord,
374 distance: f64,
375 planar_extension: f64,
376) -> Result<NurbsSurface, String> {
377 offset_surface_with_lane(face, distance, &CarrierExtension::uniform(planar_extension))
378 .map(|(surface, _)| surface)
379}
380
381pub fn offset_surface_measured(
390 face: &FaceRecord,
391 distance: f64,
392 planar_extension: f64,
393 band: f64,
394) -> Result<MeasuredOffsetSurface, String> {
395 offset_surface_measured_sided(
396 face,
397 distance,
398 &CarrierExtension::uniform(planar_extension),
399 band,
400 )
401}
402
403pub fn offset_surface_measured_sided(
405 face: &FaceRecord,
406 distance: f64,
407 extension: &CarrierExtension,
408 band: f64,
409) -> Result<MeasuredOffsetSurface, String> {
410 let (surface, lane, extended) = offset_surface_constructed(face, distance, extension)?;
411 let fit = match lane {
412 OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
413 OffsetSurfaceLane::Analytic | OffsetSurfaceLane::Fit => {
418 Some(measure_surface_fit_against_pointwise_offset(
419 extended.as_ref().unwrap_or(&face.surface),
420 face.same_sense,
421 &surface,
422 distance,
423 band,
424 )?)
425 }
426 OffsetSurfaceLane::Reparameterised => None,
427 };
428 Ok(MeasuredOffsetSurface { surface, lane, fit })
429}
430
431fn sphere_offset_surface(face: &FaceRecord, distance: f64) -> Result<Option<NurbsSurface>, String> {
460 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
464 return Ok(None);
465 }
466 let source = &face.surface;
467 let Some((centre, radius)) = source.analytic().and_then(|shape| shape.sphere_geometry()) else {
468 return Ok(None);
469 };
470 let ([u0, u1], [v0, v1]) = domains(source)?;
471 let Ok(sample) = OffsetEvaluator::new(
478 "offset_surface",
479 source,
480 OffsetNormal::FaceStable {
481 same_sense: face.same_sense,
482 },
483 )
484 .at((u0 + u1) / 2.0, (v0 + v1) / 2.0, -distance)
485 else {
486 return Ok(None);
487 };
488 let source_radial = sample.source.sub(centre);
489 let offset_radial = sample.point.sub(centre);
490 let offset_radius = offset_radial.length();
491 let scale = radius.max(1.0);
492 if radius <= 1e-12
493 || offset_radius <= 1e-9 * scale
494 || offset_radial.dot(source_radial) <= 0.0
497 || ((offset_radius - radius).abs() - distance.abs()).abs() > 1e-9 * scale
500 {
501 return Ok(None);
502 }
503 let factor = offset_radius / radius;
504 let Ok(controls) = source
507 .control_points
508 .iter()
509 .map(|row| {
510 row.iter()
511 .map(|control| {
512 Ok(Vec4::from_point(
513 centre.add(control.point()?.sub(centre).scale(factor)),
514 control.w,
515 ))
516 })
517 .collect::<Result<Vec<_>, String>>()
518 })
519 .collect::<Result<Vec<Vec<_>>, String>>()
520 else {
521 return Ok(None);
522 };
523 let Ok(surface) = NurbsSurface::new(
524 source.degree_u,
525 source.degree_v,
526 source.knots_u.clone(),
527 source.knots_v.clone(),
528 controls,
529 ) else {
530 return Ok(None);
531 };
532 match measure_surface_fit_against_pointwise_offset(
536 source,
537 face.same_sense,
538 &surface,
539 distance,
540 1e-9 * scale,
541 ) {
542 Ok(measured) if measured.within_band() => Ok(Some(surface)),
543 _ => Ok(None),
544 }
545}
546
547fn revolved_arc_offset_surface(
580 face: &FaceRecord,
581 distance: f64,
582) -> Result<Option<NurbsSurface>, String> {
583 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
584 return Ok(None);
585 }
586 let source = &face.surface;
587 match source.analytic() {
588 Some(crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. }) => {}
589 _ => return Ok(None),
590 }
591 let rows = &source.control_points;
592 let Some(first_row) = rows.first() else {
593 return Ok(None);
594 };
595 if rows.iter().flatten().any(|control| !(control.w > 0.0)) {
596 return Ok(None);
597 }
598 let Ok(generatrix) = NurbsCurve::new(source.degree_v, source.knots_v.clone(), first_row.clone())
599 else {
600 return Ok(None);
601 };
602 let [t0, t1] = generatrix.domain()?;
603 let at = |fraction: f64| generatrix.evaluate(t0 + (t1 - t0) * fraction);
604 let (a, b, e) = (at(0.0)?, at(1.0 / 3.0)?, at(2.0 / 3.0)?);
606 let (ab, ae) = (b.sub(a), e.sub(a));
607 let normal = ab.cross(ae);
608 let normal_length_squared = normal.dot(normal);
609 if normal_length_squared <= 1e-24 {
610 return Ok(None);
611 }
612 let centre = a.add(
613 ae.cross(normal)
614 .scale(ab.dot(ab))
615 .add(normal.cross(ab).scale(ae.dot(ae)))
616 .scale(0.5 / normal_length_squared),
617 );
618 let radius = a.sub(centre).length();
619 let scale = (centre.length() + radius).max(1.0);
620 let band = 1e-9 * scale;
621 let unit_normal = normal.scale(1.0 / normal_length_squared.sqrt());
622 let samples = 16 * generatrix.control_points.len();
623 for index in 0..=samples {
624 let point = at(index as f64 / samples as f64)?;
625 let radial = point.sub(centre);
626 if (radial.length() - radius).abs() > band || radial.dot(unit_normal).abs() > band {
627 return Ok(None);
628 }
629 }
630 let controls = first_row
633 .iter()
634 .map(|control| control.point())
635 .collect::<Result<Vec<_>, String>>()?;
636 let origin = controls[0];
637 let mut best = (0usize, 0usize, 0.0f64);
638 for first in 1..controls.len() {
639 for second in first + 1..controls.len() {
640 let area = controls[first]
641 .sub(origin)
642 .cross(controls[second].sub(origin))
643 .length();
644 if area > best.2 {
645 best = (first, second, area);
646 }
647 }
648 }
649 let (first, second, area) = best;
650 if area <= 1e-9 * scale * scale {
651 return Ok(None);
652 }
653 let (along_first, along_second) = (controls[first].sub(origin), controls[second].sub(origin));
654 let offset_from_origin = centre.sub(origin);
655 let (g11, g12, g22) = (
656 along_first.dot(along_first),
657 along_first.dot(along_second),
658 along_second.dot(along_second),
659 );
660 let determinant = g11 * g22 - g12 * g12;
661 let (r1, r2) = (along_first.dot(offset_from_origin), along_second.dot(offset_from_origin));
662 let s = (r1 * g22 - r2 * g12) / determinant;
663 let t = (r2 * g11 - r1 * g12) / determinant;
664 if origin
665 .add(along_first.scale(s))
666 .add(along_second.scale(t))
667 .sub(centre)
668 .length()
669 > band
670 {
671 return Ok(None);
672 }
673 let ([u0, _], [v0, v1]) = domains(source)?;
676 let Ok(sample) = OffsetEvaluator::new(
677 "offset_surface",
678 source,
679 OffsetNormal::FaceStable {
680 same_sense: face.same_sense,
681 },
682 )
683 .at(u0, (v0 + v1) / 2.0, -distance)
684 else {
685 return Ok(None);
686 };
687 let source_radial = sample.source.sub(centre);
688 let offset_radial = sample.point.sub(centre);
689 let offset_radius = offset_radial.length();
690 if (source_radial.length() - radius).abs() > band
691 || offset_radius <= band
692 || offset_radial.dot(source_radial) <= 0.0
693 || ((offset_radius - radius).abs() - distance.abs()).abs() > band
694 {
695 return Ok(None);
696 }
697 let factor = offset_radius / radius;
698 let Ok(offset_rows) = rows
699 .iter()
700 .map(|row| {
701 let points = row.iter().map(|control| control.point()).collect::<Result<Vec<_>, String>>()?;
702 let row_origin = points[0];
703 let row_centre = row_origin
704 .add(points[first].sub(row_origin).scale(s))
705 .add(points[second].sub(row_origin).scale(t));
706 Ok(points
707 .iter()
708 .zip(row)
709 .map(|(point, control)| {
710 Vec4::from_point(row_centre.add(point.sub(row_centre).scale(factor)), control.w)
711 })
712 .collect::<Vec<_>>())
713 })
714 .collect::<Result<Vec<Vec<_>>, String>>()
715 else {
716 return Ok(None);
717 };
718 let Ok(surface) = NurbsSurface::new(
719 source.degree_u,
720 source.degree_v,
721 source.knots_u.clone(),
722 source.knots_v.clone(),
723 offset_rows,
724 ) else {
725 return Ok(None);
726 };
727 match measure_surface_fit_against_pointwise_offset(
728 source,
729 face.same_sense,
730 &surface,
731 distance,
732 band,
733 ) {
734 Ok(measured) if measured.within_band() => Ok(Some(surface)),
735 _ => Ok(None),
736 }
737}
738
739pub(crate) fn offset_surface_with_lane(
740 face: &FaceRecord,
741 distance: f64,
742 extension: &CarrierExtension,
743) -> Result<(NurbsSurface, OffsetSurfaceLane), String> {
744 offset_surface_constructed(face, distance, extension).map(|(surface, lane, _)| (surface, lane))
745}
746
747fn offset_surface_constructed(
751 face: &FaceRecord,
752 distance: f64,
753 extension: &CarrierExtension,
754) -> Result<(NurbsSurface, OffsetSurfaceLane, Option<NurbsSurface>), String> {
755 let source = &face.surface;
756 let extension_active = extension.is_active();
757 if source.is_affine()? {
758 let ([u0, u1], [v0, v1]) = domains(source)?;
759 let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
760 let shift = normal.scale(-distance);
761 let mut points = source
762 .control_points
763 .iter()
764 .map(|row| {
765 row.iter()
766 .map(|control| Ok(control.point()?.add(shift)))
767 .collect::<Result<Vec<_>, String>>()
768 })
769 .collect::<Result<Vec<_>, String>>()?;
770 if extension_active {
771 let p00 = points[0][0];
772 let p01 = points[0][1];
773 let p10 = points[1][0];
774 let direction_u = p10.sub(p00).normalized()?;
775 let direction_v = p01.sub(p00).normalized()?;
776 let (trim_u, trim_v) = trim_domain_fractions(face, [u0, u1], [v0, v1])?;
782 let (back_u, forward_u) = domain_end_moves(extension.u_min, extension.u_max, trim_u);
783 let (back_v, forward_v) = domain_end_moves(extension.v_min, extension.v_max, trim_v);
784 points[0][0] = p00
785 .sub(direction_u.scale(back_u))
786 .sub(direction_v.scale(back_v));
787 points[0][1] = p01
788 .sub(direction_u.scale(back_u))
789 .add(direction_v.scale(forward_v));
790 points[1][0] = p10
791 .add(direction_u.scale(forward_u))
792 .sub(direction_v.scale(back_v));
793 points[1][1] = points[1][1]
794 .add(direction_u.scale(forward_u))
795 .add(direction_v.scale(forward_v));
796 }
797 let controls = points
798 .into_iter()
799 .enumerate()
800 .map(|(row, points)| {
801 points
802 .into_iter()
803 .enumerate()
804 .map(|(column, point)| {
805 Vec4::from_point(point, source.control_points[row][column].w)
806 })
807 .collect()
808 })
809 .collect();
810 let lane = if extension_active {
814 OffsetSurfaceLane::Reparameterised
815 } else {
816 OffsetSurfaceLane::Affine
817 };
818 return Ok((
819 NurbsSurface::new(
820 source.degree_u,
821 source.degree_v,
822 source.knots_u.clone(),
823 source.knots_v.clone(),
824 controls,
825 )?,
826 lane,
827 None,
828 ));
829 }
830
831 if let Some(surface) = sphere_offset_surface(face, distance)? {
841 return Ok((surface, OffsetSurfaceLane::Analytic, None));
842 }
843 if let Some(surface) = revolved_arc_offset_surface(face, distance)? {
846 return Ok((surface, OffsetSurfaceLane::Analytic, None));
847 }
848
849 let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u)?;
850 let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v)?;
851 let parameters_u = greville_parameters(&knot_u);
852 let parameters_v = greville_parameters(&knot_v);
853 let evaluator = OffsetEvaluator::new(
859 "offset_surface",
860 source,
861 OffsetNormal::FaceStable {
862 same_sense: face.same_sense,
863 },
864 );
865 let mut samples = Vec::new();
866 for &u in ¶meters_u {
867 let mut row = Vec::new();
868 for &v in ¶meters_v {
869 row.push(evaluator.at(u, v, -distance)?.point);
870 }
871 samples.push(row);
872 }
873 let mut reparameterised = false;
888 let mut extended_source: Option<NurbsSurface> = None;
889 if parameters_v.len() == 2 && parameters_u.len() >= 3 {
890 let centroid = |column: usize| {
891 let mut sum = Vec3::default();
892 for row in &samples {
893 sum = sum.add(row[column]);
894 }
895 sum.scale(1.0 / samples.len() as f64)
896 };
897 let near_centroid = centroid(0);
898 let far_centroid = centroid(1);
899 let mut inverted = true;
900 let mut pinch_fraction = 0.0f64;
901 let mut near_mean = 0.0f64;
902 let mut far_mean = 0.0f64;
903 for row in &samples {
904 let near_radial = row[0].sub(near_centroid);
905 let far_radial = row[1].sub(far_centroid);
906 let near_len = near_radial.length();
907 let far_len = far_radial.length();
908 if near_len <= 1e-9 || far_len <= 1e-9 {
909 inverted = false;
910 break;
911 }
912 if near_radial.dot(far_radial) >= 0.0 {
913 inverted = false;
914 break;
915 }
916 pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
917 near_mean += near_len / samples.len() as f64;
918 far_mean += far_len / samples.len() as f64;
919 }
920 if inverted {
921 let retrim_far = far_mean <= near_mean;
922 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
946 let trim_reaches_the_pinch = if retrim_far {
947 trim_v[1] > pinch_fraction + 1e-9
948 } else {
949 trim_v[0] < pinch_fraction - 1e-9
950 };
951 if trim_reaches_the_pinch {
952 reparameterised = true;
955 for row in &mut samples {
956 let near = row[0];
957 let far = row[1];
958 let pinch = near.add(far.sub(near).scale(pinch_fraction));
959 if retrim_far {
960 row[1] = pinch;
961 } else {
962 row[0] = pinch;
963 }
964 }
965 }
966 }
967 if extension.extends_v() && !inverted {
987 let mut min_ruling = f64::MAX;
988 let mut back_allowance = f64::MAX;
989 let mut forward_allowance = f64::MAX;
990 let mut extendable = true;
991 for row in &samples {
992 let ruling = row[1].sub(row[0]);
993 let length = ruling.length();
994 min_ruling = min_ruling.min(length);
995 let near_radial = row[0].sub(near_centroid).length();
999 let far_radial = row[1].sub(far_centroid).length();
1000 if (far_radial - near_radial).abs() > 1e-9 {
1001 let apex_at = near_radial / (near_radial - far_radial);
1002 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1003 extendable = false;
1005 break;
1006 }
1007 if apex_at < 0.0 {
1008 back_allowance = back_allowance.min(0.9 * -apex_at);
1009 } else {
1010 forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
1011 }
1012 }
1013 }
1014 let mut source_back = f64::MAX;
1022 let mut source_forward = f64::MAX;
1023 if extendable {
1024 let source_centroid = |column: usize| {
1025 let mut sum = Vec3::default();
1026 for row in &source.control_points {
1027 if let Ok(point) = row[column].point() {
1028 sum = sum.add(point);
1029 }
1030 }
1031 sum.scale(1.0 / source.control_points.len() as f64)
1032 };
1033 let (near_source, far_source) = (source_centroid(0), source_centroid(1));
1034 for row in &source.control_points {
1035 let (near, far) = (row[0].point()?, row[1].point()?);
1036 let near_radial = near.sub(near_source).length();
1037 let far_radial = far.sub(far_source).length();
1038 if (far_radial - near_radial).abs() > 1e-9 {
1039 let apex_at = near_radial / (near_radial - far_radial);
1040 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1041 extendable = false;
1042 break;
1043 }
1044 if apex_at < 0.0 {
1045 source_back = source_back.min(0.9 * -apex_at);
1046 } else {
1047 source_forward = source_forward.min(0.9 * (apex_at - 1.0));
1048 }
1049 }
1050 }
1051 }
1052 let (back_allowance, forward_allowance) =
1053 (back_allowance.min(source_back), forward_allowance.min(source_forward));
1054 if extendable && min_ruling > 1e-9 {
1055 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1062 let (back_world, forward_world) =
1063 domain_end_moves(extension.v_min, extension.v_max, trim_v);
1064 let back = (back_world / min_ruling).min(back_allowance);
1065 let forward = (forward_world / min_ruling).min(forward_allowance);
1066 let stretched = samples
1067 .iter()
1068 .map(|row| {
1069 let ruling = row[1].sub(row[0]);
1070 vec![row[0].sub(ruling.scale(back)), row[1].add(ruling.scale(forward))]
1071 })
1072 .collect::<Vec<_>>();
1073 let extended = extend_along_rulings(source, back, forward)?;
1074 let extended_offsets = OffsetEvaluator::new(
1075 "offset_surface",
1076 &extended,
1077 OffsetNormal::FaceStable {
1078 same_sense: face.same_sense,
1079 },
1080 );
1081 let [v0, v1] = knot_v.domain();
1082 let mut developability = 0.0f64;
1083 for (row, &u) in stretched.iter().zip(¶meters_u) {
1084 for fraction in [0.0, 0.5, 1.0] {
1085 let exact = extended_offsets.at(u, v0 + (v1 - v0) * fraction, -distance)?.point;
1086 let along = row[0].add(row[1].sub(row[0]).scale(fraction));
1087 developability = developability.max(exact.sub(along).length());
1088 }
1089 }
1090 let contract = offset_fit_contract(source);
1091 let stretch_kept = developability <= contract;
1092 samples = if stretch_kept {
1093 stretched
1094 } else {
1095 parameters_u
1096 .iter()
1097 .map(|&u| {
1098 parameters_v
1099 .iter()
1100 .map(|&v| Ok(extended_offsets.at(u, v, -distance)?.point))
1101 .collect::<Result<Vec<_>, String>>()
1102 })
1103 .collect::<Result<Vec<_>, String>>()?
1104 };
1105 if let Ok(path) = std::env::var("BREP_OFFSET_FIT_TRACE") {
1106 use std::io::Write;
1107 let line = format!(
1108 "extension deg=({},{}) net=({},{}) d={distance:.6} back={back:.6} forward={forward:.6} \
1109 developability={developability:.3e} contract={contract:.3e} lane={} thread={} args={}\n",
1110 source.degree_u,
1111 source.degree_v,
1112 source.control_points.len(),
1113 source.control_points[0].len(),
1114 if stretch_kept { "stretch" } else { "pointwise" },
1115 std::thread::current().name().unwrap_or("-"),
1116 std::env::args().skip(1).collect::<Vec<_>>().join(" "),
1117 );
1118 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1119 let _ = file.write_all(line.as_bytes());
1120 }
1121 }
1122 extended_source = Some(extended);
1123 }
1124 }
1125 }
1126 let reference = extended_source.as_ref().unwrap_or(source);
1127 let weights = reference
1128 .control_points
1129 .iter()
1130 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1131 .collect::<Vec<_>>();
1132 let fitted = NurbsSurface::new(
1133 source.degree_u,
1134 source.degree_v,
1135 source.knots_u.clone(),
1136 source.knots_v.clone(),
1137 interpolate_tensor(
1138 &knot_u,
1139 &knot_v,
1140 ¶meters_u,
1141 ¶meters_v,
1142 &samples,
1143 &weights,
1144 )?,
1145 )?;
1146 if reparameterised {
1147 return Ok((fitted, OffsetSurfaceLane::Reparameterised, None));
1148 }
1149 let reference_offsets = OffsetEvaluator::new(
1150 "offset_surface",
1151 reference,
1152 OffsetNormal::FaceStable {
1153 same_sense: face.same_sense,
1154 },
1155 );
1156 Ok((
1157 refine_offset_fit(reference, &reference_offsets, distance, fitted)?,
1158 OffsetSurfaceLane::Fit,
1159 extended_source,
1160 ))
1161}
1162
1163fn extend_along_rulings(surface: &NurbsSurface, back: f64, forward: f64) -> Result<NurbsSurface, String> {
1170 let rows = surface
1171 .control_points
1172 .iter()
1173 .map(|row| {
1174 let step = row[1].add(row[0].scale(-1.0));
1175 vec![row[0].add(step.scale(-back)), row[1].add(step.scale(forward))]
1176 })
1177 .collect::<Vec<_>>();
1178 if rows.iter().flatten().any(|point| point.w <= 1e-12) {
1179 return Err(format!(
1180 "offset_surface: extending a ruled face by ({back:.6}, {forward:.6}) of its rulings \
1181 carries a weight through zero; the extended face is not this surface"
1182 ));
1183 }
1184 NurbsSurface::new(surface.degree_u, 1, surface.knots_u.clone(), surface.knots_v.clone(), rows)
1185}
1186
1187fn offset_fit_contract(source: &NurbsSurface) -> f64 {
1190 let scale = crate::model_scale(
1191 source
1192 .control_points
1193 .iter()
1194 .flatten()
1195 .filter_map(|point| point.point().ok()),
1196 );
1197 crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit
1198}
1199
1200const OFFSET_FIT_REFINEMENTS: usize = 6;
1202
1203const OFFSET_FIT_MAX_CONTROLS_SEPARABLE: usize = 16_384;
1206const OFFSET_FIT_MAX_CONTROLS_TENSOR: usize = 900;
1207
1208#[derive(Clone, Copy, Debug)]
1213struct OffsetFitDeviation {
1214 along_u: f64,
1215 along_v: f64,
1216 between: f64,
1217}
1218
1219impl OffsetFitDeviation {
1220 fn worst(&self) -> f64 {
1221 self.along_u.max(self.along_v).max(self.between)
1222 }
1223}
1224
1225fn greville_midpoints(parameters: &[f64]) -> Vec<f64> {
1226 parameters
1227 .windows(2)
1228 .filter(|pair| pair[1] - pair[0] > 1e-12)
1229 .map(|pair| 0.5 * (pair[0] + pair[1]))
1230 .collect()
1231}
1232
1233fn offset_fit_deviation(
1234 fitted: &NurbsSurface,
1235 evaluator: &OffsetEvaluator,
1236 distance: f64,
1237) -> Result<OffsetFitDeviation, String> {
1238 let parameters_u = greville_parameters(&KnotVector::new(fitted.knots_u.clone(), fitted.degree_u)?);
1239 let parameters_v = greville_parameters(&KnotVector::new(fitted.knots_v.clone(), fitted.degree_v)?);
1240 let (middle_u, middle_v) = (greville_midpoints(¶meters_u), greville_midpoints(¶meters_v));
1241 let gap = |u: f64, v: f64| -> Result<f64, String> {
1242 Ok(fitted.evaluate(u, v)?.sub(evaluator.at(u, v, -distance)?.point).length())
1243 };
1244 let mut deviation = OffsetFitDeviation {
1245 along_u: 0.0,
1246 along_v: 0.0,
1247 between: 0.0,
1248 };
1249 for &u in &middle_u {
1250 for &v in ¶meters_v {
1251 deviation.along_u = deviation.along_u.max(gap(u, v)?);
1252 }
1253 for &v in &middle_v {
1254 deviation.between = deviation.between.max(gap(u, v)?);
1255 }
1256 }
1257 for &u in ¶meters_u {
1258 for &v in &middle_v {
1259 deviation.along_v = deviation.along_v.max(gap(u, v)?);
1260 }
1261 }
1262 Ok(deviation)
1263}
1264
1265fn raise_linear_directions(
1275 surface: &NurbsSurface,
1276 in_u: bool,
1277 in_v: bool,
1278) -> Result<Option<NurbsSurface>, String> {
1279 fn raise(knots: &[f64], row: &[Vec4]) -> Option<(Vec<f64>, Vec<Vec4>)> {
1280 let count = row.len();
1282 if knots.len() != count + 2 {
1283 return None;
1284 }
1285 let breaks = &knots[1..=count];
1286 if breaks.windows(2).any(|pair| pair[1] - pair[0] <= 1e-12) {
1287 return None;
1288 }
1289 let mut raised_knots = vec![breaks[0]; 4];
1290 for &interior in &breaks[1..count - 1] {
1291 raised_knots.extend([interior; 3]);
1292 }
1293 raised_knots.extend([breaks[count - 1]; 4]);
1294 let mut points = Vec::with_capacity(3 * (count - 1) + 1);
1295 for pair in row.windows(2) {
1296 let step = pair[1].add(pair[0].scale(-1.0));
1297 points.push(pair[0]);
1298 points.push(pair[0].add(step.scale(1.0 / 3.0)));
1299 points.push(pair[0].add(step.scale(2.0 / 3.0)));
1300 }
1301 points.push(row[count - 1]);
1302 Some((raised_knots, points))
1303 }
1304 let mut knots_u = surface.knots_u.clone();
1305 let mut knots_v = surface.knots_v.clone();
1306 let mut degree_u = surface.degree_u;
1307 let mut degree_v = surface.degree_v;
1308 let mut net = surface.control_points.clone();
1309 if in_v {
1310 if degree_v != 1 {
1311 return Ok(None);
1312 }
1313 let mut rows = Vec::with_capacity(net.len());
1314 for row in &net {
1315 let Some((knots, points)) = raise(&surface.knots_v, row) else {
1316 return Ok(None);
1317 };
1318 knots_v = knots;
1319 rows.push(points);
1320 }
1321 net = rows;
1322 degree_v = 3;
1323 }
1324 if in_u {
1325 if degree_u != 1 {
1326 return Ok(None);
1327 }
1328 let count_v = net[0].len();
1329 let mut columns = Vec::with_capacity(count_v);
1330 for column in 0..count_v {
1331 let row = net.iter().map(|row| row[column]).collect::<Vec<_>>();
1332 let Some((knots, points)) = raise(&surface.knots_u, &row) else {
1333 return Ok(None);
1334 };
1335 knots_u = knots;
1336 columns.push(points);
1337 }
1338 let count_u = columns[0].len();
1339 net = (0..count_u)
1340 .map(|row| columns.iter().map(|column| column[row]).collect())
1341 .collect();
1342 degree_u = 3;
1343 }
1344 Ok(Some(NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net)?))
1345}
1346
1347fn halve_knot_spans(surface: &NurbsSurface, in_u: bool, in_v: bool) -> Result<NurbsSurface, String> {
1351 fn midpoints(knots: &[f64], degree: usize) -> Vec<f64> {
1352 let last = knots.len() - 1 - degree;
1353 knots[degree..=last]
1354 .windows(2)
1355 .filter(|pair| pair[1] - pair[0] > 1e-12)
1356 .map(|pair| 0.5 * (pair[0] + pair[1]))
1357 .collect()
1358 }
1359 let refine = |degree: usize, knots: &[f64], net: Vec<Vec<Vec4>>| -> Result<(Vec<f64>, Vec<Vec<Vec4>>), String> {
1360 let inserted = midpoints(knots, degree);
1361 let mut refined_knots = knots.to_vec();
1362 let mut rows = Vec::with_capacity(net.len());
1363 for row in net {
1364 let mut curve = NurbsCurve::new(degree, knots.to_vec(), row)?;
1365 for ¶meter in &inserted {
1366 curve = curve.insert_knot(parameter, 1)?;
1367 }
1368 refined_knots = curve.knots.clone();
1369 rows.push(curve.control_points);
1370 }
1371 Ok((refined_knots, rows))
1372 };
1373 let mut knots_u = surface.knots_u.clone();
1374 let mut knots_v = surface.knots_v.clone();
1375 let mut net = surface.control_points.clone();
1376 if in_v {
1377 let (knots, rows) = refine(surface.degree_v, &knots_v, net)?;
1378 knots_v = knots;
1379 net = rows;
1380 }
1381 if in_u {
1382 let count_v = net[0].len();
1384 let columns = (0..count_v)
1385 .map(|column| net.iter().map(|row| row[column]).collect::<Vec<_>>())
1386 .collect::<Vec<_>>();
1387 let (knots, refined) = refine(surface.degree_u, &knots_u, columns)?;
1388 knots_u = knots;
1389 let count_u = refined[0].len();
1390 net = (0..count_u)
1391 .map(|row| refined.iter().map(|column| column[row]).collect())
1392 .collect();
1393 }
1394 NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net)
1395}
1396
1397fn refine_offset_fit(
1433 source: &NurbsSurface,
1434 evaluator: &OffsetEvaluator,
1435 distance: f64,
1436 fitted: NurbsSurface,
1437) -> Result<NurbsSurface, String> {
1438 let scale = crate::model_scale(
1439 source
1440 .control_points
1441 .iter()
1442 .flatten()
1443 .filter_map(|point| point.point().ok()),
1444 );
1445 let bar = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
1446 let trace = std::env::var("BREP_OFFSET_FIT_TRACE").ok();
1447 let started = web_time::Instant::now();
1448 let enabled = std::env::var("BREP_OFFSET_FIT_REFINE").as_deref() != Ok("0");
1449 let Ok(initial) = offset_fit_deviation(&fitted, evaluator, distance) else {
1450 return Ok(fitted);
1451 };
1452 let mut best = fitted;
1453 let mut best_deviation = initial;
1454 let mut refined_source = source.clone();
1455 let mut rounds = 0usize;
1456 let mut stop = "under bar";
1457 if enabled {
1458 while best_deviation.worst() > bar {
1459 if rounds == OFFSET_FIT_REFINEMENTS {
1460 stop = "refinement limit";
1461 break;
1462 }
1463 let (along_u, along_v) = (best_deviation.along_u > bar, best_deviation.along_v > bar);
1464 let (in_u, in_v) = if along_u || along_v {
1465 (along_u, along_v)
1466 } else {
1467 (true, true)
1468 };
1469 let (raise_u, raise_v) = (in_u && refined_source.degree_u == 1, in_v && refined_source.degree_v == 1);
1470 let raised = if raise_u || raise_v {
1471 raise_linear_directions(&refined_source, raise_u, raise_v)?
1472 } else {
1473 None
1474 };
1475 let (halve_u, halve_v) = match raised {
1476 Some(_) => (in_u && !raise_u, in_v && !raise_v),
1477 None => (in_u, in_v),
1478 };
1479 let raised = raised.unwrap_or_else(|| refined_source.clone());
1480 let candidate_source = if halve_u || halve_v {
1481 halve_knot_spans(&raised, halve_u, halve_v)?
1482 } else {
1483 raised
1484 };
1485 let weights = candidate_source
1486 .control_points
1487 .iter()
1488 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1489 .collect::<Vec<_>>();
1490 let controls = weights.len() * weights[0].len();
1491 let ceiling = if separable_weights(&weights).is_some() {
1492 OFFSET_FIT_MAX_CONTROLS_SEPARABLE
1493 } else {
1494 OFFSET_FIT_MAX_CONTROLS_TENSOR
1495 };
1496 if controls > ceiling {
1497 stop = "net ceiling";
1498 break;
1499 }
1500 let knot_u = KnotVector::new(candidate_source.knots_u.clone(), candidate_source.degree_u)?;
1501 let knot_v = KnotVector::new(candidate_source.knots_v.clone(), candidate_source.degree_v)?;
1502 let parameters_u = greville_parameters(&knot_u);
1503 let parameters_v = greville_parameters(&knot_v);
1504 let samples = parameters_u
1505 .iter()
1506 .map(|&u| {
1507 parameters_v
1508 .iter()
1509 .map(|&v| Ok(evaluator.at(u, v, -distance)?.point))
1510 .collect::<Result<Vec<_>, String>>()
1511 })
1512 .collect::<Result<Vec<_>, String>>();
1513 let Ok(samples) = samples else {
1514 stop = "unevaluable sample";
1515 break;
1516 };
1517 let candidate = NurbsSurface::new(
1518 candidate_source.degree_u,
1519 candidate_source.degree_v,
1520 candidate_source.knots_u.clone(),
1521 candidate_source.knots_v.clone(),
1522 interpolate_tensor(&knot_u, &knot_v, ¶meters_u, ¶meters_v, &samples, &weights)?,
1523 )?;
1524 let Ok(deviation) = offset_fit_deviation(&candidate, evaluator, distance) else {
1525 stop = "unmeasurable round";
1526 break;
1527 };
1528 rounds += 1;
1529 if deviation.worst() >= best_deviation.worst() {
1530 stop = "stopped improving";
1531 break;
1532 }
1533 best = candidate;
1534 best_deviation = deviation;
1535 refined_source = candidate_source;
1536 }
1537 } else if best_deviation.worst() > bar {
1538 stop = "refinement disabled";
1539 }
1540 let band = offset_construction_band(scale);
1545 let refused = enabled && best_deviation.worst() > band;
1546 if let Some(path) = trace {
1547 use std::io::Write;
1548 let caller = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
1549 let thread = std::thread::current().name().unwrap_or("-").to_string();
1550 let line = format!(
1551 "fit deg=({},{}) net=({},{})->({},{}) d={distance:.6} scale={scale:.4} bar={bar:.3e} \
1552 initial=(u {:.3e}, v {:.3e}, uv {:.3e}) final={:.3e} rounds={rounds} stop={stop} \
1553 refused={refused} seconds={:.4} thread={thread} args={caller}\n",
1554 source.degree_u,
1555 source.degree_v,
1556 source.control_points.len(),
1557 source.control_points[0].len(),
1558 best.control_points.len(),
1559 best.control_points[0].len(),
1560 initial.along_u,
1561 initial.along_v,
1562 initial.between,
1563 best_deviation.worst(),
1564 started.elapsed().as_secs_f64(),
1565 );
1566 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1567 let _ = file.write_all(line.as_bytes());
1568 }
1569 }
1570 if refused {
1571 return Err(format!(
1572 "offset_surface: the fitted offset carrier stays {:.3e} off its pointwise offset at \
1573 distance {distance} after {rounds} refinement round(s) ({stop}), past the offset \
1574 construction band {band:.3e}",
1575 best_deviation.worst()
1576 ));
1577 }
1578 Ok(best)
1579}
1580
1581pub(crate) fn unify_offset_sheet_bases(
1591 first: &NurbsSurface,
1592 second: &NurbsSurface,
1593) -> Result<(NurbsSurface, NurbsSurface), String> {
1594 fn raise_to(surface: &NurbsSurface, degree_u: usize, degree_v: usize) -> Result<NurbsSurface, String> {
1595 let raise_u = surface.degree_u == 1 && degree_u == 3;
1596 let raise_v = surface.degree_v == 1 && degree_v == 3;
1597 if !raise_u && !raise_v {
1598 return Ok(surface.clone());
1599 }
1600 raise_linear_directions(surface, raise_u, raise_v)?
1601 .ok_or_else(|| "offset sheets: a linear direction could not be raised".to_string())
1602 }
1603 let degree_u = first.degree_u.max(second.degree_u);
1604 let degree_v = first.degree_v.max(second.degree_v);
1605 let mut first = raise_to(first, degree_u, degree_v)?;
1606 let mut second = raise_to(second, degree_u, degree_v)?;
1607 if first.degree_u != second.degree_u || first.degree_v != second.degree_v {
1608 return Err(format!(
1609 "offset sheets: degrees ({},{}) and ({},{}) have no common basis",
1610 first.degree_u, first.degree_v, second.degree_u, second.degree_v
1611 ));
1612 }
1613 fn missing(from: &[f64], into: &[f64]) -> Vec<f64> {
1615 let mut extra = Vec::new();
1616 let mut index = 0usize;
1617 for &knot in from {
1618 while index < into.len() && into[index] < knot - 1e-12 {
1619 index += 1;
1620 }
1621 if index < into.len() && (into[index] - knot).abs() <= 1e-12 {
1622 index += 1;
1623 } else {
1624 extra.push(knot);
1625 }
1626 }
1627 extra
1628 }
1629 fn insert(surface: &NurbsSurface, knots: &[f64], along_u: bool) -> Result<NurbsSurface, String> {
1630 if knots.is_empty() {
1631 return Ok(surface.clone());
1632 }
1633 let (degree, vector) = if along_u {
1634 (surface.degree_u, &surface.knots_u)
1635 } else {
1636 (surface.degree_v, &surface.knots_v)
1637 };
1638 let lines = if along_u {
1639 (0..surface.control_points[0].len())
1640 .map(|column| surface.control_points.iter().map(|row| row[column]).collect::<Vec<_>>())
1641 .collect::<Vec<_>>()
1642 } else {
1643 surface.control_points.clone()
1644 };
1645 let mut refined_knots = vector.clone();
1646 let mut refined = Vec::with_capacity(lines.len());
1647 for line in lines {
1648 let mut curve = NurbsCurve::new(degree, vector.clone(), line)?;
1649 for &knot in knots {
1650 curve = curve.insert_knot(knot, 1)?;
1651 }
1652 refined_knots = curve.knots.clone();
1653 refined.push(curve.control_points);
1654 }
1655 let net = if along_u {
1656 (0..refined[0].len())
1657 .map(|row| refined.iter().map(|column| column[row]).collect())
1658 .collect()
1659 } else {
1660 refined
1661 };
1662 let (knots_u, knots_v) = if along_u {
1663 (refined_knots, surface.knots_v.clone())
1664 } else {
1665 (surface.knots_u.clone(), refined_knots)
1666 };
1667 NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net)
1668 }
1669 let (to_first_u, to_second_u) = (
1670 missing(&second.knots_u, &first.knots_u),
1671 missing(&first.knots_u, &second.knots_u),
1672 );
1673 first = insert(&first, &to_first_u, true)?;
1674 second = insert(&second, &to_second_u, true)?;
1675 let (to_first_v, to_second_v) = (
1676 missing(&second.knots_v, &first.knots_v),
1677 missing(&first.knots_v, &second.knots_v),
1678 );
1679 first = insert(&first, &to_first_v, false)?;
1680 second = insert(&second, &to_second_v, false)?;
1681 Ok((first, second))
1682}
1683
1684fn carrier_edge_curve(
1706 surface: &NurbsSurface,
1707 pcurve: &NurbsCurve,
1708 points: &[Vec3],
1709 parameters: &[f64],
1710 fit_tolerance: f64,
1711) -> Result<(NurbsCurve, f64, f64), String> {
1712 let image = if std::env::var("BREP_CARRIER_POLYLINE").as_deref() == Ok("1") {
1714 Err("BREP_CARRIER_POLYLINE set".to_string())
1715 } else {
1716 crate::image_curve::image_curve(surface, pcurve, fit_tolerance, "offset_face_carrier")
1717 };
1718 match image {
1719 Ok(image) if image.t0 <= image.t1 => Ok((image.curve, image.t0, image.t1)),
1720 Ok(image) => {
1721 let [start, end] = image.curve.domain()?;
1724 let curve = image.curve.reversed()?;
1725 Ok((curve, start + end - image.t0, start + end - image.t1))
1726 }
1727 Err(_) => {
1728 let curve = interpolate_curve(points, 1, parameters)?;
1729 let [t0, t1] = curve.domain()?;
1730 Ok((curve, t0, t1))
1731 }
1732 }
1733}
1734
1735fn mapped_pcurve_polyline(
1736 surface: &NurbsSurface,
1737 pcurve: &NurbsCurve,
1738 degenerate: bool,
1739) -> Result<(Vec<Vec3>, Vec<f64>), String> {
1740 let [start, end] = pcurve.domain()?;
1741 let evaluate = |fraction: f64| {
1742 let uv = pcurve.evaluate(start + (end - start) * fraction)?;
1743 surface.evaluate(uv.x, uv.y)
1744 };
1745 let first = evaluate(0.0)?;
1746 let last = evaluate(1.0)?;
1747 if degenerate {
1748 return Ok((vec![first, last], vec![0.0, 1.0]));
1749 }
1750 fn append(
1751 evaluate: &impl Fn(f64) -> Result<Vec3, String>,
1752 a_fraction: f64,
1753 a: Vec3,
1754 b_fraction: f64,
1755 b: Vec3,
1756 depth: usize,
1757 parameters: &mut Vec<f64>,
1758 points: &mut Vec<Vec3>,
1759 ) -> Result<(), String> {
1760 let fractions =
1761 [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
1762 let samples = fractions
1763 .map(evaluate)
1764 .into_iter()
1765 .collect::<Result<Vec<_>, String>>()?;
1766 let deviation = samples
1767 .iter()
1768 .enumerate()
1769 .map(|(index, point)| {
1770 point
1771 .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
1772 .length()
1773 })
1774 .fold(0.0, f64::max);
1775 if deviation <= 5e-4 || depth >= 10 {
1776 parameters.push(b_fraction);
1777 points.push(b);
1778 return Ok(());
1779 }
1780 append(
1781 evaluate,
1782 a_fraction,
1783 a,
1784 fractions[1],
1785 samples[1],
1786 depth + 1,
1787 parameters,
1788 points,
1789 )?;
1790 append(
1791 evaluate,
1792 fractions[1],
1793 samples[1],
1794 b_fraction,
1795 b,
1796 depth + 1,
1797 parameters,
1798 points,
1799 )
1800 }
1801 let mut parameters = vec![0.0];
1802 let mut points = vec![first];
1803 append(
1804 &evaluate,
1805 0.0,
1806 first,
1807 1.0,
1808 last,
1809 0,
1810 &mut parameters,
1811 &mut points,
1812 )?;
1813 Ok((points, parameters))
1814}
1815
1816#[derive(Clone, Debug)]
1830pub struct CarrierDeviation {
1831 pub band: f64,
1834 pub lane: OffsetSurfaceLane,
1836 pub surface: Option<MeasuredTolerance>,
1838 pub edges: Vec<(u64, MeasuredTolerance)>,
1843 pub vertices: Vec<(u64, f64)>,
1847}
1848
1849impl CarrierDeviation {
1850 pub fn worst(&self) -> Option<MeasuredTolerance> {
1853 MeasuredTolerance::worst(
1854 self.surface
1855 .into_iter()
1856 .chain(self.edges.iter().map(|(_, measured)| *measured))
1857 .chain(
1858 self.vertices
1859 .iter()
1860 .map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
1861 ),
1862 )
1863 }
1864
1865 pub fn exceedances(&self) -> Vec<String> {
1868 let mut out = Vec::new();
1869 if let Some(surface) = self.surface {
1870 if surface.exceeds_band() {
1871 out.push(format!("carrier surface fit {}", surface.describe()));
1872 }
1873 }
1874 for (id, measured) in &self.edges {
1875 if measured.exceeds_band() {
1876 out.push(format!("edge {id} {}", measured.describe()));
1877 }
1878 }
1879 for (id, gap) in &self.vertices {
1880 let measured = MeasuredTolerance::new(*gap, self.band);
1881 if measured.exceeds_band() {
1882 out.push(format!("vertex {id} {}", measured.describe()));
1883 }
1884 }
1885 out
1886 }
1887}
1888
1889#[derive(Clone, Debug, Serialize)]
1890pub struct OffsetFaceCarrier {
1891 pub vertices: Vec<VertexRecord>,
1892 pub edges: Vec<EdgeRecord>,
1893 pub face: FaceRecord,
1894 #[serde(skip)]
1902 pub deviation: Option<CarrierDeviation>,
1903}
1904
1905fn claim_vertex_image(
1906 source_id: u64,
1907 point: Vec3,
1908 vertex_images: &mut HashMap<u64, u64>,
1909 vertices: &mut Vec<VertexRecord>,
1910 next_id: &mut u64,
1911) -> u64 {
1912 if let Some(id) = vertex_images.get(&source_id) {
1913 return *id;
1914 }
1915 let id = *next_id;
1916 *next_id += 1;
1917 vertices.push(VertexRecord { id, point });
1918 vertex_images.insert(source_id, id);
1919 id
1920}
1921
1922pub fn offset_face_carrier(
1923 solid: &BrepSolid,
1924 face_id: u64,
1925 distance: f64,
1926 planar_extension: f64,
1927) -> Result<OffsetFaceCarrier, String> {
1928 offset_face_carrier_impl(
1929 solid,
1930 face_id,
1931 distance,
1932 &CarrierExtension::uniform(planar_extension),
1933 false,
1934 )
1935}
1936
1937pub fn offset_face_carrier_sided(
1939 solid: &BrepSolid,
1940 face_id: u64,
1941 distance: f64,
1942 extension: &CarrierExtension,
1943) -> Result<OffsetFaceCarrier, String> {
1944 offset_face_carrier_impl(solid, face_id, distance, extension, false)
1945}
1946
1947pub fn offset_face_carrier_measured(
1967 solid: &BrepSolid,
1968 face_id: u64,
1969 distance: f64,
1970 planar_extension: f64,
1971) -> Result<OffsetFaceCarrier, String> {
1972 offset_face_carrier_impl(
1973 solid,
1974 face_id,
1975 distance,
1976 &CarrierExtension::uniform(planar_extension),
1977 true,
1978 )
1979}
1980
1981fn measure_carrier(
1992 surface: &NurbsSurface,
1993 vertices: &[VertexRecord],
1994 edges: &[EdgeRecord],
1995 loops: &[LoopRecord],
1996 lane: OffsetSurfaceLane,
1997 surface_fit: Option<MeasuredTolerance>,
1998 band: f64,
1999) -> Result<CarrierDeviation, String> {
2000 let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
2001 let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
2005 for loop_record in loops {
2006 for coedge in &loop_record.coedges {
2007 let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
2008 continue;
2009 };
2010 let measured = measure_edge_against_pcurve_image(
2011 surface,
2012 &coedge.pcurve,
2013 edge,
2014 coedge.forward,
2015 band,
2016 )?;
2017 per_edge
2018 .entry(edge.id)
2019 .and_modify(|existing| *existing = existing.worse_of(measured))
2020 .or_insert(measured);
2021 }
2022 }
2023
2024 let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
2025 measured_edges.sort_by_key(|(id, _)| *id);
2026 let deviation_of: HashMap<u64, f64> = measured_edges
2027 .iter()
2028 .map(|(id, measured)| (*id, measured.deviation()))
2029 .collect();
2030
2031 let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
2035 for edge in edges {
2036 ends_at
2037 .entry(edge.start_vertex_id)
2038 .or_default()
2039 .push((edge, edge.t0));
2040 ends_at
2041 .entry(edge.end_vertex_id)
2042 .or_default()
2043 .push((edge, edge.t1));
2044 }
2045
2046 let mut measured_vertices = Vec::with_capacity(vertices.len());
2047 for vertex in vertices {
2048 let mut gaps = Vec::new();
2049 let mut incident = Vec::new();
2050 for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
2051 gaps.push(vertex_endpoint_gap(
2052 vertex.point,
2053 edge.curve.evaluate(*parameter)?,
2054 ));
2055 incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
2056 }
2057 measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
2058 }
2059
2060 Ok(CarrierDeviation {
2061 band,
2062 lane,
2063 surface: surface_fit,
2064 edges: measured_edges,
2065 vertices: measured_vertices,
2066 })
2067}
2068
2069fn offset_face_carrier_impl(
2070 solid: &BrepSolid,
2071 face_id: u64,
2072 distance: f64,
2073 extension: &CarrierExtension,
2074 measure: bool,
2075) -> Result<OffsetFaceCarrier, String> {
2076 let source = solid
2077 .shells
2078 .iter()
2079 .flat_map(|shell| &shell.faces)
2080 .find(|face| face.id == face_id)
2081 .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}"))?;
2082 let band = offset_construction_band(solid_model_scale(solid));
2090 let fit_tolerance =
2097 crate::KernelTolerances::for_scale(solid_model_scale(solid), 1e-7).intersection_fit;
2098 let (surface, lane, surface_fit) = if measure {
2099 let measured = offset_surface_measured_sided(source, distance, extension, band)?;
2100 (measured.surface, measured.lane, measured.fit)
2101 } else {
2102 let (surface, lane) = offset_surface_with_lane(source, distance, extension)?;
2103 (surface, lane, None)
2104 };
2105 let source_edges = solid
2106 .edges
2107 .iter()
2108 .map(|edge| (edge.id, edge))
2109 .collect::<HashMap<_, _>>();
2110 let source_vertices = solid
2111 .vertices
2112 .iter()
2113 .map(|vertex| (vertex.id, vertex))
2114 .collect::<HashMap<_, _>>();
2115 let mut vertices = Vec::new();
2116 let mut vertex_images = HashMap::default();
2117 let mut edges = Vec::new();
2118 let mut edge_images = HashMap::default();
2119 let mut loops = Vec::new();
2120 let mut next_id = 1u64;
2121
2122 for source_loop in &source.loops {
2123 let mut coedges = Vec::new();
2124 for source_coedge in &source_loop.coedges {
2125 let source_edge = source_edges
2126 .get(&source_coedge.edge_id)
2127 .ok_or_else(|| "offset_face_carrier: missing source edge".to_string())?;
2128 let (source_start, source_end) = if source_coedge.forward {
2129 (source_edge.start_vertex_id, source_edge.end_vertex_id)
2130 } else {
2131 (source_edge.end_vertex_id, source_edge.start_vertex_id)
2132 };
2133 if !source_vertices.contains_key(&source_start)
2134 || !source_vertices.contains_key(&source_end)
2135 {
2136 return Err("offset_face_carrier: missing source vertex".into());
2137 }
2138 let (points, parameters) =
2156 mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
2157 let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
2158 let image_collapsed = points
2159 .iter()
2160 .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
2161 let (edge_id, forward) =
2162 if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
2163 edge_images.get(&source_edge.id)
2164 {
2165 if source_start == source_end {
2166 (*edge_id, source_coedge.forward == *creator_forward)
2189 } else {
2190 (
2193 *edge_id,
2194 vertex_images.get(&source_start) == Some(edge_start_vertex_id),
2195 )
2196 }
2197 } else {
2198 let (curve, t0, t1) = if image_collapsed {
2199 let curve = NurbsCurve::new(
2200 1,
2201 vec![0.0, 0.0, 1.0, 1.0],
2202 vec![
2203 Vec4::from_point(points[0], 1.0),
2204 Vec4::from_point(points[0], 1.0),
2205 ],
2206 )?;
2207 (curve, 0.0, 1.0)
2208 } else {
2209 carrier_edge_curve(
2210 &surface,
2211 &source_coedge.pcurve,
2212 &points,
2213 ¶meters,
2214 fit_tolerance,
2215 )?
2216 };
2217 let start_vertex_id = claim_vertex_image(
2218 source_start,
2219 points[0],
2220 &mut vertex_images,
2221 &mut vertices,
2222 &mut next_id,
2223 );
2224 let end_vertex_id = claim_vertex_image(
2225 source_end,
2226 points[points.len() - 1],
2227 &mut vertex_images,
2228 &mut vertices,
2229 &mut next_id,
2230 );
2231 let id = next_id;
2232 next_id += 1;
2233 edges.push(EdgeRecord {
2234 id,
2235 curve,
2236 t0,
2237 t1,
2238 start_vertex_id,
2239 end_vertex_id,
2240 degenerate: image_collapsed,
2254 name: source_edge
2258 .name
2259 .as_ref()
2260 .map(|name| format!("{name}_Offset")),
2261 });
2262 edge_images.insert(
2263 source_edge.id,
2264 (id, start_vertex_id, source_coedge.forward),
2265 );
2266 (id, true)
2267 };
2268 let id = next_id;
2269 next_id += 1;
2270 coedges.push(CoedgeRecord {
2271 id,
2272 edge_id,
2273 forward,
2274 pcurve: source_coedge.pcurve.clone(),
2275 });
2276 }
2277 let id = next_id;
2278 next_id += 1;
2279 loops.push(LoopRecord { id, coedges });
2280 }
2281 let deviation = if measure {
2282 Some(measure_carrier(
2283 &surface,
2284 &vertices,
2285 &edges,
2286 &loops,
2287 lane,
2288 surface_fit,
2289 band,
2290 )?)
2291 } else {
2292 None
2293 };
2294 Ok(OffsetFaceCarrier {
2295 vertices,
2296 edges,
2297 face: FaceRecord {
2298 id: next_id,
2299 surface,
2300 same_sense: source.same_sense,
2301 loops,
2302 name: source.name.as_ref().map(|name| format!("{name}_Offset")),
2303 },
2304 deviation,
2305 })
2306}
2307