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, 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
246pub fn offset_surface(
250 face: &FaceRecord,
251 distance: f64,
252 planar_extension: f64,
253) -> Result<NurbsSurface, String> {
254 offset_surface_with_lane(face, distance, planar_extension).map(|(surface, _)| surface)
255}
256
257pub fn offset_surface_measured(
265 face: &FaceRecord,
266 distance: f64,
267 planar_extension: f64,
268 band: f64,
269) -> Result<MeasuredOffsetSurface, String> {
270 let (surface, lane) = offset_surface_with_lane(face, distance, planar_extension)?;
271 let fit = match lane {
272 OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
273 OffsetSurfaceLane::Fit => Some(measure_surface_fit_against_pointwise_offset(
274 &face.surface,
275 face.same_sense,
276 &surface,
277 distance,
278 band,
279 )?),
280 OffsetSurfaceLane::Reparameterised => None,
281 };
282 Ok(MeasuredOffsetSurface { surface, lane, fit })
283}
284
285fn offset_surface_with_lane(
286 face: &FaceRecord,
287 distance: f64,
288 planar_extension: f64,
289) -> Result<(NurbsSurface, OffsetSurfaceLane), String> {
290 let source = &face.surface;
291 if source.is_affine()? {
292 let ([u0, u1], [v0, v1]) = domains(source)?;
293 let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
294 let shift = normal.scale(-distance);
295 let mut points = source
296 .control_points
297 .iter()
298 .map(|row| {
299 row.iter()
300 .map(|control| Ok(control.point()?.add(shift)))
301 .collect::<Result<Vec<_>, String>>()
302 })
303 .collect::<Result<Vec<_>, String>>()?;
304 if planar_extension > 0.0 {
305 let p00 = points[0][0];
306 let p01 = points[0][1];
307 let p10 = points[1][0];
308 let direction_u = p10.sub(p00).normalized()?;
309 let direction_v = p01.sub(p00).normalized()?;
310 points[0][0] = p00
311 .sub(direction_u.scale(planar_extension))
312 .sub(direction_v.scale(planar_extension));
313 points[0][1] = p01
314 .sub(direction_u.scale(planar_extension))
315 .add(direction_v.scale(planar_extension));
316 points[1][0] = p10
317 .add(direction_u.scale(planar_extension))
318 .sub(direction_v.scale(planar_extension));
319 points[1][1] = points[1][1]
320 .add(direction_u.scale(planar_extension))
321 .add(direction_v.scale(planar_extension));
322 }
323 let controls = points
324 .into_iter()
325 .enumerate()
326 .map(|(row, points)| {
327 points
328 .into_iter()
329 .enumerate()
330 .map(|(column, point)| {
331 Vec4::from_point(point, source.control_points[row][column].w)
332 })
333 .collect()
334 })
335 .collect();
336 let lane = if planar_extension > 0.0 {
340 OffsetSurfaceLane::Reparameterised
341 } else {
342 OffsetSurfaceLane::Affine
343 };
344 return Ok((
345 NurbsSurface::new(
346 source.degree_u,
347 source.degree_v,
348 source.knots_u.clone(),
349 source.knots_v.clone(),
350 controls,
351 )?,
352 lane,
353 ));
354 }
355
356 let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u)?;
357 let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v)?;
358 let parameters_u = greville_parameters(&knot_u);
359 let parameters_v = greville_parameters(&knot_v);
360 let evaluator = OffsetEvaluator::new(
366 "offset_surface",
367 source,
368 OffsetNormal::FaceStable {
369 same_sense: face.same_sense,
370 },
371 );
372 let mut samples = Vec::new();
373 for &u in ¶meters_u {
374 let mut row = Vec::new();
375 for &v in ¶meters_v {
376 row.push(evaluator.at(u, v, -distance)?.point);
377 }
378 samples.push(row);
379 }
380 let mut reparameterised = false;
395 if parameters_v.len() == 2 && parameters_u.len() >= 3 {
396 let centroid = |column: usize| {
397 let mut sum = Vec3::default();
398 for row in &samples {
399 sum = sum.add(row[column]);
400 }
401 sum.scale(1.0 / samples.len() as f64)
402 };
403 let near_centroid = centroid(0);
404 let far_centroid = centroid(1);
405 let mut inverted = true;
406 let mut pinch_fraction = 0.0f64;
407 let mut near_mean = 0.0f64;
408 let mut far_mean = 0.0f64;
409 for row in &samples {
410 let near_radial = row[0].sub(near_centroid);
411 let far_radial = row[1].sub(far_centroid);
412 let near_len = near_radial.length();
413 let far_len = far_radial.length();
414 if near_len <= 1e-9 || far_len <= 1e-9 {
415 inverted = false;
416 break;
417 }
418 if near_radial.dot(far_radial) >= 0.0 {
419 inverted = false;
420 break;
421 }
422 pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
423 near_mean += near_len / samples.len() as f64;
424 far_mean += far_len / samples.len() as f64;
425 }
426 if inverted {
427 reparameterised = true;
430 let retrim_far = far_mean <= near_mean;
431 for row in &mut samples {
432 let near = row[0];
433 let far = row[1];
434 let pinch = near.add(far.sub(near).scale(pinch_fraction));
435 if retrim_far {
436 row[1] = pinch;
437 } else {
438 row[0] = pinch;
439 }
440 }
441 }
442 if planar_extension > 0.0 && !inverted {
451 let mut min_ruling = f64::MAX;
452 let mut back_allowance = f64::MAX;
453 let mut forward_allowance = f64::MAX;
454 let mut extendable = true;
455 for row in &samples {
456 let ruling = row[1].sub(row[0]);
457 let length = ruling.length();
458 min_ruling = min_ruling.min(length);
459 let near_radial = row[0].sub(near_centroid).length();
463 let far_radial = row[1].sub(far_centroid).length();
464 if (far_radial - near_radial).abs() > 1e-9 {
465 let apex_at = near_radial / (near_radial - far_radial);
466 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
467 extendable = false;
469 break;
470 }
471 if apex_at < 0.0 {
472 back_allowance = back_allowance.min(0.9 * -apex_at);
473 } else {
474 forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
475 }
476 }
477 }
478 if extendable && min_ruling > 1e-9 {
479 reparameterised = true;
480 let stretch = planar_extension / min_ruling;
481 let back = stretch.min(back_allowance);
482 let forward = stretch.min(forward_allowance);
483 for row in &mut samples {
484 let ruling = row[1].sub(row[0]);
485 row[0] = row[0].sub(ruling.scale(back));
486 row[1] = row[1].add(ruling.scale(forward));
487 }
488 }
489 }
490 }
491 let weights = source
492 .control_points
493 .iter()
494 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
495 .collect::<Vec<_>>();
496 let lane = if reparameterised {
497 OffsetSurfaceLane::Reparameterised
498 } else {
499 OffsetSurfaceLane::Fit
500 };
501 Ok((
502 NurbsSurface::new(
503 source.degree_u,
504 source.degree_v,
505 source.knots_u.clone(),
506 source.knots_v.clone(),
507 interpolate_tensor(
508 &knot_u,
509 &knot_v,
510 ¶meters_u,
511 ¶meters_v,
512 &samples,
513 &weights,
514 )?,
515 )?,
516 lane,
517 ))
518}
519
520fn mapped_pcurve_polyline(
521 surface: &NurbsSurface,
522 pcurve: &NurbsCurve,
523 degenerate: bool,
524) -> Result<(Vec<Vec3>, Vec<f64>), String> {
525 let [start, end] = pcurve.domain()?;
526 let evaluate = |fraction: f64| {
527 let uv = pcurve.evaluate(start + (end - start) * fraction)?;
528 surface.evaluate(uv.x, uv.y)
529 };
530 let first = evaluate(0.0)?;
531 let last = evaluate(1.0)?;
532 if degenerate {
533 return Ok((vec![first, last], vec![0.0, 1.0]));
534 }
535 fn append(
536 evaluate: &impl Fn(f64) -> Result<Vec3, String>,
537 a_fraction: f64,
538 a: Vec3,
539 b_fraction: f64,
540 b: Vec3,
541 depth: usize,
542 parameters: &mut Vec<f64>,
543 points: &mut Vec<Vec3>,
544 ) -> Result<(), String> {
545 let fractions =
546 [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
547 let samples = fractions
548 .map(evaluate)
549 .into_iter()
550 .collect::<Result<Vec<_>, String>>()?;
551 let deviation = samples
552 .iter()
553 .enumerate()
554 .map(|(index, point)| {
555 point
556 .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
557 .length()
558 })
559 .fold(0.0, f64::max);
560 if deviation <= 5e-4 || depth >= 10 {
561 parameters.push(b_fraction);
562 points.push(b);
563 return Ok(());
564 }
565 append(
566 evaluate,
567 a_fraction,
568 a,
569 fractions[1],
570 samples[1],
571 depth + 1,
572 parameters,
573 points,
574 )?;
575 append(
576 evaluate,
577 fractions[1],
578 samples[1],
579 b_fraction,
580 b,
581 depth + 1,
582 parameters,
583 points,
584 )
585 }
586 let mut parameters = vec![0.0];
587 let mut points = vec![first];
588 append(
589 &evaluate,
590 0.0,
591 first,
592 1.0,
593 last,
594 0,
595 &mut parameters,
596 &mut points,
597 )?;
598 Ok((points, parameters))
599}
600
601#[derive(Clone, Debug)]
615pub struct CarrierDeviation {
616 pub band: f64,
619 pub lane: OffsetSurfaceLane,
621 pub surface: Option<MeasuredTolerance>,
623 pub edges: Vec<(u64, MeasuredTolerance)>,
628 pub vertices: Vec<(u64, f64)>,
632}
633
634impl CarrierDeviation {
635 pub fn worst(&self) -> Option<MeasuredTolerance> {
638 MeasuredTolerance::worst(
639 self.surface
640 .into_iter()
641 .chain(self.edges.iter().map(|(_, measured)| *measured))
642 .chain(
643 self.vertices
644 .iter()
645 .map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
646 ),
647 )
648 }
649
650 pub fn exceedances(&self) -> Vec<String> {
653 let mut out = Vec::new();
654 if let Some(surface) = self.surface {
655 if surface.exceeds_band() {
656 out.push(format!("carrier surface fit {}", surface.describe()));
657 }
658 }
659 for (id, measured) in &self.edges {
660 if measured.exceeds_band() {
661 out.push(format!("edge {id} {}", measured.describe()));
662 }
663 }
664 for (id, gap) in &self.vertices {
665 let measured = MeasuredTolerance::new(*gap, self.band);
666 if measured.exceeds_band() {
667 out.push(format!("vertex {id} {}", measured.describe()));
668 }
669 }
670 out
671 }
672}
673
674#[derive(Clone, Debug, Serialize)]
675pub struct OffsetFaceCarrier {
676 pub vertices: Vec<VertexRecord>,
677 pub edges: Vec<EdgeRecord>,
678 pub face: FaceRecord,
679 #[serde(skip)]
687 pub deviation: Option<CarrierDeviation>,
688}
689
690fn claim_vertex_image(
691 source_id: u64,
692 point: Vec3,
693 vertex_images: &mut HashMap<u64, u64>,
694 vertices: &mut Vec<VertexRecord>,
695 next_id: &mut u64,
696) -> u64 {
697 if let Some(id) = vertex_images.get(&source_id) {
698 return *id;
699 }
700 let id = *next_id;
701 *next_id += 1;
702 vertices.push(VertexRecord { id, point });
703 vertex_images.insert(source_id, id);
704 id
705}
706
707pub fn offset_face_carrier(
708 solid: &BrepSolid,
709 face_id: u64,
710 distance: f64,
711 planar_extension: f64,
712) -> Result<OffsetFaceCarrier, String> {
713 offset_face_carrier_impl(solid, face_id, distance, planar_extension, false)
714}
715
716pub fn offset_face_carrier_measured(
736 solid: &BrepSolid,
737 face_id: u64,
738 distance: f64,
739 planar_extension: f64,
740) -> Result<OffsetFaceCarrier, String> {
741 offset_face_carrier_impl(solid, face_id, distance, planar_extension, true)
742}
743
744fn measure_carrier(
755 surface: &NurbsSurface,
756 vertices: &[VertexRecord],
757 edges: &[EdgeRecord],
758 loops: &[LoopRecord],
759 lane: OffsetSurfaceLane,
760 surface_fit: Option<MeasuredTolerance>,
761 band: f64,
762) -> Result<CarrierDeviation, String> {
763 let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
764 let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
768 for loop_record in loops {
769 for coedge in &loop_record.coedges {
770 let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
771 continue;
772 };
773 let measured = measure_edge_against_pcurve_image(
774 surface,
775 &coedge.pcurve,
776 edge,
777 coedge.forward,
778 band,
779 )?;
780 per_edge
781 .entry(edge.id)
782 .and_modify(|existing| *existing = existing.worse_of(measured))
783 .or_insert(measured);
784 }
785 }
786
787 let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
788 measured_edges.sort_by_key(|(id, _)| *id);
789 let deviation_of: HashMap<u64, f64> = measured_edges
790 .iter()
791 .map(|(id, measured)| (*id, measured.deviation()))
792 .collect();
793
794 let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
798 for edge in edges {
799 ends_at
800 .entry(edge.start_vertex_id)
801 .or_default()
802 .push((edge, edge.t0));
803 ends_at
804 .entry(edge.end_vertex_id)
805 .or_default()
806 .push((edge, edge.t1));
807 }
808
809 let mut measured_vertices = Vec::with_capacity(vertices.len());
810 for vertex in vertices {
811 let mut gaps = Vec::new();
812 let mut incident = Vec::new();
813 for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
814 gaps.push(vertex_endpoint_gap(
815 vertex.point,
816 edge.curve.evaluate(*parameter)?,
817 ));
818 incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
819 }
820 measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
821 }
822
823 Ok(CarrierDeviation {
824 band,
825 lane,
826 surface: surface_fit,
827 edges: measured_edges,
828 vertices: measured_vertices,
829 })
830}
831
832fn offset_face_carrier_impl(
833 solid: &BrepSolid,
834 face_id: u64,
835 distance: f64,
836 planar_extension: f64,
837 measure: bool,
838) -> Result<OffsetFaceCarrier, String> {
839 let source = solid
840 .shells
841 .iter()
842 .flat_map(|shell| &shell.faces)
843 .find(|face| face.id == face_id)
844 .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}"))?;
845 let band = offset_construction_band(solid_model_scale(solid));
851 let (surface, lane, surface_fit) = if measure {
852 let measured = offset_surface_measured(source, distance, planar_extension, band)?;
853 (measured.surface, measured.lane, measured.fit)
854 } else {
855 let (surface, lane) = offset_surface_with_lane(source, distance, planar_extension)?;
856 (surface, lane, None)
857 };
858 let source_edges = solid
859 .edges
860 .iter()
861 .map(|edge| (edge.id, edge))
862 .collect::<HashMap<_, _>>();
863 let source_vertices = solid
864 .vertices
865 .iter()
866 .map(|vertex| (vertex.id, vertex))
867 .collect::<HashMap<_, _>>();
868 let mut vertices = Vec::new();
869 let mut vertex_images = HashMap::default();
870 let mut edges = Vec::new();
871 let mut edge_images = HashMap::default();
872 let mut loops = Vec::new();
873 let mut next_id = 1u64;
874
875 for source_loop in &source.loops {
876 let mut coedges = Vec::new();
877 for source_coedge in &source_loop.coedges {
878 let source_edge = source_edges
879 .get(&source_coedge.edge_id)
880 .ok_or_else(|| "offset_face_carrier: missing source edge".to_string())?;
881 let (source_start, source_end) = if source_coedge.forward {
882 (source_edge.start_vertex_id, source_edge.end_vertex_id)
883 } else {
884 (source_edge.end_vertex_id, source_edge.start_vertex_id)
885 };
886 if !source_vertices.contains_key(&source_start)
887 || !source_vertices.contains_key(&source_end)
888 {
889 return Err("offset_face_carrier: missing source vertex".into());
890 }
891 let (points, parameters) =
909 mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
910 let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
911 let image_collapsed = points
912 .iter()
913 .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
914 let (edge_id, forward) =
915 if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
916 edge_images.get(&source_edge.id)
917 {
918 if source_start == source_end {
919 (*edge_id, source_coedge.forward == *creator_forward)
942 } else {
943 (
946 *edge_id,
947 vertex_images.get(&source_start) == Some(edge_start_vertex_id),
948 )
949 }
950 } else {
951 let curve = if image_collapsed {
952 NurbsCurve::new(
953 1,
954 vec![0.0, 0.0, 1.0, 1.0],
955 vec![
956 Vec4::from_point(points[0], 1.0),
957 Vec4::from_point(points[0], 1.0),
958 ],
959 )?
960 } else {
961 interpolate_curve(&points, 1, ¶meters)?
962 };
963 let start_vertex_id = claim_vertex_image(
964 source_start,
965 points[0],
966 &mut vertex_images,
967 &mut vertices,
968 &mut next_id,
969 );
970 let end_vertex_id = claim_vertex_image(
971 source_end,
972 points[points.len() - 1],
973 &mut vertex_images,
974 &mut vertices,
975 &mut next_id,
976 );
977 let id = next_id;
978 next_id += 1;
979 let domain = curve.domain()?;
980 edges.push(EdgeRecord {
981 id,
982 curve,
983 t0: domain[0],
984 t1: domain[1],
985 start_vertex_id,
986 end_vertex_id,
987 degenerate: source_edge.degenerate && image_collapsed,
991 name: source_edge
995 .name
996 .as_ref()
997 .map(|name| format!("{name}_Offset")),
998 });
999 edge_images.insert(
1000 source_edge.id,
1001 (id, start_vertex_id, source_coedge.forward),
1002 );
1003 (id, true)
1004 };
1005 let id = next_id;
1006 next_id += 1;
1007 coedges.push(CoedgeRecord {
1008 id,
1009 edge_id,
1010 forward,
1011 pcurve: source_coedge.pcurve.clone(),
1012 });
1013 }
1014 let id = next_id;
1015 next_id += 1;
1016 loops.push(LoopRecord { id, coedges });
1017 }
1018 let deviation = if measure {
1019 Some(measure_carrier(
1020 &surface,
1021 &vertices,
1022 &edges,
1023 &loops,
1024 lane,
1025 surface_fit,
1026 band,
1027 )?)
1028 } else {
1029 None
1030 };
1031 Ok(OffsetFaceCarrier {
1032 vertices,
1033 edges,
1034 face: FaceRecord {
1035 id: next_id,
1036 surface,
1037 same_sense: source.same_sense,
1038 loops,
1039 name: source.name.as_ref().map(|name| format!("{name}_Offset")),
1040 },
1041 deviation,
1042 })
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048 use crate::{make_box_brep, make_cylinder_brep};
1049
1050 #[test]
1061 fn a_full_torus_carrier_reproduces_the_source_seam_senses() {
1062 let solid =
1063 crate::make_torus_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 5.0, 1.5).unwrap();
1064 assert!(
1065 solid.validate().is_empty(),
1066 "the source torus is the oracle and must be clean"
1067 );
1068 let face = &solid.shells[0].faces[0];
1069 let source_senses: Vec<bool> = face
1070 .loops
1071 .iter()
1072 .flat_map(|loop_record| &loop_record.coedges)
1073 .map(|coedge| coedge.forward)
1074 .collect();
1075 assert_eq!(
1076 source_senses,
1077 vec![true, true, false, false],
1078 "a full torus's two seam edges are each traversed both ways"
1079 );
1080
1081 let carrier = offset_face_carrier_measured(&solid, face.id, 0.25, 0.0).unwrap();
1082 let carrier_senses: Vec<bool> = carrier
1083 .face
1084 .loops
1085 .iter()
1086 .flat_map(|loop_record| &loop_record.coedges)
1087 .map(|coedge| coedge.forward)
1088 .collect();
1089 assert_eq!(
1090 carrier_senses, source_senses,
1091 "the carrier copies the source topology, senses included"
1092 );
1093
1094 let deviation = carrier.deviation.expect("measured");
1097 for (id, measured) in &deviation.edges {
1098 assert!(
1099 measured.deviation() < 1e-3,
1100 "carrier edge {id} deviates {:.3e} from its pcurve image",
1101 measured.deviation()
1102 );
1103 }
1104 }
1105
1106 #[test]
1112 fn measuring_a_carrier_cannot_change_it() {
1113 let cases: Vec<(&str, BrepSolid)> = vec![
1114 ("box", make_box_brep(Vec3::default(), 20.0, 20.0, 4.0).unwrap()),
1115 (
1116 "cylinder",
1117 make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 5.0, 12.0).unwrap(),
1118 ),
1119 (
1120 "cone",
1121 crate::make_cone_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 5.0, 2.5, 10.0)
1122 .unwrap(),
1123 ),
1124 (
1125 "sphere",
1126 crate::make_sphere_brep(Vec3::default(), 5.0, Vec3::new(0.0, 0.0, 1.0)).unwrap(),
1127 ),
1128 ];
1129 for (label, solid) in cases {
1130 let face_ids: Vec<u64> = solid
1131 .shells
1132 .iter()
1133 .flat_map(|shell| &shell.faces)
1134 .map(|face| face.id)
1135 .collect();
1136 for face_id in face_ids {
1137 for distance in [0.25, -0.25] {
1138 for extension in [0.0, 0.5] {
1139 let plain = offset_face_carrier(&solid, face_id, distance, extension);
1140 let measured =
1141 offset_face_carrier_measured(&solid, face_id, distance, extension);
1142 match (plain, measured) {
1143 (Ok(plain), Ok(measured)) => assert_eq!(
1144 serde_json::to_string(&plain).unwrap(),
1145 serde_json::to_string(&measured).unwrap(),
1146 "{label} face {face_id} d={distance} ext={extension} moved"
1147 ),
1148 (Err(plain), Err(measured)) => assert_eq!(
1149 plain, measured,
1150 "{label} face {face_id} refusal text moved"
1151 ),
1152 (plain, measured) => panic!(
1153 "{label} face {face_id}: outcomes disagree ({:?} vs {:?})",
1154 plain.is_ok(),
1155 measured.is_ok()
1156 ),
1157 }
1158 }
1159 }
1160 }
1161 }
1162 }
1163
1164 #[test]
1168 fn the_surface_lane_names_the_branch_that_ran() {
1169 let plate = make_box_brep(Vec3::default(), 20.0, 20.0, 4.0).unwrap();
1170 let plane = &plate.shells[0].faces[0];
1171 assert_eq!(
1172 offset_surface_measured(plane, 0.25, 0.0, 1e-2).unwrap().lane,
1173 OffsetSurfaceLane::Affine
1174 );
1175 assert_eq!(
1176 offset_surface_measured(plane, 0.25, 0.5, 1e-2).unwrap().lane,
1177 OffsetSurfaceLane::Reparameterised,
1178 "a grown plane no longer names the same point at the same (u, v)"
1179 );
1180 assert!(
1181 offset_surface_measured(plane, 0.25, 0.5, 1e-2)
1182 .unwrap()
1183 .fit
1184 .is_none(),
1185 "a deliberate divergence is not reported as a fit error"
1186 );
1187
1188 let ball = crate::make_sphere_brep(Vec3::default(), 5.0, Vec3::new(0.0, 0.0, 1.0)).unwrap();
1189 let sphere = &ball.shells[0].faces[0];
1190 let measured = offset_surface_measured(sphere, 0.25, 0.0, 1e-2).unwrap();
1191 assert_eq!(measured.lane, OffsetSurfaceLane::Fit);
1192 let fit = measured.fit.expect("a fit has an error to report");
1193 assert!(
1194 fit.deviation() > 0.0 && fit.deviation() < 1e-4,
1195 "the sphere's collocation fit is small but not exact (got {:.3e})",
1196 fit.deviation()
1197 );
1198 }
1199
1200 #[test]
1201 fn affine_offset_is_exact_and_preserves_weights() {
1202 let solid = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
1203 let face = &solid.shells[0].faces[0];
1204 let offset = offset_surface(face, 0.75, 0.0).unwrap();
1205 let domain_u = KnotVector::new(face.surface.knots_u.clone(), 1)
1206 .unwrap()
1207 .domain();
1208 let domain_v = KnotVector::new(face.surface.knots_v.clone(), 1)
1209 .unwrap()
1210 .domain();
1211 let u = (domain_u[0] + domain_u[1]) / 2.0;
1212 let v = (domain_v[0] + domain_v[1]) / 2.0;
1213 let displacement = offset
1214 .evaluate(u, v)
1215 .unwrap()
1216 .sub(face.surface.evaluate(u, v).unwrap());
1217 assert!((displacement.length() - 0.75).abs() < 1e-12);
1218 }
1219
1220 #[test]
1221 fn curved_offset_carrier_maps_every_trim_to_new_surface() {
1222 let solid =
1223 make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 2.0, 4.0).unwrap();
1224 let side = &solid.shells[0].faces[0];
1225 let carrier = offset_face_carrier(&solid, side.id, 0.5, 0.0).unwrap();
1226 for coedge in carrier
1227 .face
1228 .loops
1229 .iter()
1230 .flat_map(|loop_record| &loop_record.coedges)
1231 {
1232 let edge = carrier
1233 .edges
1234 .iter()
1235 .find(|edge| edge.id == coedge.edge_id)
1236 .unwrap();
1237 for fraction in [0.0, 0.3, 0.8, 1.0] {
1238 let uv = coedge.pcurve.evaluate(fraction).unwrap();
1239 let on_surface = carrier.face.surface.evaluate(uv.x, uv.y).unwrap();
1240 let parameter = if coedge.forward {
1241 edge.t0 + (edge.t1 - edge.t0) * fraction
1242 } else {
1243 edge.t1 - (edge.t1 - edge.t0) * fraction
1244 };
1245 assert!(
1246 on_surface
1247 .sub(edge.curve.evaluate(parameter).unwrap())
1248 .length()
1249 < 7e-4
1250 );
1251 }
1252 }
1253 }
1254}