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 #[default]
223 Fit,
224 Reparameterised,
230}
231
232#[derive(Clone, Debug)]
235pub struct MeasuredOffsetSurface {
236 pub surface: NurbsSurface,
237 pub lane: OffsetSurfaceLane,
238 pub fit: Option<MeasuredTolerance>,
244}
245
246#[derive(Clone, Copy, Debug, PartialEq)]
258pub struct CarrierExtension {
259 pub u_min: f64,
260 pub u_max: f64,
261 pub v_min: f64,
262 pub v_max: f64,
263}
264
265impl CarrierExtension {
266 pub const NONE: CarrierExtension = CarrierExtension {
267 u_min: 0.0,
268 u_max: 0.0,
269 v_min: 0.0,
270 v_max: 0.0,
271 };
272
273 pub fn uniform(amount: f64) -> CarrierExtension {
274 CarrierExtension {
275 u_min: amount,
276 u_max: amount,
277 v_min: amount,
278 v_max: amount,
279 }
280 }
281
282 pub fn is_active(&self) -> bool {
283 self.u_min > 0.0 || self.u_max > 0.0 || self.v_min > 0.0 || self.v_max > 0.0
284 }
285
286 fn extends_v(&self) -> bool {
287 self.v_min > 0.0 || self.v_max > 0.0
288 }
289}
290
291fn trim_domain_fractions(
295 face: &FaceRecord,
296 [u0, u1]: [f64; 2],
297 [v0, v1]: [f64; 2],
298) -> Result<([f64; 2], [f64; 2]), String> {
299 let mut low = Vec2 {
300 x: f64::INFINITY,
301 y: f64::INFINITY,
302 };
303 let mut high = Vec2 {
304 x: f64::NEG_INFINITY,
305 y: f64::NEG_INFINITY,
306 };
307 for coedge in face
308 .loops
309 .iter()
310 .flat_map(|loop_record| &loop_record.coedges)
311 {
312 let [p0, p1] = coedge.pcurve.domain()?;
313 for sample in 0..=24 {
314 let uv = coedge
315 .pcurve
316 .evaluate(p0 + (p1 - p0) * sample as f64 / 24.0)?;
317 low.x = low.x.min(uv.x);
318 low.y = low.y.min(uv.y);
319 high.x = high.x.max(uv.x);
320 high.y = high.y.max(uv.y);
321 }
322 }
323 let fraction = |value: f64, start: f64, end: f64| {
324 let span = end - start;
325 if span.abs() <= f64::EPSILON || !value.is_finite() {
326 return None;
327 }
328 Some(((value - start) / span).clamp(0.0, 1.0))
329 };
330 let u = match (fraction(low.x, u0, u1), fraction(high.x, u0, u1)) {
331 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
332 _ => [0.0, 1.0],
333 };
334 let v = match (fraction(low.y, v0, v1), fraction(high.y, v0, v1)) {
335 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
336 _ => [0.0, 1.0],
337 };
338 Ok((u, v))
339}
340
341fn domain_end_moves(grow_min: f64, grow_max: f64, [ta, tb]: [f64; 2]) -> (f64, f64) {
351 let span = tb - ta;
352 if span <= 1e-6 {
353 return (grow_min, grow_max);
354 }
355 let rate = (grow_min + grow_max) / span;
356 let back = grow_min + ta * rate;
357 let forward = (1.0 - ta) * rate - grow_min;
358 (back.max(0.0), forward.max(0.0))
359}
360
361pub fn offset_surface(
365 face: &FaceRecord,
366 distance: f64,
367 planar_extension: f64,
368) -> Result<NurbsSurface, String> {
369 offset_surface_with_lane(face, distance, &CarrierExtension::uniform(planar_extension))
370 .map(|(surface, _)| surface)
371}
372
373pub fn offset_surface_measured(
381 face: &FaceRecord,
382 distance: f64,
383 planar_extension: f64,
384 band: f64,
385) -> Result<MeasuredOffsetSurface, String> {
386 offset_surface_measured_sided(
387 face,
388 distance,
389 &CarrierExtension::uniform(planar_extension),
390 band,
391 )
392}
393
394pub fn offset_surface_measured_sided(
396 face: &FaceRecord,
397 distance: f64,
398 extension: &CarrierExtension,
399 band: f64,
400) -> Result<MeasuredOffsetSurface, String> {
401 let (surface, lane) = offset_surface_with_lane(face, distance, extension)?;
402 let fit = match lane {
403 OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
404 OffsetSurfaceLane::Fit => Some(measure_surface_fit_against_pointwise_offset(
405 &face.surface,
406 face.same_sense,
407 &surface,
408 distance,
409 band,
410 )?),
411 OffsetSurfaceLane::Reparameterised => None,
412 };
413 Ok(MeasuredOffsetSurface { surface, lane, fit })
414}
415
416fn offset_surface_with_lane(
417 face: &FaceRecord,
418 distance: f64,
419 extension: &CarrierExtension,
420) -> Result<(NurbsSurface, OffsetSurfaceLane), String> {
421 let source = &face.surface;
422 let extension_active = extension.is_active();
423 if source.is_affine()? {
424 let ([u0, u1], [v0, v1]) = domains(source)?;
425 let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
426 let shift = normal.scale(-distance);
427 let mut points = source
428 .control_points
429 .iter()
430 .map(|row| {
431 row.iter()
432 .map(|control| Ok(control.point()?.add(shift)))
433 .collect::<Result<Vec<_>, String>>()
434 })
435 .collect::<Result<Vec<_>, String>>()?;
436 if extension_active {
437 let p00 = points[0][0];
438 let p01 = points[0][1];
439 let p10 = points[1][0];
440 let direction_u = p10.sub(p00).normalized()?;
441 let direction_v = p01.sub(p00).normalized()?;
442 let (trim_u, trim_v) = trim_domain_fractions(face, [u0, u1], [v0, v1])?;
448 let (back_u, forward_u) = domain_end_moves(extension.u_min, extension.u_max, trim_u);
449 let (back_v, forward_v) = domain_end_moves(extension.v_min, extension.v_max, trim_v);
450 points[0][0] = p00
451 .sub(direction_u.scale(back_u))
452 .sub(direction_v.scale(back_v));
453 points[0][1] = p01
454 .sub(direction_u.scale(back_u))
455 .add(direction_v.scale(forward_v));
456 points[1][0] = p10
457 .add(direction_u.scale(forward_u))
458 .sub(direction_v.scale(back_v));
459 points[1][1] = points[1][1]
460 .add(direction_u.scale(forward_u))
461 .add(direction_v.scale(forward_v));
462 }
463 let controls = points
464 .into_iter()
465 .enumerate()
466 .map(|(row, points)| {
467 points
468 .into_iter()
469 .enumerate()
470 .map(|(column, point)| {
471 Vec4::from_point(point, source.control_points[row][column].w)
472 })
473 .collect()
474 })
475 .collect();
476 let lane = if extension_active {
480 OffsetSurfaceLane::Reparameterised
481 } else {
482 OffsetSurfaceLane::Affine
483 };
484 return Ok((
485 NurbsSurface::new(
486 source.degree_u,
487 source.degree_v,
488 source.knots_u.clone(),
489 source.knots_v.clone(),
490 controls,
491 )?,
492 lane,
493 ));
494 }
495
496 let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u)?;
497 let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v)?;
498 let parameters_u = greville_parameters(&knot_u);
499 let parameters_v = greville_parameters(&knot_v);
500 let evaluator = OffsetEvaluator::new(
506 "offset_surface",
507 source,
508 OffsetNormal::FaceStable {
509 same_sense: face.same_sense,
510 },
511 );
512 let mut samples = Vec::new();
513 for &u in ¶meters_u {
514 let mut row = Vec::new();
515 for &v in ¶meters_v {
516 row.push(evaluator.at(u, v, -distance)?.point);
517 }
518 samples.push(row);
519 }
520 let mut reparameterised = false;
535 if parameters_v.len() == 2 && parameters_u.len() >= 3 {
536 let centroid = |column: usize| {
537 let mut sum = Vec3::default();
538 for row in &samples {
539 sum = sum.add(row[column]);
540 }
541 sum.scale(1.0 / samples.len() as f64)
542 };
543 let near_centroid = centroid(0);
544 let far_centroid = centroid(1);
545 let mut inverted = true;
546 let mut pinch_fraction = 0.0f64;
547 let mut near_mean = 0.0f64;
548 let mut far_mean = 0.0f64;
549 for row in &samples {
550 let near_radial = row[0].sub(near_centroid);
551 let far_radial = row[1].sub(far_centroid);
552 let near_len = near_radial.length();
553 let far_len = far_radial.length();
554 if near_len <= 1e-9 || far_len <= 1e-9 {
555 inverted = false;
556 break;
557 }
558 if near_radial.dot(far_radial) >= 0.0 {
559 inverted = false;
560 break;
561 }
562 pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
563 near_mean += near_len / samples.len() as f64;
564 far_mean += far_len / samples.len() as f64;
565 }
566 if inverted {
567 reparameterised = true;
570 let retrim_far = far_mean <= near_mean;
571 for row in &mut samples {
572 let near = row[0];
573 let far = row[1];
574 let pinch = near.add(far.sub(near).scale(pinch_fraction));
575 if retrim_far {
576 row[1] = pinch;
577 } else {
578 row[0] = pinch;
579 }
580 }
581 }
582 if extension.extends_v() && !inverted {
591 let mut min_ruling = f64::MAX;
592 let mut back_allowance = f64::MAX;
593 let mut forward_allowance = f64::MAX;
594 let mut extendable = true;
595 for row in &samples {
596 let ruling = row[1].sub(row[0]);
597 let length = ruling.length();
598 min_ruling = min_ruling.min(length);
599 let near_radial = row[0].sub(near_centroid).length();
603 let far_radial = row[1].sub(far_centroid).length();
604 if (far_radial - near_radial).abs() > 1e-9 {
605 let apex_at = near_radial / (near_radial - far_radial);
606 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
607 extendable = false;
609 break;
610 }
611 if apex_at < 0.0 {
612 back_allowance = back_allowance.min(0.9 * -apex_at);
613 } else {
614 forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
615 }
616 }
617 }
618 if extendable && min_ruling > 1e-9 {
619 reparameterised = true;
620 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
627 let (back_world, forward_world) =
628 domain_end_moves(extension.v_min, extension.v_max, trim_v);
629 let back = (back_world / min_ruling).min(back_allowance);
630 let forward = (forward_world / min_ruling).min(forward_allowance);
631 for row in &mut samples {
632 let ruling = row[1].sub(row[0]);
633 row[0] = row[0].sub(ruling.scale(back));
634 row[1] = row[1].add(ruling.scale(forward));
635 }
636 }
637 }
638 }
639 let weights = source
640 .control_points
641 .iter()
642 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
643 .collect::<Vec<_>>();
644 let lane = if reparameterised {
645 OffsetSurfaceLane::Reparameterised
646 } else {
647 OffsetSurfaceLane::Fit
648 };
649 Ok((
650 NurbsSurface::new(
651 source.degree_u,
652 source.degree_v,
653 source.knots_u.clone(),
654 source.knots_v.clone(),
655 interpolate_tensor(
656 &knot_u,
657 &knot_v,
658 ¶meters_u,
659 ¶meters_v,
660 &samples,
661 &weights,
662 )?,
663 )?,
664 lane,
665 ))
666}
667
668fn carrier_edge_curve(
690 surface: &NurbsSurface,
691 pcurve: &NurbsCurve,
692 points: &[Vec3],
693 parameters: &[f64],
694 fit_tolerance: f64,
695) -> Result<(NurbsCurve, f64, f64), String> {
696 let image = if std::env::var("BREP_CARRIER_POLYLINE").as_deref() == Ok("1") {
698 Err("BREP_CARRIER_POLYLINE set".to_string())
699 } else {
700 crate::image_curve::image_curve(surface, pcurve, fit_tolerance, "offset_face_carrier")
701 };
702 match image {
703 Ok(image) if image.t0 <= image.t1 => Ok((image.curve, image.t0, image.t1)),
704 Ok(image) => {
705 let [start, end] = image.curve.domain()?;
708 let curve = image.curve.reversed()?;
709 Ok((curve, start + end - image.t0, start + end - image.t1))
710 }
711 Err(_) => {
712 let curve = interpolate_curve(points, 1, parameters)?;
713 let [t0, t1] = curve.domain()?;
714 Ok((curve, t0, t1))
715 }
716 }
717}
718
719fn mapped_pcurve_polyline(
720 surface: &NurbsSurface,
721 pcurve: &NurbsCurve,
722 degenerate: bool,
723) -> Result<(Vec<Vec3>, Vec<f64>), String> {
724 let [start, end] = pcurve.domain()?;
725 let evaluate = |fraction: f64| {
726 let uv = pcurve.evaluate(start + (end - start) * fraction)?;
727 surface.evaluate(uv.x, uv.y)
728 };
729 let first = evaluate(0.0)?;
730 let last = evaluate(1.0)?;
731 if degenerate {
732 return Ok((vec![first, last], vec![0.0, 1.0]));
733 }
734 fn append(
735 evaluate: &impl Fn(f64) -> Result<Vec3, String>,
736 a_fraction: f64,
737 a: Vec3,
738 b_fraction: f64,
739 b: Vec3,
740 depth: usize,
741 parameters: &mut Vec<f64>,
742 points: &mut Vec<Vec3>,
743 ) -> Result<(), String> {
744 let fractions =
745 [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
746 let samples = fractions
747 .map(evaluate)
748 .into_iter()
749 .collect::<Result<Vec<_>, String>>()?;
750 let deviation = samples
751 .iter()
752 .enumerate()
753 .map(|(index, point)| {
754 point
755 .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
756 .length()
757 })
758 .fold(0.0, f64::max);
759 if deviation <= 5e-4 || depth >= 10 {
760 parameters.push(b_fraction);
761 points.push(b);
762 return Ok(());
763 }
764 append(
765 evaluate,
766 a_fraction,
767 a,
768 fractions[1],
769 samples[1],
770 depth + 1,
771 parameters,
772 points,
773 )?;
774 append(
775 evaluate,
776 fractions[1],
777 samples[1],
778 b_fraction,
779 b,
780 depth + 1,
781 parameters,
782 points,
783 )
784 }
785 let mut parameters = vec![0.0];
786 let mut points = vec![first];
787 append(
788 &evaluate,
789 0.0,
790 first,
791 1.0,
792 last,
793 0,
794 &mut parameters,
795 &mut points,
796 )?;
797 Ok((points, parameters))
798}
799
800#[derive(Clone, Debug)]
814pub struct CarrierDeviation {
815 pub band: f64,
818 pub lane: OffsetSurfaceLane,
820 pub surface: Option<MeasuredTolerance>,
822 pub edges: Vec<(u64, MeasuredTolerance)>,
827 pub vertices: Vec<(u64, f64)>,
831}
832
833impl CarrierDeviation {
834 pub fn worst(&self) -> Option<MeasuredTolerance> {
837 MeasuredTolerance::worst(
838 self.surface
839 .into_iter()
840 .chain(self.edges.iter().map(|(_, measured)| *measured))
841 .chain(
842 self.vertices
843 .iter()
844 .map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
845 ),
846 )
847 }
848
849 pub fn exceedances(&self) -> Vec<String> {
852 let mut out = Vec::new();
853 if let Some(surface) = self.surface {
854 if surface.exceeds_band() {
855 out.push(format!("carrier surface fit {}", surface.describe()));
856 }
857 }
858 for (id, measured) in &self.edges {
859 if measured.exceeds_band() {
860 out.push(format!("edge {id} {}", measured.describe()));
861 }
862 }
863 for (id, gap) in &self.vertices {
864 let measured = MeasuredTolerance::new(*gap, self.band);
865 if measured.exceeds_band() {
866 out.push(format!("vertex {id} {}", measured.describe()));
867 }
868 }
869 out
870 }
871}
872
873#[derive(Clone, Debug, Serialize)]
874pub struct OffsetFaceCarrier {
875 pub vertices: Vec<VertexRecord>,
876 pub edges: Vec<EdgeRecord>,
877 pub face: FaceRecord,
878 #[serde(skip)]
886 pub deviation: Option<CarrierDeviation>,
887}
888
889fn claim_vertex_image(
890 source_id: u64,
891 point: Vec3,
892 vertex_images: &mut HashMap<u64, u64>,
893 vertices: &mut Vec<VertexRecord>,
894 next_id: &mut u64,
895) -> u64 {
896 if let Some(id) = vertex_images.get(&source_id) {
897 return *id;
898 }
899 let id = *next_id;
900 *next_id += 1;
901 vertices.push(VertexRecord { id, point });
902 vertex_images.insert(source_id, id);
903 id
904}
905
906pub fn offset_face_carrier(
907 solid: &BrepSolid,
908 face_id: u64,
909 distance: f64,
910 planar_extension: f64,
911) -> Result<OffsetFaceCarrier, String> {
912 offset_face_carrier_impl(
913 solid,
914 face_id,
915 distance,
916 &CarrierExtension::uniform(planar_extension),
917 false,
918 )
919}
920
921pub fn offset_face_carrier_sided(
923 solid: &BrepSolid,
924 face_id: u64,
925 distance: f64,
926 extension: &CarrierExtension,
927) -> Result<OffsetFaceCarrier, String> {
928 offset_face_carrier_impl(solid, face_id, distance, extension, false)
929}
930
931pub fn offset_face_carrier_measured(
951 solid: &BrepSolid,
952 face_id: u64,
953 distance: f64,
954 planar_extension: f64,
955) -> Result<OffsetFaceCarrier, String> {
956 offset_face_carrier_impl(
957 solid,
958 face_id,
959 distance,
960 &CarrierExtension::uniform(planar_extension),
961 true,
962 )
963}
964
965fn measure_carrier(
976 surface: &NurbsSurface,
977 vertices: &[VertexRecord],
978 edges: &[EdgeRecord],
979 loops: &[LoopRecord],
980 lane: OffsetSurfaceLane,
981 surface_fit: Option<MeasuredTolerance>,
982 band: f64,
983) -> Result<CarrierDeviation, String> {
984 let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
985 let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
989 for loop_record in loops {
990 for coedge in &loop_record.coedges {
991 let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
992 continue;
993 };
994 let measured = measure_edge_against_pcurve_image(
995 surface,
996 &coedge.pcurve,
997 edge,
998 coedge.forward,
999 band,
1000 )?;
1001 per_edge
1002 .entry(edge.id)
1003 .and_modify(|existing| *existing = existing.worse_of(measured))
1004 .or_insert(measured);
1005 }
1006 }
1007
1008 let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
1009 measured_edges.sort_by_key(|(id, _)| *id);
1010 let deviation_of: HashMap<u64, f64> = measured_edges
1011 .iter()
1012 .map(|(id, measured)| (*id, measured.deviation()))
1013 .collect();
1014
1015 let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
1019 for edge in edges {
1020 ends_at
1021 .entry(edge.start_vertex_id)
1022 .or_default()
1023 .push((edge, edge.t0));
1024 ends_at
1025 .entry(edge.end_vertex_id)
1026 .or_default()
1027 .push((edge, edge.t1));
1028 }
1029
1030 let mut measured_vertices = Vec::with_capacity(vertices.len());
1031 for vertex in vertices {
1032 let mut gaps = Vec::new();
1033 let mut incident = Vec::new();
1034 for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
1035 gaps.push(vertex_endpoint_gap(
1036 vertex.point,
1037 edge.curve.evaluate(*parameter)?,
1038 ));
1039 incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
1040 }
1041 measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
1042 }
1043
1044 Ok(CarrierDeviation {
1045 band,
1046 lane,
1047 surface: surface_fit,
1048 edges: measured_edges,
1049 vertices: measured_vertices,
1050 })
1051}
1052
1053fn offset_face_carrier_impl(
1054 solid: &BrepSolid,
1055 face_id: u64,
1056 distance: f64,
1057 extension: &CarrierExtension,
1058 measure: bool,
1059) -> Result<OffsetFaceCarrier, String> {
1060 let source = solid
1061 .shells
1062 .iter()
1063 .flat_map(|shell| &shell.faces)
1064 .find(|face| face.id == face_id)
1065 .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}"))?;
1066 let band = offset_construction_band(solid_model_scale(solid));
1072 let fit_tolerance =
1079 crate::KernelTolerances::for_scale(solid_model_scale(solid), 1e-7).intersection_fit;
1080 let (surface, lane, surface_fit) = if measure {
1081 let measured = offset_surface_measured_sided(source, distance, extension, band)?;
1082 (measured.surface, measured.lane, measured.fit)
1083 } else {
1084 let (surface, lane) = offset_surface_with_lane(source, distance, extension)?;
1085 (surface, lane, None)
1086 };
1087 let source_edges = solid
1088 .edges
1089 .iter()
1090 .map(|edge| (edge.id, edge))
1091 .collect::<HashMap<_, _>>();
1092 let source_vertices = solid
1093 .vertices
1094 .iter()
1095 .map(|vertex| (vertex.id, vertex))
1096 .collect::<HashMap<_, _>>();
1097 let mut vertices = Vec::new();
1098 let mut vertex_images = HashMap::default();
1099 let mut edges = Vec::new();
1100 let mut edge_images = HashMap::default();
1101 let mut loops = Vec::new();
1102 let mut next_id = 1u64;
1103
1104 for source_loop in &source.loops {
1105 let mut coedges = Vec::new();
1106 for source_coedge in &source_loop.coedges {
1107 let source_edge = source_edges
1108 .get(&source_coedge.edge_id)
1109 .ok_or_else(|| "offset_face_carrier: missing source edge".to_string())?;
1110 let (source_start, source_end) = if source_coedge.forward {
1111 (source_edge.start_vertex_id, source_edge.end_vertex_id)
1112 } else {
1113 (source_edge.end_vertex_id, source_edge.start_vertex_id)
1114 };
1115 if !source_vertices.contains_key(&source_start)
1116 || !source_vertices.contains_key(&source_end)
1117 {
1118 return Err("offset_face_carrier: missing source vertex".into());
1119 }
1120 let (points, parameters) =
1138 mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
1139 let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
1140 let image_collapsed = points
1141 .iter()
1142 .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
1143 let (edge_id, forward) =
1144 if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
1145 edge_images.get(&source_edge.id)
1146 {
1147 if source_start == source_end {
1148 (*edge_id, source_coedge.forward == *creator_forward)
1171 } else {
1172 (
1175 *edge_id,
1176 vertex_images.get(&source_start) == Some(edge_start_vertex_id),
1177 )
1178 }
1179 } else {
1180 let (curve, t0, t1) = if image_collapsed {
1181 let curve = NurbsCurve::new(
1182 1,
1183 vec![0.0, 0.0, 1.0, 1.0],
1184 vec![
1185 Vec4::from_point(points[0], 1.0),
1186 Vec4::from_point(points[0], 1.0),
1187 ],
1188 )?;
1189 (curve, 0.0, 1.0)
1190 } else {
1191 carrier_edge_curve(
1192 &surface,
1193 &source_coedge.pcurve,
1194 &points,
1195 ¶meters,
1196 fit_tolerance,
1197 )?
1198 };
1199 let start_vertex_id = claim_vertex_image(
1200 source_start,
1201 points[0],
1202 &mut vertex_images,
1203 &mut vertices,
1204 &mut next_id,
1205 );
1206 let end_vertex_id = claim_vertex_image(
1207 source_end,
1208 points[points.len() - 1],
1209 &mut vertex_images,
1210 &mut vertices,
1211 &mut next_id,
1212 );
1213 let id = next_id;
1214 next_id += 1;
1215 edges.push(EdgeRecord {
1216 id,
1217 curve,
1218 t0,
1219 t1,
1220 start_vertex_id,
1221 end_vertex_id,
1222 degenerate: source_edge.degenerate && image_collapsed,
1226 name: source_edge
1230 .name
1231 .as_ref()
1232 .map(|name| format!("{name}_Offset")),
1233 });
1234 edge_images.insert(
1235 source_edge.id,
1236 (id, start_vertex_id, source_coedge.forward),
1237 );
1238 (id, true)
1239 };
1240 let id = next_id;
1241 next_id += 1;
1242 coedges.push(CoedgeRecord {
1243 id,
1244 edge_id,
1245 forward,
1246 pcurve: source_coedge.pcurve.clone(),
1247 });
1248 }
1249 let id = next_id;
1250 next_id += 1;
1251 loops.push(LoopRecord { id, coedges });
1252 }
1253 let deviation = if measure {
1254 Some(measure_carrier(
1255 &surface,
1256 &vertices,
1257 &edges,
1258 &loops,
1259 lane,
1260 surface_fit,
1261 band,
1262 )?)
1263 } else {
1264 None
1265 };
1266 Ok(OffsetFaceCarrier {
1267 vertices,
1268 edges,
1269 face: FaceRecord {
1270 id: next_id,
1271 surface,
1272 same_sense: source.same_sense,
1273 loops,
1274 name: source.name.as_ref().map(|name| format!("{name}_Offset")),
1275 },
1276 deviation,
1277 })
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use super::*;
1283 use crate::{make_box_brep, make_cylinder_brep};
1284
1285 #[test]
1296 fn a_full_torus_carrier_reproduces_the_source_seam_senses() {
1297 let solid =
1298 crate::make_torus_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 5.0, 1.5).unwrap();
1299 assert!(
1300 solid.validate().is_empty(),
1301 "the source torus is the oracle and must be clean"
1302 );
1303 let face = &solid.shells[0].faces[0];
1304 let source_senses: Vec<bool> = face
1305 .loops
1306 .iter()
1307 .flat_map(|loop_record| &loop_record.coedges)
1308 .map(|coedge| coedge.forward)
1309 .collect();
1310 assert_eq!(
1311 source_senses,
1312 vec![true, true, false, false],
1313 "a full torus's two seam edges are each traversed both ways"
1314 );
1315
1316 let carrier = offset_face_carrier_measured(&solid, face.id, 0.25, 0.0).unwrap();
1317 let carrier_senses: Vec<bool> = carrier
1318 .face
1319 .loops
1320 .iter()
1321 .flat_map(|loop_record| &loop_record.coedges)
1322 .map(|coedge| coedge.forward)
1323 .collect();
1324 assert_eq!(
1325 carrier_senses, source_senses,
1326 "the carrier copies the source topology, senses included"
1327 );
1328
1329 let deviation = carrier.deviation.expect("measured");
1332 for (id, measured) in &deviation.edges {
1333 assert!(
1334 measured.deviation() < 1e-3,
1335 "carrier edge {id} deviates {:.3e} from its pcurve image",
1336 measured.deviation()
1337 );
1338 }
1339 }
1340
1341 #[test]
1347 fn measuring_a_carrier_cannot_change_it() {
1348 let cases: Vec<(&str, BrepSolid)> = vec![
1349 ("box", make_box_brep(Vec3::default(), 20.0, 20.0, 4.0).unwrap()),
1350 (
1351 "cylinder",
1352 make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 5.0, 12.0).unwrap(),
1353 ),
1354 (
1355 "cone",
1356 crate::make_cone_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 5.0, 2.5, 10.0)
1357 .unwrap(),
1358 ),
1359 (
1360 "sphere",
1361 crate::make_sphere_brep(Vec3::default(), 5.0, Vec3::new(0.0, 0.0, 1.0)).unwrap(),
1362 ),
1363 ];
1364 for (label, solid) in cases {
1365 let face_ids: Vec<u64> = solid
1366 .shells
1367 .iter()
1368 .flat_map(|shell| &shell.faces)
1369 .map(|face| face.id)
1370 .collect();
1371 for face_id in face_ids {
1372 for distance in [0.25, -0.25] {
1373 for extension in [0.0, 0.5] {
1374 let plain = offset_face_carrier(&solid, face_id, distance, extension);
1375 let measured =
1376 offset_face_carrier_measured(&solid, face_id, distance, extension);
1377 match (plain, measured) {
1378 (Ok(plain), Ok(measured)) => assert_eq!(
1379 serde_json::to_string(&plain).unwrap(),
1380 serde_json::to_string(&measured).unwrap(),
1381 "{label} face {face_id} d={distance} ext={extension} moved"
1382 ),
1383 (Err(plain), Err(measured)) => assert_eq!(
1384 plain, measured,
1385 "{label} face {face_id} refusal text moved"
1386 ),
1387 (plain, measured) => panic!(
1388 "{label} face {face_id}: outcomes disagree ({:?} vs {:?})",
1389 plain.is_ok(),
1390 measured.is_ok()
1391 ),
1392 }
1393 }
1394 }
1395 }
1396 }
1397 }
1398
1399 #[test]
1403 fn the_surface_lane_names_the_branch_that_ran() {
1404 let plate = make_box_brep(Vec3::default(), 20.0, 20.0, 4.0).unwrap();
1405 let plane = &plate.shells[0].faces[0];
1406 assert_eq!(
1407 offset_surface_measured(plane, 0.25, 0.0, 1e-2).unwrap().lane,
1408 OffsetSurfaceLane::Affine
1409 );
1410 assert_eq!(
1411 offset_surface_measured(plane, 0.25, 0.5, 1e-2).unwrap().lane,
1412 OffsetSurfaceLane::Reparameterised,
1413 "a grown plane no longer names the same point at the same (u, v)"
1414 );
1415 assert!(
1416 offset_surface_measured(plane, 0.25, 0.5, 1e-2)
1417 .unwrap()
1418 .fit
1419 .is_none(),
1420 "a deliberate divergence is not reported as a fit error"
1421 );
1422
1423 let ball = crate::make_sphere_brep(Vec3::default(), 5.0, Vec3::new(0.0, 0.0, 1.0)).unwrap();
1424 let sphere = &ball.shells[0].faces[0];
1425 let measured = offset_surface_measured(sphere, 0.25, 0.0, 1e-2).unwrap();
1426 assert_eq!(measured.lane, OffsetSurfaceLane::Fit);
1427 let fit = measured.fit.expect("a fit has an error to report");
1428 assert!(
1429 fit.deviation() > 0.0 && fit.deviation() < 1e-4,
1430 "the sphere's collocation fit is small but not exact (got {:.3e})",
1431 fit.deviation()
1432 );
1433 }
1434
1435 #[test]
1436 fn affine_offset_is_exact_and_preserves_weights() {
1437 let solid = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
1438 let face = &solid.shells[0].faces[0];
1439 let offset = offset_surface(face, 0.75, 0.0).unwrap();
1440 let domain_u = KnotVector::new(face.surface.knots_u.clone(), 1)
1441 .unwrap()
1442 .domain();
1443 let domain_v = KnotVector::new(face.surface.knots_v.clone(), 1)
1444 .unwrap()
1445 .domain();
1446 let u = (domain_u[0] + domain_u[1]) / 2.0;
1447 let v = (domain_v[0] + domain_v[1]) / 2.0;
1448 let displacement = offset
1449 .evaluate(u, v)
1450 .unwrap()
1451 .sub(face.surface.evaluate(u, v).unwrap());
1452 assert!((displacement.length() - 0.75).abs() < 1e-12);
1453 }
1454
1455 #[test]
1456 fn curved_offset_carrier_maps_every_trim_to_new_surface() {
1457 let solid =
1458 make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 2.0, 4.0).unwrap();
1459 let side = &solid.shells[0].faces[0];
1460 let carrier = offset_face_carrier(&solid, side.id, 0.5, 0.0).unwrap();
1461 for coedge in carrier
1462 .face
1463 .loops
1464 .iter()
1465 .flat_map(|loop_record| &loop_record.coedges)
1466 {
1467 let edge = carrier
1468 .edges
1469 .iter()
1470 .find(|edge| edge.id == coedge.edge_id)
1471 .unwrap();
1472 for fraction in [0.0, 0.3, 0.8, 1.0] {
1473 let uv = coedge.pcurve.evaluate(fraction).unwrap();
1474 let on_surface = carrier.face.surface.evaluate(uv.x, uv.y).unwrap();
1475 let parameter = if coedge.forward {
1476 edge.t0 + (edge.t1 - edge.t0) * fraction
1477 } else {
1478 edge.t1 - (edge.t1 - edge.t0) * fraction
1479 };
1480 assert!(
1481 on_surface
1482 .sub(edge.curve.evaluate(parameter).unwrap())
1483 .length()
1484 < 7e-4
1485 );
1486 }
1487 }
1488 }
1489}