1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use crate::fit::solve_dense;
3use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, VertexRecord};
4use crate::{
5 interpolate_curve, measure_edge_against_pcurve_image,
6 measure_surface_fit_against_pointwise_offset, offset_construction_band, solid_model_scale,
7 vertex_endpoint_gap, vertex_tolerance_from_edges, KnotVector, MeasuredTolerance, NurbsCurve,
8 NurbsSurface, OffsetEvaluator, OffsetNormal, Vec2, Vec3, Vec4,
9};
10use rustc_hash::FxHashMap as HashMap;
11use serde::Serialize;
12
13fn domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
14 Ok((
15 KnotVector::new(surface.knots_u.clone(), surface.degree_u).or_refuse(KernelStage::Refine, "new")?.domain(),
16 KnotVector::new(surface.knots_v.clone(), surface.degree_v).or_refuse(KernelStage::Refine, "new")?.domain(),
17 ))
18}
19
20fn stable_face_normal(face: &FaceRecord, u: f64, v: f64) -> Result<Vec3, KernelRefusal> {
24 OffsetEvaluator::new(
25 "offset_surface",
26 &face.surface,
27 OffsetNormal::FaceStable {
28 same_sense: face.same_sense,
29 },
30 )
31 .normal(u, v)
32 .or_refuse(KernelStage::Refine, "normal")
33}
34
35fn greville_parameters(knots: &KnotVector) -> Vec<f64> {
36 let mut parameters = (0..knots.control_point_count())
37 .map(|index| {
38 knots.knots[index + 1..=index + knots.degree]
39 .iter()
40 .sum::<f64>()
41 / knots.degree as f64
42 })
43 .collect::<Vec<_>>();
44 let domain = knots.domain();
45 parameters[0] = domain[0];
46 *parameters.last_mut().unwrap() = domain[1];
47 parameters
48}
49
50fn collocation_matrix(knots: &KnotVector, parameters: &[f64], weights: &[f64]) -> Vec<Vec<f64>> {
54 parameters
55 .iter()
56 .map(|parameter| {
57 let mut row = vec![0.0; knots.control_point_count()];
58 let span = knots.find_span(*parameter);
59 for (offset, value) in knots
60 .basis_functions(span, *parameter)
61 .into_iter()
62 .enumerate()
63 {
64 let index = span - knots.degree + offset;
65 row[index] = value * weights[index];
66 }
67 let denominator: f64 = row.iter().sum();
68 if denominator.abs() > 0.0 {
69 for value in &mut row {
70 *value /= denominator;
71 }
72 }
73 row
74 })
75 .collect()
76}
77
78fn separable_weights(weights: &[Vec<f64>]) -> Option<(Vec<f64>, Vec<f64>)> {
82 let first_row = weights.first()?;
83 let anchor = *first_row.first()?;
84 if anchor.abs() <= 1e-12 {
85 return None;
86 }
87 let a: Vec<f64> = weights.iter().map(|row| row[0]).collect();
88 let b: Vec<f64> = first_row.iter().map(|w| w / anchor).collect();
89 for (i, row) in weights.iter().enumerate() {
90 for (j, &w) in row.iter().enumerate() {
91 if (w - a[i] * b[j]).abs() > 1e-10 * (1.0 + w.abs()) {
92 return None;
93 }
94 }
95 }
96 Some((a, b))
97}
98
99fn interpolate_tensor(
105 knot_u: &KnotVector,
106 knot_v: &KnotVector,
107 parameters_u: &[f64],
108 parameters_v: &[f64],
109 samples: &[Vec<Vec3>],
110 weights: &[Vec<f64>],
111) -> Result<Vec<Vec<Vec4>>, KernelRefusal> {
112 let count_u = parameters_u.len();
113 let count_v = parameters_v.len();
114 if let Some((weights_u, weights_v)) = separable_weights(weights) {
115 let matrix_u = collocation_matrix(knot_u, parameters_u, &weights_u);
116 let matrix_v = collocation_matrix(knot_v, parameters_v, &weights_v);
117 let mut intermediate = vec![vec![Vec3::default(); count_v]; count_u];
118 for column in 0..count_v {
119 let solve_axis = |axis: fn(Vec3) -> f64| {
120 solve_dense(
121 matrix_u.clone(),
122 samples.iter().map(|row| axis(row[column])).collect(),
123 )
124 };
125 let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
126 let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
127 let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
128 for row in 0..count_u {
129 intermediate[row][column] = Vec3::new(x[row], y[row], z[row]);
130 }
131 }
132 let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
133 for row in 0..count_u {
134 let solve_axis = |axis: fn(Vec3) -> f64| {
135 solve_dense(
136 matrix_v.clone(),
137 intermediate[row].iter().copied().map(axis).collect(),
138 )
139 };
140 let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
141 let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
142 let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
143 for column in 0..count_v {
144 controls[row][column] = Vec4::from_point(
145 Vec3::new(x[column], y[column], z[column]),
146 weights[row][column],
147 );
148 }
149 }
150 return Ok(controls);
151 }
152
153 let unknowns = count_u * count_v;
156 let mut matrix = vec![vec![0.0; unknowns]; unknowns];
157 for (k, &u) in parameters_u.iter().enumerate() {
158 let span_u = knot_u.find_span(u);
159 let basis_u = knot_u.basis_functions(span_u, u);
160 for (l, &v) in parameters_v.iter().enumerate() {
161 let span_v = knot_v.find_span(v);
162 let basis_v = knot_v.basis_functions(span_v, v);
163 let row = &mut matrix[k * count_v + l];
164 let mut denominator = 0.0;
165 for (du, value_u) in basis_u.iter().enumerate() {
166 let i = span_u - knot_u.degree + du;
167 for (dv, value_v) in basis_v.iter().enumerate() {
168 let j = span_v - knot_v.degree + dv;
169 let entry = value_u * value_v * weights[i][j];
170 row[i * count_v + j] = entry;
171 denominator += entry;
172 }
173 }
174 if denominator.abs() > 0.0 {
175 for value in row.iter_mut() {
176 *value /= denominator;
177 }
178 }
179 }
180 }
181 let solve_axis = |axis: fn(Vec3) -> f64| {
182 solve_dense(
183 matrix.clone(),
184 samples
185 .iter()
186 .flat_map(|row| row.iter().copied().map(axis))
187 .collect(),
188 )
189 };
190 let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
191 let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
192 let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
193 let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
194 for row in 0..count_u {
195 for column in 0..count_v {
196 let index = row * count_v + column;
197 controls[row][column] = Vec4::from_point(
198 Vec3::new(x[index], y[index], z[index]),
199 weights[row][column],
200 );
201 }
202 }
203 Ok(controls)
204}
205
206#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
216pub enum OffsetSurfaceLane {
217 Affine,
221 Analytic,
227 #[default]
231 Fit,
232 Reparameterised,
238}
239
240#[derive(Clone, Debug)]
243pub struct MeasuredOffsetSurface {
244 pub surface: NurbsSurface,
245 pub lane: OffsetSurfaceLane,
246 pub fit: Option<MeasuredTolerance>,
254}
255
256#[derive(Clone, Copy, Debug, PartialEq)]
268pub struct CarrierExtension {
269 pub u_min: f64,
270 pub u_max: f64,
271 pub v_min: f64,
272 pub v_max: f64,
273}
274
275impl CarrierExtension {
276 pub const NONE: CarrierExtension = CarrierExtension {
277 u_min: 0.0,
278 u_max: 0.0,
279 v_min: 0.0,
280 v_max: 0.0,
281 };
282
283 pub fn uniform(amount: f64) -> CarrierExtension {
284 CarrierExtension {
285 u_min: amount,
286 u_max: amount,
287 v_min: amount,
288 v_max: amount,
289 }
290 }
291
292 pub fn is_active(&self) -> bool {
293 self.u_min > 0.0 || self.u_max > 0.0 || self.v_min > 0.0 || self.v_max > 0.0
294 }
295
296 fn extends_v(&self) -> bool {
297 self.v_min > 0.0 || self.v_max > 0.0
298 }
299}
300
301fn trim_domain_fractions(
305 face: &FaceRecord,
306 [u0, u1]: [f64; 2],
307 [v0, v1]: [f64; 2],
308) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
309 let mut low = Vec2 {
310 x: f64::INFINITY,
311 y: f64::INFINITY,
312 };
313 let mut high = Vec2 {
314 x: f64::NEG_INFINITY,
315 y: f64::NEG_INFINITY,
316 };
317 for coedge in face
318 .loops
319 .iter()
320 .flat_map(|loop_record| &loop_record.coedges)
321 {
322 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
323 for sample in 0..=24 {
324 let uv = coedge
325 .pcurve
326 .evaluate(p0 + (p1 - p0) * sample as f64 / 24.0).or_refuse(KernelStage::Refine, "evaluate")?;
327 low.x = low.x.min(uv.x);
328 low.y = low.y.min(uv.y);
329 high.x = high.x.max(uv.x);
330 high.y = high.y.max(uv.y);
331 }
332 }
333 let fraction = |value: f64, start: f64, end: f64| {
334 let span = end - start;
335 if span.abs() <= f64::EPSILON || !value.is_finite() {
336 return None;
337 }
338 Some(((value - start) / span).clamp(0.0, 1.0))
339 };
340 let u = match (fraction(low.x, u0, u1), fraction(high.x, u0, u1)) {
341 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
342 _ => [0.0, 1.0],
343 };
344 let v = match (fraction(low.y, v0, v1), fraction(high.y, v0, v1)) {
345 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
346 _ => [0.0, 1.0],
347 };
348 Ok((u, v))
349}
350
351fn domain_end_moves(grow_min: f64, grow_max: f64, [ta, tb]: [f64; 2]) -> (f64, f64) {
361 let span = tb - ta;
362 if span <= 1e-6 {
363 return (grow_min, grow_max);
364 }
365 let rate = (grow_min + grow_max) / span;
366 let back = grow_min + ta * rate;
367 let forward = (1.0 - ta) * rate - grow_min;
368 (back.max(0.0), forward.max(0.0))
369}
370
371pub fn offset_surface(
375 face: &FaceRecord,
376 distance: f64,
377 planar_extension: f64,
378) -> Result<NurbsSurface, KernelRefusal> {
379 offset_surface_with_lane(face, distance, &CarrierExtension::uniform(planar_extension))
380 .map(|(surface, _)| surface)
381}
382
383pub fn offset_surface_measured(
392 face: &FaceRecord,
393 distance: f64,
394 planar_extension: f64,
395 band: f64,
396) -> Result<MeasuredOffsetSurface, KernelRefusal> {
397 offset_surface_measured_sided(
398 face,
399 distance,
400 &CarrierExtension::uniform(planar_extension),
401 band,
402 )
403}
404
405pub fn offset_surface_measured_sided(
407 face: &FaceRecord,
408 distance: f64,
409 extension: &CarrierExtension,
410 band: f64,
411) -> Result<MeasuredOffsetSurface, KernelRefusal> {
412 offset_surface_measured_sided_mode(face, distance, extension, band,
413 std::env::var("BREP_OFFSET_SPHERE_READING").map_or(true, |v| v != "0" && v != "off"))
414}
415
416fn offset_surface_measured_sided_mode(
417 face: &FaceRecord, distance: f64, extension: &CarrierExtension, band: f64, reuse: bool,
418) -> Result<MeasuredOffsetSurface, KernelRefusal> {
419 let mut sphere_reading = None;
420 let (surface, lane, extended) = offset_surface_constructed(face, distance, extension, &mut sphere_reading)?;
421 let fit = match lane {
422 OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
423 OffsetSurfaceLane::Analytic if reuse && sphere_reading.is_some() => {
428 if std::env::var_os("BREP_PROFILE").is_some() {
432 eprintln!("offset.sphere_reading.profile reused=1");
433 }
434 Some(MeasuredTolerance::new(sphere_reading.unwrap(), band))
435 }
436 OffsetSurfaceLane::Analytic | OffsetSurfaceLane::Fit => {
437 Some(measure_surface_fit_against_pointwise_offset(
438 extended.as_ref().unwrap_or(&face.surface),
439 face.same_sense,
440 &surface,
441 distance,
442 band,
443 )?)
444 }
445 OffsetSurfaceLane::Reparameterised => None,
446 };
447 Ok(MeasuredOffsetSurface { surface, lane, fit })
448}
449
450fn sphere_offset_surface(face: &FaceRecord, distance: f64, reading: &mut Option<f64>) -> Result<Option<NurbsSurface>, KernelRefusal> {
479 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
483 return Ok(None);
484 }
485 let source = &face.surface;
486 let Some((centre, radius)) = source.analytic().and_then(|shape| shape.sphere_geometry()) else {
487 return Ok(None);
488 };
489 let ([u0, u1], [v0, v1]) = domains(source)?;
490 let Ok(sample) = OffsetEvaluator::new(
497 "offset_surface",
498 source,
499 OffsetNormal::FaceStable {
500 same_sense: face.same_sense,
501 },
502 )
503 .at((u0 + u1) / 2.0, (v0 + v1) / 2.0, -distance)
504 else {
505 return Ok(None);
506 };
507 let source_radial = sample.source.sub(centre);
508 let offset_radial = sample.point.sub(centre);
509 let offset_radius = offset_radial.length();
510 let scale = radius.max(1.0);
511 if radius <= 1e-12
512 || offset_radius <= 1e-9 * scale
513 || offset_radial.dot(source_radial) <= 0.0
516 || ((offset_radius - radius).abs() - distance.abs()).abs() > 1e-9 * scale
519 {
520 return Ok(None);
521 }
522 let factor = offset_radius / radius;
523 let Ok(controls) = source
526 .control_points
527 .iter()
528 .map(|row| {
529 row.iter()
530 .map(|control| {
531 Ok(Vec4::from_point(
532 centre.add(control.point().or_refuse(KernelStage::Refine, "point")?.sub(centre).scale(factor)),
533 control.w,
534 ))
535 })
536 .collect::<Result<Vec<_>, KernelRefusal>>()
537 })
538 .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
539 else {
540 return Ok(None);
541 };
542 let Ok(surface) = NurbsSurface::new(
543 source.degree_u,
544 source.degree_v,
545 source.knots_u.clone(),
546 source.knots_v.clone(),
547 controls,
548 ) else {
549 return Ok(None);
550 };
551 match crate::offset_measure::sphere_fit_reading(
555 source,
556 face.same_sense,
557 &surface,
558 distance,
559 1e-9 * scale,
560 ) {
561 Ok(measured) if measured.within_band() => {
562 *reading = Some(measured.deviation());
563 Ok(Some(surface))
564 },
565 _ => Ok(None),
566 }
567}
568
569fn revolved_arc_offset_surface(
602 face: &FaceRecord,
603 distance: f64,
604) -> Result<Option<NurbsSurface>, KernelRefusal> {
605 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
606 return Ok(None);
607 }
608 let source = &face.surface;
609 match source.analytic() {
610 Some(crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. }) => {}
611 _ => return Ok(None),
612 }
613 let rows = &source.control_points;
614 let Some(first_row) = rows.first() else {
615 return Ok(None);
616 };
617 if rows.iter().flatten().any(|control| !(control.w > 0.0)) {
618 return Ok(None);
619 }
620 let Ok(generatrix) = NurbsCurve::new(source.degree_v, source.knots_v.clone(), first_row.clone())
621 else {
622 return Ok(None);
623 };
624 let [t0, t1] = generatrix.domain().or_refuse(KernelStage::Refine, "domain")?;
625 let at = |fraction: f64| generatrix.evaluate(t0 + (t1 - t0) * fraction);
626 let (a, b, e) = (at(0.0).or_refuse(KernelStage::Refine, "at")?, at(1.0 / 3.0).or_refuse(KernelStage::Refine, "at")?, at(2.0 / 3.0).or_refuse(KernelStage::Refine, "at")?);
628 let (ab, ae) = (b.sub(a), e.sub(a));
629 let normal = ab.cross(ae);
630 let normal_length_squared = normal.dot(normal);
631 if normal_length_squared <= 1e-24 {
632 return Ok(None);
633 }
634 let centre = a.add(
635 ae.cross(normal)
636 .scale(ab.dot(ab))
637 .add(normal.cross(ab).scale(ae.dot(ae)))
638 .scale(0.5 / normal_length_squared),
639 );
640 let radius = a.sub(centre).length();
641 let scale = (centre.length() + radius).max(1.0);
642 let band = 1e-9 * scale;
643 let unit_normal = normal.scale(1.0 / normal_length_squared.sqrt());
644 let samples = 16 * generatrix.control_points.len();
645 for index in 0..=samples {
646 let point = at(index as f64 / samples as f64).or_refuse(KernelStage::Refine, "at")?;
647 let radial = point.sub(centre);
648 if (radial.length() - radius).abs() > band || radial.dot(unit_normal).abs() > band {
649 return Ok(None);
650 }
651 }
652 let controls = first_row
655 .iter()
656 .map(|control| control.point().or_refuse(KernelStage::Refine, "point"))
657 .collect::<Result<Vec<_>, KernelRefusal>>()?;
658 let origin = controls[0];
659 let mut best = (0usize, 0usize, 0.0f64);
660 for first in 1..controls.len() {
661 for second in first + 1..controls.len() {
662 let area = controls[first]
663 .sub(origin)
664 .cross(controls[second].sub(origin))
665 .length();
666 if area > best.2 {
667 best = (first, second, area);
668 }
669 }
670 }
671 let (first, second, area) = best;
672 if area <= 1e-9 * scale * scale {
673 return Ok(None);
674 }
675 let (along_first, along_second) = (controls[first].sub(origin), controls[second].sub(origin));
676 let offset_from_origin = centre.sub(origin);
677 let (g11, g12, g22) = (
678 along_first.dot(along_first),
679 along_first.dot(along_second),
680 along_second.dot(along_second),
681 );
682 let determinant = g11 * g22 - g12 * g12;
683 let (r1, r2) = (along_first.dot(offset_from_origin), along_second.dot(offset_from_origin));
684 let s = (r1 * g22 - r2 * g12) / determinant;
685 let t = (r2 * g11 - r1 * g12) / determinant;
686 if origin
687 .add(along_first.scale(s))
688 .add(along_second.scale(t))
689 .sub(centre)
690 .length()
691 > band
692 {
693 return Ok(None);
694 }
695 let ([u0, _], [v0, v1]) = domains(source)?;
698 let Ok(sample) = OffsetEvaluator::new(
699 "offset_surface",
700 source,
701 OffsetNormal::FaceStable {
702 same_sense: face.same_sense,
703 },
704 )
705 .at(u0, (v0 + v1) / 2.0, -distance)
706 else {
707 return Ok(None);
708 };
709 let source_radial = sample.source.sub(centre);
710 let offset_radial = sample.point.sub(centre);
711 let offset_radius = offset_radial.length();
712 if (source_radial.length() - radius).abs() > band
713 || offset_radius <= band
714 || offset_radial.dot(source_radial) <= 0.0
715 || ((offset_radius - radius).abs() - distance.abs()).abs() > band
716 {
717 return Ok(None);
718 }
719 let factor = offset_radius / radius;
720 let Ok(offset_rows) = rows
721 .iter()
722 .map(|row| {
723 let points = row.iter().map(|control| control.point().or_refuse(KernelStage::Refine, "point")).collect::<Result<Vec<_>, KernelRefusal>>()?;
724 let row_origin = points[0];
725 let row_centre = row_origin
726 .add(points[first].sub(row_origin).scale(s))
727 .add(points[second].sub(row_origin).scale(t));
728 Ok(points
729 .iter()
730 .zip(row)
731 .map(|(point, control)| {
732 Vec4::from_point(row_centre.add(point.sub(row_centre).scale(factor)), control.w)
733 })
734 .collect::<Vec<_>>())
735 })
736 .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
737 else {
738 return Ok(None);
739 };
740 let Ok(surface) = NurbsSurface::new(
741 source.degree_u,
742 source.degree_v,
743 source.knots_u.clone(),
744 source.knots_v.clone(),
745 offset_rows,
746 ) else {
747 return Ok(None);
748 };
749 match measure_surface_fit_against_pointwise_offset(
750 source,
751 face.same_sense,
752 &surface,
753 distance,
754 band,
755 ) {
756 Ok(measured) if measured.within_band() => Ok(Some(surface)),
757 _ => Ok(None),
758 }
759}
760
761pub(crate) fn offset_surface_with_lane(
762 face: &FaceRecord,
763 distance: f64,
764 extension: &CarrierExtension,
765) -> Result<(NurbsSurface, OffsetSurfaceLane), KernelRefusal> {
766 offset_surface_constructed(face, distance, extension, &mut None).map(|(surface, lane, _)| (surface, lane))
767}
768
769pub(crate) fn transpose_offset_surface(surface: &NurbsSurface) -> Result<NurbsSurface, KernelRefusal> {
773 let controls = (0..surface.control_points[0].len())
774 .map(|column| surface.control_points.iter().map(|row| row[column]).collect())
775 .collect();
776 NurbsSurface::new(surface.degree_v, surface.degree_u, surface.knots_v.clone(),
777 surface.knots_u.clone(), controls).or_refuse(KernelStage::Refine, "offset_transpose")
778}
779
780fn offset_surface_constructed(
781 face: &FaceRecord,
782 distance: f64,
783 extension: &CarrierExtension,
784 sphere_reading: &mut Option<f64>,
785) -> Result<(NurbsSurface, OffsetSurfaceLane, Option<NurbsSurface>), KernelRefusal> {
786 let source = &face.surface;
787 let extension_active = extension.is_active();
788 let normalize_ruling = std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0")
794 && source.degree_u == 1 && source.control_points.len() == 2;
795 let normalize_sphere = std::env::var("BREP_OFFSET_FOLD_CHART").as_deref() != Ok("0")
796 && source.degree_u == 2 && source.degree_v == 2 && source.analytic().is_none();
797 if normalize_ruling || normalize_sphere {
798 let transposed = transpose_offset_surface(source)?;
799 let ruled = normalize_ruling && matches!(transposed.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. }));
800 let spherical = normalize_sphere && transposed.analytic().and_then(|shape| shape.sphere_geometry()).is_some();
801 if ruled || spherical {
802 let mut turned = face.clone();
803 turned.surface = transposed;
804 turned.same_sense = !turned.same_sense;
805 for coedge in turned.loops.iter_mut().flat_map(|lp| &mut lp.coedges) {
806 let controls = coedge.pcurve.control_points.iter().map(|point| {
807 let mut swapped = *point;
808 std::mem::swap(&mut swapped.x, &mut swapped.y);
809 swapped
810 }).collect();
811 coedge.pcurve = NurbsCurve::new(coedge.pcurve.degree,
812 coedge.pcurve.knots.clone(), controls).or_refuse(KernelStage::Refine, "offset_transpose_pcurve")?;
813 }
814 let turned_extension = CarrierExtension {
815 u_min: extension.v_min, u_max: extension.v_max,
816 v_min: extension.u_min, v_max: extension.u_max,
817 };
818 let (surface, lane, extended) = offset_surface_constructed(&turned, distance, &turned_extension, &mut None)?;
819 return Ok((transpose_offset_surface(&surface)?, lane,
820 extended.as_ref().map(transpose_offset_surface).transpose()?));
821 }
822 }
823 if source.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
824 let ([u0, u1], [v0, v1]) = domains(source)?;
825 let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
826 let shift = normal.scale(-distance);
827 let mut points = source
828 .control_points
829 .iter()
830 .map(|row| {
831 row.iter()
832 .map(|control| Ok(control.point().or_refuse(KernelStage::Refine, "point")?.add(shift)))
833 .collect::<Result<Vec<_>, KernelRefusal>>()
834 })
835 .collect::<Result<Vec<_>, KernelRefusal>>()?;
836 if extension_active {
837 let p00 = points[0][0];
838 let p01 = points[0][1];
839 let p10 = points[1][0];
840 let direction_u = p10.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
841 let direction_v = p01.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
842 let (trim_u, trim_v) = trim_domain_fractions(face, [u0, u1], [v0, v1])?;
848 let (back_u, forward_u) = domain_end_moves(extension.u_min, extension.u_max, trim_u);
849 let (back_v, forward_v) = domain_end_moves(extension.v_min, extension.v_max, trim_v);
850 points[0][0] = p00
851 .sub(direction_u.scale(back_u))
852 .sub(direction_v.scale(back_v));
853 points[0][1] = p01
854 .sub(direction_u.scale(back_u))
855 .add(direction_v.scale(forward_v));
856 points[1][0] = p10
857 .add(direction_u.scale(forward_u))
858 .sub(direction_v.scale(back_v));
859 points[1][1] = points[1][1]
860 .add(direction_u.scale(forward_u))
861 .add(direction_v.scale(forward_v));
862 }
863 let controls = points
864 .into_iter()
865 .enumerate()
866 .map(|(row, points)| {
867 points
868 .into_iter()
869 .enumerate()
870 .map(|(column, point)| {
871 Vec4::from_point(point, source.control_points[row][column].w)
872 })
873 .collect()
874 })
875 .collect();
876 let lane = if extension_active {
880 OffsetSurfaceLane::Reparameterised
881 } else {
882 OffsetSurfaceLane::Affine
883 };
884 return Ok((
885 NurbsSurface::new(
886 source.degree_u,
887 source.degree_v,
888 source.knots_u.clone(),
889 source.knots_v.clone(),
890 controls,
891 ).or_refuse(KernelStage::Refine, "new")?,
892 lane,
893 None,
894 ));
895 }
896
897 if let Some(surface) = sphere_offset_surface(face, distance, sphere_reading)? {
907 return Ok((surface, OffsetSurfaceLane::Analytic, None));
908 }
909 if let Some(surface) = revolved_arc_offset_surface(face, distance)? {
912 return Ok((surface, OffsetSurfaceLane::Analytic, None));
913 }
914
915 let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u).or_refuse(KernelStage::Refine, "new")?;
916 let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v).or_refuse(KernelStage::Refine, "new")?;
917 let parameters_u = greville_parameters(&knot_u);
918 let parameters_v = greville_parameters(&knot_v);
919 let evaluator = OffsetEvaluator::new(
925 "offset_surface",
926 source,
927 OffsetNormal::FaceStable {
928 same_sense: face.same_sense,
929 },
930 );
931 let mut samples = Vec::new();
932 for &u in ¶meters_u {
933 let mut row = Vec::new();
934 for &v in ¶meters_v {
935 row.push(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point);
936 }
937 samples.push(row);
938 }
939 let mut reparameterised = false;
954 let mut extended_source: Option<NurbsSurface> = None;
955 if parameters_v.len() == 2 && parameters_u.len() >= 2 {
962 let centroid = |column: usize| {
963 let mut sum = Vec3::default();
964 for row in &samples {
965 sum = sum.add(row[column]);
966 }
967 sum.scale(1.0 / samples.len() as f64)
968 };
969 let near_centroid = centroid(0);
970 let far_centroid = centroid(1);
971 let mut inverted = true;
972 let mut pinch_fraction = 0.0f64;
973 let mut near_mean = 0.0f64;
974 let mut far_mean = 0.0f64;
975 for row in &samples {
976 let near_radial = row[0].sub(near_centroid);
977 let far_radial = row[1].sub(far_centroid);
978 let near_len = near_radial.length();
979 let far_len = far_radial.length();
980 if near_len <= 1e-9 || far_len <= 1e-9 {
981 inverted = false;
982 break;
983 }
984 if near_radial.dot(far_radial) >= 0.0 {
985 inverted = false;
986 break;
987 }
988 pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
989 near_mean += near_len / samples.len() as f64;
990 far_mean += far_len / samples.len() as f64;
991 }
992 if inverted {
993 let retrim_far = far_mean <= near_mean;
994 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1018 let trim_reaches_the_pinch = if retrim_far {
1019 trim_v[1] > pinch_fraction + 1e-9
1020 } else {
1021 trim_v[0] < pinch_fraction - 1e-9
1022 };
1023 if trim_reaches_the_pinch {
1024 reparameterised = true;
1027 for row in &mut samples {
1028 let near = row[0];
1029 let far = row[1];
1030 let pinch = near.add(far.sub(near).scale(pinch_fraction));
1031 if retrim_far {
1032 row[1] = pinch;
1033 } else {
1034 row[0] = pinch;
1035 }
1036 }
1037 }
1038 }
1039 if extension.extends_v() && !inverted {
1059 let mut min_ruling = f64::MAX;
1060 let mut back_allowance = f64::MAX;
1061 let mut forward_allowance = f64::MAX;
1062 let mut extendable = true;
1063 for row in &samples {
1064 let ruling = row[1].sub(row[0]);
1065 let length = ruling.length();
1066 min_ruling = min_ruling.min(length);
1067 let near_radial = row[0].sub(near_centroid).length();
1071 let far_radial = row[1].sub(far_centroid).length();
1072 if (far_radial - near_radial).abs() > 1e-9 {
1073 let apex_at = near_radial / (near_radial - far_radial);
1074 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1075 extendable = false;
1077 break;
1078 }
1079 if apex_at < 0.0 {
1080 back_allowance = back_allowance.min(0.9 * -apex_at);
1081 } else {
1082 forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
1083 }
1084 }
1085 }
1086 let mut source_back = f64::MAX;
1094 let mut source_forward = f64::MAX;
1095 if extendable {
1096 let source_centroid = |column: usize| {
1097 let mut sum = Vec3::default();
1098 for row in &source.control_points {
1099 if let Ok(point) = row[column].point() {
1100 sum = sum.add(point);
1101 }
1102 }
1103 sum.scale(1.0 / source.control_points.len() as f64)
1104 };
1105 let (near_source, far_source) = (source_centroid(0), source_centroid(1));
1106 for row in &source.control_points {
1107 let (near, far) = (row[0].point().or_refuse(KernelStage::Refine, "point")?, row[1].point().or_refuse(KernelStage::Refine, "point")?);
1108 let near_radial = near.sub(near_source).length();
1109 let far_radial = far.sub(far_source).length();
1110 if (far_radial - near_radial).abs() > 1e-9 {
1111 let apex_at = near_radial / (near_radial - far_radial);
1112 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1113 extendable = false;
1114 break;
1115 }
1116 if apex_at < 0.0 {
1117 source_back = source_back.min(0.9 * -apex_at);
1118 } else {
1119 source_forward = source_forward.min(0.9 * (apex_at - 1.0));
1120 }
1121 }
1122 }
1123 }
1124 let (back_allowance, forward_allowance) =
1125 (back_allowance.min(source_back), forward_allowance.min(source_forward));
1126 if extendable && min_ruling > 1e-9 {
1127 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1134 let (back_world, forward_world) =
1135 domain_end_moves(extension.v_min, extension.v_max, trim_v);
1136 let (requested_back, requested_forward) = (back_world / min_ruling, forward_world / min_ruling);
1137 let apex_end = analytic_offset_apex(source, &samples, offset_fit_contract(source)).and_then(
1149 |(at, apex)| {
1150 if at > 1.0 && at - 1.0 <= requested_forward && at - 1.0 <= source_forward {
1151 Some((false, at - 1.0, apex))
1152 } else if at < 0.0 && -at <= requested_back && -at <= source_back {
1153 Some((true, -at, apex))
1154 } else {
1155 None
1156 }
1157 },
1158 );
1159 let back = match apex_end {
1160 Some((true, reach, _)) => reach,
1161 _ => requested_back.min(back_allowance),
1162 };
1163 let forward = match apex_end {
1164 Some((false, reach, _)) => reach,
1165 _ => requested_forward.min(forward_allowance),
1166 };
1167 let stretched = samples
1168 .iter()
1169 .map(|row| {
1170 let ruling = row[1].sub(row[0]);
1171 let (near, far) = (row[0].sub(ruling.scale(back)), row[1].add(ruling.scale(forward)));
1172 match apex_end {
1173 Some((true, _, apex)) => vec![apex, far],
1174 Some((false, _, apex)) => vec![near, apex],
1175 None => vec![near, far],
1176 }
1177 })
1178 .collect::<Vec<_>>();
1179 let extended = extend_along_rulings(source, back, forward)?;
1180 let extended_offsets = OffsetEvaluator::new(
1181 "offset_surface",
1182 &extended,
1183 OffsetNormal::FaceStable {
1184 same_sense: face.same_sense,
1185 },
1186 );
1187 let [v0, v1] = knot_v.domain();
1188 let mut developability = 0.0f64;
1189 for (row, &u) in stretched.iter().zip(¶meters_u) {
1190 for fraction in [0.0, 0.5, 1.0] {
1191 let exact = extended_offsets.at(u, v0 + (v1 - v0) * fraction, -distance).or_refuse(KernelStage::Refine, "at")?.point;
1192 let along = row[0].add(row[1].sub(row[0]).scale(fraction));
1193 developability = developability.max(exact.sub(along).length());
1194 }
1195 }
1196 let contract = offset_fit_contract(source);
1197 let stretch_kept = developability <= contract;
1198 samples = if stretch_kept {
1199 stretched
1200 } else {
1201 parameters_u
1202 .iter()
1203 .map(|&u| {
1204 parameters_v
1205 .iter()
1206 .map(|&v| Ok(extended_offsets.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1207 .collect::<Result<Vec<_>, KernelRefusal>>()
1208 })
1209 .collect::<Result<Vec<_>, KernelRefusal>>()?
1210 };
1211 if let Ok(path) = std::env::var("BREP_OFFSET_FIT_TRACE") {
1212 use std::io::Write;
1213 let line = format!(
1214 "extension deg=({},{}) net=({},{}) d={distance:.6} back={back:.6} forward={forward:.6} \
1215 developability={developability:.3e} contract={contract:.3e} lane={} thread={} args={}\n",
1216 source.degree_u,
1217 source.degree_v,
1218 source.control_points.len(),
1219 source.control_points[0].len(),
1220 if stretch_kept { "stretch" } else { "pointwise" },
1221 std::thread::current().name().unwrap_or("-"),
1222 std::env::args().skip(1).collect::<Vec<_>>().join(" "),
1223 );
1224 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1225 let _ = file.write_all(line.as_bytes());
1226 }
1227 }
1228 extended_source = Some(extended);
1229 }
1230 }
1231 }
1232 let reference = extended_source.as_ref().unwrap_or(source);
1233 let weights = reference
1234 .control_points
1235 .iter()
1236 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1237 .collect::<Vec<_>>();
1238 let fitted = NurbsSurface::new(
1239 source.degree_u,
1240 source.degree_v,
1241 source.knots_u.clone(),
1242 source.knots_v.clone(),
1243 interpolate_tensor(
1244 &knot_u,
1245 &knot_v,
1246 ¶meters_u,
1247 ¶meters_v,
1248 &samples,
1249 &weights,
1250 )?,
1251 ).or_refuse(KernelStage::Refine, "new")?;
1252 if reparameterised {
1253 return Ok((fitted, OffsetSurfaceLane::Reparameterised, None));
1254 }
1255 let reference_offsets = OffsetEvaluator::new(
1256 "offset_surface",
1257 reference,
1258 OffsetNormal::FaceStable {
1259 same_sense: face.same_sense,
1260 },
1261 );
1262 Ok((
1263 refine_offset_fit(reference, &reference_offsets, distance, fitted)?,
1264 OffsetSurfaceLane::Fit,
1265 extended_source,
1266 ))
1267}
1268
1269
1270fn analytic_offset_apex(source: &NurbsSurface, samples: &[Vec<Vec3>], contract: f64) -> Option<(f64, Vec3)> {
1279 let Some(crate::AnalyticSurface::RuledRevolution { frame, .. }) = crate::analytic_surface::recognize(source) else {
1280 return None;
1281 };
1282 let radial = |point: Vec3| {
1283 let offset = point.sub(frame.origin);
1284 offset.sub(frame.axis.scale(offset.dot(frame.axis)))
1285 };
1286 let mut crossings = Vec::with_capacity(samples.len());
1287 for row in samples {
1288 let (near, far) = (radial(row[0]), radial(row[1]));
1289 let step = far.sub(near);
1290 let at = -near.dot(step) / step.dot(step);
1291 if !at.is_finite() {
1292 return None;
1293 }
1294 crossings.push((at, row[0].add(row[1].sub(row[0]).scale(at))));
1295 }
1296 let count = crossings.len() as f64;
1299 let at = crossings.iter().map(|(at, _)| at).sum::<f64>() / count;
1300 let first = crossings.first()?.1;
1301 let spread = crossings.iter().fold(Vec3::default(), |sum, (_, point)| sum.add(point.sub(first)));
1302 let centre = first.add(spread.scale(1.0 / count));
1303 let apex = frame.origin.add(frame.axis.scale(centre.sub(frame.origin).dot(frame.axis)));
1304 crossings
1307 .iter()
1308 .all(|(_, point)| radial(*point).length() <= contract && point.sub(apex).length() <= contract)
1309 .then_some((at, apex))
1310}
1311
1312fn extend_along_rulings(surface: &NurbsSurface, back: f64, forward: f64) -> Result<NurbsSurface, KernelRefusal> {
1319 let rows = surface
1320 .control_points
1321 .iter()
1322 .map(|row| {
1323 let step = row[1].add(row[0].scale(-1.0));
1324 vec![row[0].add(step.scale(-back)), row[1].add(step.scale(forward))]
1325 })
1326 .collect::<Vec<_>>();
1327 if rows.iter().flatten().any(|point| point.w <= 1e-12) {
1328 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_ruled_extension_weight", format!(
1329 "offset_surface: extending a ruled face by ({back:.6}, {forward:.6}) of its rulings \
1330 carries a weight through zero; the extended face is not this surface"
1331 )));
1332 }
1333 NurbsSurface::new(surface.degree_u, 1, surface.knots_u.clone(), surface.knots_v.clone(), rows)
1334 .or_refuse(KernelStage::Refine, "new")
1335}
1336
1337fn offset_fit_contract(source: &NurbsSurface) -> f64 {
1340 let scale = crate::model_scale(
1341 source
1342 .control_points
1343 .iter()
1344 .flatten()
1345 .filter_map(|point| point.point().ok()),
1346 );
1347 crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit
1348}
1349
1350const OFFSET_FIT_REFINEMENTS: usize = 6;
1352
1353const OFFSET_FIT_MAX_CONTROLS_SEPARABLE: usize = 16_384;
1356const OFFSET_FIT_MAX_CONTROLS_TENSOR: usize = 900;
1357
1358#[derive(Clone, Debug)]
1363struct OffsetFitDeviation {
1364 along_u: f64,
1365 along_v: f64,
1366 between: f64,
1367 u_sites: Vec<(f64, f64)>,
1368 v_sites: Vec<(f64, f64)>,
1369 between_uv: (f64, f64),
1370}
1371
1372impl OffsetFitDeviation {
1373 fn worst(&self) -> f64 {
1374 self.along_u.max(self.along_v).max(self.between)
1375 }
1376
1377 fn refinement_sites(&self, bar: f64) -> (Vec<f64>, Vec<f64>) {
1378 let mut u = self.u_sites.iter().filter(|(_, miss)| *miss > bar)
1379 .map(|(parameter, _)| *parameter).collect::<Vec<_>>();
1380 let mut v = self.v_sites.iter().filter(|(_, miss)| *miss > bar)
1381 .map(|(parameter, _)| *parameter).collect::<Vec<_>>();
1382 if u.is_empty() && v.is_empty() && self.between > bar {
1383 u.push(self.between_uv.0);
1384 v.push(self.between_uv.1);
1385 }
1386 (u, v)
1387 }
1388}
1389
1390fn greville_midpoints(parameters: &[f64]) -> Vec<f64> {
1391 parameters
1392 .windows(2)
1393 .filter(|pair| pair[1] - pair[0] > 1e-12)
1394 .map(|pair| 0.5 * (pair[0] + pair[1]))
1395 .collect()
1396}
1397
1398fn offset_fit_deviation(
1399 fitted: &NurbsSurface,
1400 evaluator: &OffsetEvaluator,
1401 distance: f64,
1402) -> Result<OffsetFitDeviation, KernelRefusal> {
1403 let parameters_u = greville_parameters(&KnotVector::new(fitted.knots_u.clone(), fitted.degree_u).or_refuse(KernelStage::Refine, "new")?);
1404 let parameters_v = greville_parameters(&KnotVector::new(fitted.knots_v.clone(), fitted.degree_v).or_refuse(KernelStage::Refine, "new")?);
1405 let (middle_u, middle_v) = (greville_midpoints(¶meters_u), greville_midpoints(¶meters_v));
1406 let residual_locus = std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0");
1407 let gap = |u: f64, v: f64| -> Result<f64, KernelRefusal> {
1408 Ok(fitted.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?.sub(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point).length())
1409 };
1410 let mut deviation = OffsetFitDeviation {
1411 along_u: 0.0,
1412 along_v: 0.0,
1413 between: 0.0,
1414 u_sites: Vec::new(),
1415 v_sites: if residual_locus { middle_v.iter().map(|&v| (v, 0.0)).collect() } else { Vec::new() },
1416 between_uv: (0.0, 0.0),
1417 };
1418 for &u in &middle_u {
1419 let mut along = 0.0f64;
1420 for &v in ¶meters_v {
1421 let miss = gap(u, v)?;
1422 along = along.max(miss);
1423 deviation.along_u = deviation.along_u.max(miss);
1424 }
1425 if residual_locus { deviation.u_sites.push((u, along)); }
1426 for &v in &middle_v {
1427 let miss = gap(u, v)?;
1428 if miss > deviation.between { deviation.between_uv = (u, v); }
1429 deviation.between = deviation.between.max(miss);
1430 }
1431 }
1432 for &u in ¶meters_u {
1433 for (index, &v) in middle_v.iter().enumerate() {
1434 let miss = gap(u, v)?;
1435 deviation.along_v = deviation.along_v.max(miss);
1436 if residual_locus { deviation.v_sites[index].1 = deviation.v_sites[index].1.max(miss); }
1437 }
1438 }
1439 Ok(deviation)
1440}
1441
1442fn raise_linear_directions(
1452 surface: &NurbsSurface,
1453 in_u: bool,
1454 in_v: bool,
1455) -> Result<Option<NurbsSurface>, KernelRefusal> {
1456 fn raise(knots: &[f64], row: &[Vec4]) -> Option<(Vec<f64>, Vec<Vec4>)> {
1457 let count = row.len();
1459 if knots.len() != count + 2 {
1460 return None;
1461 }
1462 let breaks = &knots[1..=count];
1463 if breaks.windows(2).any(|pair| pair[1] - pair[0] <= 1e-12) {
1464 return None;
1465 }
1466 let mut raised_knots = vec![breaks[0]; 4];
1467 for &interior in &breaks[1..count - 1] {
1468 raised_knots.extend([interior; 3]);
1469 }
1470 raised_knots.extend([breaks[count - 1]; 4]);
1471 let mut points = Vec::with_capacity(3 * (count - 1) + 1);
1472 for pair in row.windows(2) {
1473 let step = pair[1].add(pair[0].scale(-1.0));
1474 points.push(pair[0]);
1475 points.push(pair[0].add(step.scale(1.0 / 3.0)));
1476 points.push(pair[0].add(step.scale(2.0 / 3.0)));
1477 }
1478 points.push(row[count - 1]);
1479 Some((raised_knots, points))
1480 }
1481 let mut knots_u = surface.knots_u.clone();
1482 let mut knots_v = surface.knots_v.clone();
1483 let mut degree_u = surface.degree_u;
1484 let mut degree_v = surface.degree_v;
1485 let mut net = surface.control_points.clone();
1486 if in_v {
1487 if degree_v != 1 {
1488 return Ok(None);
1489 }
1490 let mut rows = Vec::with_capacity(net.len());
1491 for row in &net {
1492 let Some((knots, points)) = raise(&surface.knots_v, row) else {
1493 return Ok(None);
1494 };
1495 knots_v = knots;
1496 rows.push(points);
1497 }
1498 net = rows;
1499 degree_v = 3;
1500 }
1501 if in_u {
1502 if degree_u != 1 {
1503 return Ok(None);
1504 }
1505 let count_v = net[0].len();
1506 let mut columns = Vec::with_capacity(count_v);
1507 for column in 0..count_v {
1508 let row = net.iter().map(|row| row[column]).collect::<Vec<_>>();
1509 let Some((knots, points)) = raise(&surface.knots_u, &row) else {
1510 return Ok(None);
1511 };
1512 knots_u = knots;
1513 columns.push(points);
1514 }
1515 let count_u = columns[0].len();
1516 net = (0..count_u)
1517 .map(|row| columns.iter().map(|column| column[row]).collect())
1518 .collect();
1519 degree_u = 3;
1520 }
1521 Ok(Some(NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")?))
1522}
1523
1524fn halve_knot_spans(surface: &NurbsSurface, in_u: bool, in_v: bool) -> Result<NurbsSurface, KernelRefusal> {
1528 fn midpoints(knots: &[f64], degree: usize) -> Vec<f64> {
1529 let last = knots.len() - 1 - degree;
1530 knots[degree..=last]
1531 .windows(2)
1532 .filter(|pair| pair[1] - pair[0] > 1e-12)
1533 .map(|pair| 0.5 * (pair[0] + pair[1]))
1534 .collect()
1535 }
1536 insert_offset_fit_knots(surface,
1537 &if in_u { midpoints(&surface.knots_u, surface.degree_u) } else { Vec::new() },
1538 &if in_v { midpoints(&surface.knots_v, surface.degree_v) } else { Vec::new() })
1539}
1540
1541fn insert_offset_fit_knots(surface: &NurbsSurface, u: &[f64], v: &[f64]) -> Result<NurbsSurface, KernelRefusal> {
1546 let refine = |degree: usize, knots: &[f64], net: Vec<Vec<Vec4>>, inserted: &[f64]| -> Result<(Vec<f64>, Vec<Vec<Vec4>>), KernelRefusal> {
1547 let mut inserted = inserted.to_vec();
1548 inserted.sort_by(f64::total_cmp);
1549 inserted.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
1550 inserted.retain(|parameter| knots.iter().filter(|knot| (**knot - *parameter).abs() <= 1e-12).count() < degree);
1551 let mut refined_knots = knots.to_vec();
1552 let mut rows = Vec::with_capacity(net.len());
1553 for row in net {
1554 let mut curve = NurbsCurve::new(degree, knots.to_vec(), row).or_refuse(KernelStage::Refine, "new")?;
1555 for ¶meter in &inserted {
1556 curve = curve.insert_knot(parameter, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
1557 }
1558 refined_knots = curve.knots.clone();
1559 rows.push(curve.control_points);
1560 }
1561 Ok((refined_knots, rows))
1562 };
1563 let mut knots_u = surface.knots_u.clone();
1564 let mut knots_v = surface.knots_v.clone();
1565 let mut net = surface.control_points.clone();
1566 if !v.is_empty() {
1567 let (knots, rows) = refine(surface.degree_v, &knots_v, net, v)?;
1568 knots_v = knots;
1569 net = rows;
1570 }
1571 if !u.is_empty() {
1572 let count_v = net[0].len();
1574 let columns = (0..count_v)
1575 .map(|column| net.iter().map(|row| row[column]).collect::<Vec<_>>())
1576 .collect::<Vec<_>>();
1577 let (knots, refined) = refine(surface.degree_u, &knots_u, columns, u)?;
1578 knots_u = knots;
1579 let count_u = refined[0].len();
1580 net = (0..count_u)
1581 .map(|row| refined.iter().map(|column| column[row]).collect())
1582 .collect();
1583 }
1584 NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
1585}
1586
1587fn refine_offset_fit(
1623 source: &NurbsSurface,
1624 evaluator: &OffsetEvaluator,
1625 distance: f64,
1626 fitted: NurbsSurface,
1627) -> Result<NurbsSurface, KernelRefusal> {
1628 let scale = crate::model_scale(
1629 source
1630 .control_points
1631 .iter()
1632 .flatten()
1633 .filter_map(|point| point.point().ok()),
1634 );
1635 let bar = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
1636 let trace = std::env::var("BREP_OFFSET_FIT_TRACE").ok();
1637 let started = web_time::Instant::now();
1638 let enabled = std::env::var("BREP_OFFSET_FIT_REFINE").as_deref() != Ok("0");
1639 let Ok(initial) = offset_fit_deviation(&fitted, evaluator, distance) else {
1640 return Ok(fitted);
1641 };
1642 let mut best = fitted;
1643 let mut best_deviation = initial.clone();
1644 let mut refined_source = source.clone();
1645 let mut rounds = 0usize;
1646 let mut residual_mode = false;
1647 let mut stop = "under bar";
1648 if enabled {
1649 while best_deviation.worst() > bar {
1650 if rounds == OFFSET_FIT_REFINEMENTS {
1651 stop = "refinement limit";
1652 break;
1653 }
1654 let (along_u, along_v) = (best_deviation.along_u > bar, best_deviation.along_v > bar);
1655 let (in_u, in_v) = if along_u || along_v {
1656 (along_u, along_v)
1657 } else {
1658 (true, true)
1659 };
1660 let (raise_u, raise_v) = (in_u && refined_source.degree_u == 1, in_v && refined_source.degree_v == 1);
1661 let raised = if raise_u || raise_v {
1662 raise_linear_directions(&refined_source, raise_u, raise_v)?
1663 } else {
1664 None
1665 };
1666 let (halve_u, halve_v) = match raised {
1667 Some(_) => (in_u && !raise_u, in_v && !raise_v),
1668 None => (in_u, in_v),
1669 };
1670 let raised = raised.unwrap_or_else(|| refined_source.clone());
1671 let candidate_source = if halve_u || halve_v {
1672 if residual_mode {
1673 let (u, v) = best_deviation.refinement_sites(bar);
1674 insert_offset_fit_knots(&raised,
1675 if halve_u { &u } else { &[] }, if halve_v { &v } else { &[] })?
1676 } else {
1677 halve_knot_spans(&raised, halve_u, halve_v)?
1678 }
1679 } else {
1680 raised
1681 };
1682 let weights = candidate_source
1683 .control_points
1684 .iter()
1685 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1686 .collect::<Vec<_>>();
1687 let controls = weights.len() * weights[0].len();
1688 let ceiling = if separable_weights(&weights).is_some() {
1689 OFFSET_FIT_MAX_CONTROLS_SEPARABLE
1690 } else {
1691 OFFSET_FIT_MAX_CONTROLS_TENSOR
1692 };
1693 if controls > ceiling {
1694 stop = "net ceiling";
1695 break;
1696 }
1697 let knot_u = KnotVector::new(candidate_source.knots_u.clone(), candidate_source.degree_u).or_refuse(KernelStage::Refine, "new")?;
1698 let knot_v = KnotVector::new(candidate_source.knots_v.clone(), candidate_source.degree_v).or_refuse(KernelStage::Refine, "new")?;
1699 let parameters_u = greville_parameters(&knot_u);
1700 let parameters_v = greville_parameters(&knot_v);
1701 let samples = parameters_u
1702 .iter()
1703 .map(|&u| {
1704 parameters_v
1705 .iter()
1706 .map(|&v| Ok(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1707 .collect::<Result<Vec<_>, KernelRefusal>>()
1708 })
1709 .collect::<Result<Vec<_>, KernelRefusal>>();
1710 let Ok(samples) = samples else {
1711 stop = "unevaluable sample";
1712 break;
1713 };
1714 let candidate = NurbsSurface::new(
1715 candidate_source.degree_u,
1716 candidate_source.degree_v,
1717 candidate_source.knots_u.clone(),
1718 candidate_source.knots_v.clone(),
1719 interpolate_tensor(&knot_u, &knot_v, ¶meters_u, ¶meters_v, &samples, &weights)?,
1720 ).or_refuse(KernelStage::Refine, "new")?;
1721 let Ok(deviation) = offset_fit_deviation(&candidate, evaluator, distance) else {
1722 stop = "unmeasurable round";
1723 break;
1724 };
1725 rounds += 1;
1726 if deviation.worst() >= best_deviation.worst() {
1727 if !residual_mode && best_deviation.worst() > offset_construction_band(scale)
1732 && std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0")
1733 {
1734 residual_mode = true;
1735 continue;
1736 }
1737 stop = "stopped improving";
1738 break;
1739 }
1740 best = candidate;
1741 best_deviation = deviation;
1742 refined_source = candidate_source;
1743 }
1744 } else if best_deviation.worst() > bar {
1745 stop = "refinement disabled";
1746 }
1747 let band = offset_construction_band(scale);
1752 let refused = enabled && best_deviation.worst() > band;
1753 if let Some(path) = trace {
1754 use std::io::Write;
1755 let caller = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
1756 let thread = std::thread::current().name().unwrap_or("-").to_string();
1757 let line = format!(
1758 "fit deg=({},{}) net=({},{})->({},{}) d={distance:.6} scale={scale:.4} bar={bar:.3e} \
1759 initial=(u {:.3e}, v {:.3e}, uv {:.3e}) final={:.3e} rounds={rounds} stop={stop} \
1760 refused={refused} seconds={:.4} thread={thread} args={caller}\n",
1761 source.degree_u,
1762 source.degree_v,
1763 source.control_points.len(),
1764 source.control_points[0].len(),
1765 best.control_points.len(),
1766 best.control_points[0].len(),
1767 initial.along_u,
1768 initial.along_v,
1769 initial.between,
1770 best_deviation.worst(),
1771 started.elapsed().as_secs_f64(),
1772 );
1773 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1774 let _ = file.write_all(line.as_bytes());
1775 }
1776 }
1777 if refused {
1778 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "offset_carrier_fit", format!(
1779 "offset_surface: the fitted offset carrier stays {:.3e} off its pointwise offset at \
1780 distance {distance} after {rounds} refinement round(s) ({stop}), past the offset \
1781 construction band {band:.3e}",
1782 best_deviation.worst()
1783 )));
1784 }
1785 Ok(best)
1786}
1787
1788pub(crate) fn unify_offset_sheet_bases(
1798 first: &NurbsSurface,
1799 second: &NurbsSurface,
1800) -> Result<(NurbsSurface, NurbsSurface), KernelRefusal> {
1801 fn raise_to(surface: &NurbsSurface, degree_u: usize, degree_v: usize) -> Result<NurbsSurface, KernelRefusal> {
1802 let raise_u = surface.degree_u == 1 && degree_u == 3;
1803 let raise_v = surface.degree_v == 1 && degree_v == 3;
1804 if !raise_u && !raise_v {
1805 return Ok(surface.clone());
1806 }
1807 raise_linear_directions(surface, raise_u, raise_v)?
1808 .ok_or_else(|| {
1809 KernelRefusal::internal(
1810 KernelStage::Refine,
1811 "offset_raise_linear",
1812 "offset sheets: a linear direction could not be raised",
1813 )
1814 })
1815 }
1816 let degree_u = first.degree_u.max(second.degree_u);
1817 let degree_v = first.degree_v.max(second.degree_v);
1818 let mut first = raise_to(first, degree_u, degree_v)?;
1819 let mut second = raise_to(second, degree_u, degree_v)?;
1820 if first.degree_u != second.degree_u || first.degree_v != second.degree_v {
1821 return Err(KernelRefusal::internal(KernelStage::Refine, "offset_common_basis", format!(
1822 "offset sheets: degrees ({},{}) and ({},{}) have no common basis",
1823 first.degree_u, first.degree_v, second.degree_u, second.degree_v
1824 )));
1825 }
1826 fn missing(from: &[f64], into: &[f64]) -> Vec<f64> {
1828 let mut extra = Vec::new();
1829 let mut index = 0usize;
1830 for &knot in from {
1831 while index < into.len() && into[index] < knot - 1e-12 {
1832 index += 1;
1833 }
1834 if index < into.len() && (into[index] - knot).abs() <= 1e-12 {
1835 index += 1;
1836 } else {
1837 extra.push(knot);
1838 }
1839 }
1840 extra
1841 }
1842 fn insert(surface: &NurbsSurface, knots: &[f64], along_u: bool) -> Result<NurbsSurface, KernelRefusal> {
1843 if knots.is_empty() {
1844 return Ok(surface.clone());
1845 }
1846 let (degree, vector) = if along_u {
1847 (surface.degree_u, &surface.knots_u)
1848 } else {
1849 (surface.degree_v, &surface.knots_v)
1850 };
1851 let lines = if along_u {
1852 (0..surface.control_points[0].len())
1853 .map(|column| surface.control_points.iter().map(|row| row[column]).collect::<Vec<_>>())
1854 .collect::<Vec<_>>()
1855 } else {
1856 surface.control_points.clone()
1857 };
1858 let mut refined_knots = vector.clone();
1859 let mut refined = Vec::with_capacity(lines.len());
1860 for line in lines {
1861 let mut curve = NurbsCurve::new(degree, vector.clone(), line).or_refuse(KernelStage::Refine, "new")?;
1862 for &knot in knots {
1863 curve = curve.insert_knot(knot, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
1864 }
1865 refined_knots = curve.knots.clone();
1866 refined.push(curve.control_points);
1867 }
1868 let net = if along_u {
1869 (0..refined[0].len())
1870 .map(|row| refined.iter().map(|column| column[row]).collect())
1871 .collect()
1872 } else {
1873 refined
1874 };
1875 let (knots_u, knots_v) = if along_u {
1876 (refined_knots, surface.knots_v.clone())
1877 } else {
1878 (surface.knots_u.clone(), refined_knots)
1879 };
1880 NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
1881 }
1882 let (to_first_u, to_second_u) = (
1883 missing(&second.knots_u, &first.knots_u),
1884 missing(&first.knots_u, &second.knots_u),
1885 );
1886 first = insert(&first, &to_first_u, true)?;
1887 second = insert(&second, &to_second_u, true)?;
1888 let (to_first_v, to_second_v) = (
1889 missing(&second.knots_v, &first.knots_v),
1890 missing(&first.knots_v, &second.knots_v),
1891 );
1892 first = insert(&first, &to_first_v, false)?;
1893 second = insert(&second, &to_second_v, false)?;
1894 Ok((first, second))
1895}
1896
1897fn carrier_edge_curve(
1919 surface: &NurbsSurface,
1920 pcurve: &NurbsCurve,
1921 points: &[Vec3],
1922 parameters: &[f64],
1923 fit_tolerance: f64,
1924) -> Result<(NurbsCurve, f64, f64), KernelRefusal> {
1925 let image = if std::env::var("BREP_CARRIER_POLYLINE").as_deref() == Ok("1") {
1927 Err("BREP_CARRIER_POLYLINE set".to_string())
1928 } else {
1929 crate::image_curve::image_curve(surface, pcurve, fit_tolerance, "offset_face_carrier")
1930 };
1931 match image {
1932 Ok(image) if image.t0 <= image.t1 => Ok((image.curve, image.t0, image.t1)),
1933 Ok(image) => {
1934 let [start, end] = image.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1937 let curve = image.curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
1938 Ok((curve, start + end - image.t0, start + end - image.t1))
1939 }
1940 Err(_) => {
1941 let curve = interpolate_curve(points, 1, parameters).or_refuse(KernelStage::Refine, "interpolate_curve")?;
1942 let [t0, t1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1943 Ok((curve, t0, t1))
1944 }
1945 }
1946}
1947
1948fn mapped_pcurve_polyline(
1949 surface: &NurbsSurface,
1950 pcurve: &NurbsCurve,
1951 degenerate: bool,
1952) -> Result<(Vec<Vec3>, Vec<f64>), KernelRefusal> {
1953 let [start, end] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1954 let evaluate = |fraction: f64| -> Result<Vec3, KernelRefusal> {
1955 let uv = pcurve.evaluate(start + (end - start) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
1956 surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")
1957 };
1958 let first = evaluate(0.0)?;
1959 let last = evaluate(1.0)?;
1960 if degenerate {
1961 return Ok((vec![first, last], vec![0.0, 1.0]));
1962 }
1963 fn append(
1964 evaluate: &impl Fn(f64) -> Result<Vec3, KernelRefusal>,
1965 a_fraction: f64,
1966 a: Vec3,
1967 b_fraction: f64,
1968 b: Vec3,
1969 depth: usize,
1970 parameters: &mut Vec<f64>,
1971 points: &mut Vec<Vec3>,
1972 ) -> Result<(), KernelRefusal> {
1973 let fractions =
1974 [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
1975 let samples = fractions
1976 .map(evaluate)
1977 .into_iter()
1978 .collect::<Result<Vec<_>, KernelRefusal>>()?;
1979 let deviation = samples
1980 .iter()
1981 .enumerate()
1982 .map(|(index, point)| {
1983 point
1984 .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
1985 .length()
1986 })
1987 .fold(0.0, f64::max);
1988 if deviation <= 5e-4 || depth >= 10 {
1989 parameters.push(b_fraction);
1990 points.push(b);
1991 return Ok(());
1992 }
1993 append(
1994 evaluate,
1995 a_fraction,
1996 a,
1997 fractions[1],
1998 samples[1],
1999 depth + 1,
2000 parameters,
2001 points,
2002 )?;
2003 append(
2004 evaluate,
2005 fractions[1],
2006 samples[1],
2007 b_fraction,
2008 b,
2009 depth + 1,
2010 parameters,
2011 points,
2012 )
2013 }
2014 let mut parameters = vec![0.0];
2015 let mut points = vec![first];
2016 append(
2017 &evaluate,
2018 0.0,
2019 first,
2020 1.0,
2021 last,
2022 0,
2023 &mut parameters,
2024 &mut points,
2025 )?;
2026 Ok((points, parameters))
2027}
2028
2029#[derive(Clone, Debug)]
2043pub struct CarrierDeviation {
2044 pub band: f64,
2047 pub lane: OffsetSurfaceLane,
2049 pub surface: Option<MeasuredTolerance>,
2051 pub edges: Vec<(u64, MeasuredTolerance)>,
2056 pub vertices: Vec<(u64, f64)>,
2060}
2061
2062impl CarrierDeviation {
2063 pub fn worst(&self) -> Option<MeasuredTolerance> {
2066 MeasuredTolerance::worst(
2067 self.surface
2068 .into_iter()
2069 .chain(self.edges.iter().map(|(_, measured)| *measured))
2070 .chain(
2071 self.vertices
2072 .iter()
2073 .map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
2074 ),
2075 )
2076 }
2077
2078 pub fn exceedances(&self) -> Vec<String> {
2081 let mut out = Vec::new();
2082 if let Some(surface) = self.surface {
2083 if surface.exceeds_band() {
2084 out.push(format!("carrier surface fit {}", surface.describe()));
2085 }
2086 }
2087 for (id, measured) in &self.edges {
2088 if measured.exceeds_band() {
2089 out.push(format!("edge {id} {}", measured.describe()));
2090 }
2091 }
2092 for (id, gap) in &self.vertices {
2093 let measured = MeasuredTolerance::new(*gap, self.band);
2094 if measured.exceeds_band() {
2095 out.push(format!("vertex {id} {}", measured.describe()));
2096 }
2097 }
2098 out
2099 }
2100}
2101
2102#[derive(Clone, Debug, Serialize)]
2103pub struct OffsetFaceCarrier {
2104 pub vertices: Vec<VertexRecord>,
2105 pub edges: Vec<EdgeRecord>,
2106 pub face: FaceRecord,
2107 #[serde(skip)]
2115 pub deviation: Option<CarrierDeviation>,
2116 #[serde(skip)]
2120 pub lane: OffsetSurfaceLane,
2121}
2122
2123fn claim_vertex_image(
2134 source_id: u64,
2135 point: Vec3,
2136 split: f64,
2137 vertex_images: &mut HashMap<u64, Vec<u64>>,
2138 vertices: &mut Vec<VertexRecord>,
2139 next_id: &mut u64,
2140) -> u64 {
2141 if let Some(ids) = vertex_images.get(&source_id) {
2142 let near = ids.iter().copied().find(|id| {
2143 vertices
2144 .iter()
2145 .find(|vertex| vertex.id == *id)
2146 .is_some_and(|vertex| vertex.point.sub(point).length() <= split)
2147 });
2148 if let Some(id) = near {
2149 return id;
2150 }
2151 }
2152 let id = *next_id;
2153 *next_id += 1;
2154 vertices.push(VertexRecord { id, point });
2155 vertex_images.entry(source_id).or_default().push(id);
2156 id
2157}
2158
2159pub fn offset_face_carrier(
2160 solid: &BrepSolid,
2161 face_id: u64,
2162 distance: f64,
2163 planar_extension: f64,
2164) -> Result<OffsetFaceCarrier, KernelRefusal> {
2165 offset_face_carrier_impl(
2166 solid,
2167 face_id,
2168 distance,
2169 &CarrierExtension::uniform(planar_extension),
2170 false,
2171 )
2172}
2173
2174pub fn offset_face_carrier_sided(
2176 solid: &BrepSolid,
2177 face_id: u64,
2178 distance: f64,
2179 extension: &CarrierExtension,
2180) -> Result<OffsetFaceCarrier, KernelRefusal> {
2181 offset_face_carrier_impl(solid, face_id, distance, extension, false)
2182}
2183
2184pub fn offset_face_carrier_measured(
2204 solid: &BrepSolid,
2205 face_id: u64,
2206 distance: f64,
2207 planar_extension: f64,
2208) -> Result<OffsetFaceCarrier, KernelRefusal> {
2209 offset_face_carrier_impl(
2210 solid,
2211 face_id,
2212 distance,
2213 &CarrierExtension::uniform(planar_extension),
2214 true,
2215 )
2216}
2217
2218fn measure_carrier(
2229 surface: &NurbsSurface,
2230 vertices: &[VertexRecord],
2231 edges: &[EdgeRecord],
2232 loops: &[LoopRecord],
2233 lane: OffsetSurfaceLane,
2234 surface_fit: Option<MeasuredTolerance>,
2235 band: f64,
2236) -> Result<CarrierDeviation, KernelRefusal> {
2237 let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
2238 let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
2242 for loop_record in loops {
2243 for coedge in &loop_record.coedges {
2244 let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
2245 continue;
2246 };
2247 let measured = measure_edge_against_pcurve_image(
2248 surface,
2249 &coedge.pcurve,
2250 edge,
2251 coedge.forward,
2252 band,
2253 )?;
2254 per_edge
2255 .entry(edge.id)
2256 .and_modify(|existing| *existing = existing.worse_of(measured))
2257 .or_insert(measured);
2258 }
2259 }
2260
2261 let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
2262 measured_edges.sort_by_key(|(id, _)| *id);
2263 let deviation_of: HashMap<u64, f64> = measured_edges
2264 .iter()
2265 .map(|(id, measured)| (*id, measured.deviation()))
2266 .collect();
2267
2268 let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
2272 for edge in edges {
2273 ends_at
2274 .entry(edge.start_vertex_id)
2275 .or_default()
2276 .push((edge, edge.t0));
2277 ends_at
2278 .entry(edge.end_vertex_id)
2279 .or_default()
2280 .push((edge, edge.t1));
2281 }
2282
2283 let mut measured_vertices = Vec::with_capacity(vertices.len());
2284 for vertex in vertices {
2285 let mut gaps = Vec::new();
2286 let mut incident = Vec::new();
2287 for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
2288 gaps.push(vertex_endpoint_gap(
2289 vertex.point,
2290 edge.curve.evaluate(*parameter).or_refuse(KernelStage::Refine, "evaluate")?,
2291 ));
2292 incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
2293 }
2294 measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
2295 }
2296
2297 Ok(CarrierDeviation {
2298 band,
2299 lane,
2300 surface: surface_fit,
2301 edges: measured_edges,
2302 vertices: measured_vertices,
2303 })
2304}
2305
2306fn offset_face_carrier_impl(
2307 solid: &BrepSolid,
2308 face_id: u64,
2309 distance: f64,
2310 extension: &CarrierExtension,
2311 measure: bool,
2312) -> Result<OffsetFaceCarrier, KernelRefusal> {
2313 let source = solid
2314 .shells
2315 .iter()
2316 .flat_map(|shell| &shell.faces)
2317 .find(|face| face.id == face_id)
2318 .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}")).or_refuse(KernelStage::Refine, "offset_missing_face")?;
2319 let band = offset_construction_band(solid_model_scale(solid));
2327 let fit_tolerance =
2334 crate::KernelTolerances::for_scale(solid_model_scale(solid), 1e-7).intersection_fit;
2335 let (surface, lane, surface_fit) = if measure {
2336 let measured = offset_surface_measured_sided(source, distance, extension, band)?;
2337 (measured.surface, measured.lane, measured.fit)
2338 } else {
2339 let (surface, lane) = offset_surface_with_lane(source, distance, extension)?;
2340 (surface, lane, None)
2341 };
2342 let source_edges = solid
2343 .edges
2344 .iter()
2345 .map(|edge| (edge.id, edge))
2346 .collect::<HashMap<_, _>>();
2347 let source_vertices = solid
2348 .vertices
2349 .iter()
2350 .map(|vertex| (vertex.id, vertex))
2351 .collect::<HashMap<_, _>>();
2352 let mut vertices = Vec::new();
2353 let mut vertex_images = HashMap::default();
2354 let vertex_split = (distance.abs() * 0.1).max(band);
2358 let mut edges = Vec::new();
2359 let mut edge_images = HashMap::default();
2360 let mut loops = Vec::new();
2361 let mut next_id = 1u64;
2362
2363 for source_loop in &source.loops {
2364 let mut coedges = Vec::new();
2365 for source_coedge in &source_loop.coedges {
2366 let source_edge = source_edges
2367 .get(&source_coedge.edge_id)
2368 .ok_or_else(|| "offset_face_carrier: missing source edge".to_string()).or_refuse(KernelStage::Refine, "offset_missing_edge")?;
2369 let (source_start, source_end) = if source_coedge.forward {
2370 (source_edge.start_vertex_id, source_edge.end_vertex_id)
2371 } else {
2372 (source_edge.end_vertex_id, source_edge.start_vertex_id)
2373 };
2374 if !source_vertices.contains_key(&source_start)
2375 || !source_vertices.contains_key(&source_end)
2376 {
2377 return Err(KernelRefusal::internal(KernelStage::Refine, "offset_missing_vertex", "offset_face_carrier: missing source vertex"));
2378 }
2379 let (points, parameters) =
2397 mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
2398 let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
2399 let image_collapsed = points
2400 .iter()
2401 .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
2402 let (edge_id, forward) =
2403 if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
2404 edge_images.get(&source_edge.id)
2405 {
2406 if source_start == source_end {
2407 (*edge_id, source_coedge.forward == *creator_forward)
2430 } else {
2431 (
2434 *edge_id,
2435 vertex_images
2436 .get(&source_start)
2437 .is_some_and(|ids: &Vec<u64>| ids.contains(edge_start_vertex_id)),
2438 )
2439 }
2440 } else {
2441 let (curve, t0, t1) = if image_collapsed {
2442 let curve = NurbsCurve::new(
2443 1,
2444 vec![0.0, 0.0, 1.0, 1.0],
2445 vec![
2446 Vec4::from_point(points[0], 1.0),
2447 Vec4::from_point(points[0], 1.0),
2448 ],
2449 ).or_refuse(KernelStage::Refine, "new")?;
2450 (curve, 0.0, 1.0)
2451 } else {
2452 carrier_edge_curve(
2453 &surface,
2454 &source_coedge.pcurve,
2455 &points,
2456 ¶meters,
2457 fit_tolerance,
2458 )?
2459 };
2460 let start_vertex_id = claim_vertex_image(
2461 source_start,
2462 points[0],
2463 vertex_split,
2464 &mut vertex_images,
2465 &mut vertices,
2466 &mut next_id,
2467 );
2468 let end_vertex_id = claim_vertex_image(
2469 source_end,
2470 points[points.len() - 1],
2471 vertex_split,
2472 &mut vertex_images,
2473 &mut vertices,
2474 &mut next_id,
2475 );
2476 let id = next_id;
2477 next_id += 1;
2478 edges.push(EdgeRecord {
2479 id,
2480 curve,
2481 t0,
2482 t1,
2483 start_vertex_id,
2484 end_vertex_id,
2485 degenerate: image_collapsed,
2499 name: source_edge
2503 .name
2504 .as_ref()
2505 .map(|name| format!("{name}_Offset")),
2506 });
2507 edge_images.insert(
2508 source_edge.id,
2509 (id, start_vertex_id, source_coedge.forward),
2510 );
2511 (id, true)
2512 };
2513 let id = next_id;
2514 next_id += 1;
2515 coedges.push(CoedgeRecord {
2516 id,
2517 edge_id,
2518 forward,
2519 pcurve: source_coedge.pcurve.clone(),
2520 });
2521 }
2522 let id = next_id;
2523 next_id += 1;
2524 loops.push(LoopRecord { id, coedges });
2525 }
2526 let deviation = if measure {
2527 Some(measure_carrier(
2528 &surface,
2529 &vertices,
2530 &edges,
2531 &loops,
2532 lane,
2533 surface_fit,
2534 band,
2535 )?)
2536 } else {
2537 None
2538 };
2539 Ok(OffsetFaceCarrier {
2540 vertices,
2541 edges,
2542 face: FaceRecord {
2543 id: next_id,
2544 surface,
2545 same_sense: source.same_sense,
2546 loops,
2547 name: source.name.as_ref().map(|name| format!("{name}_Offset")),
2548 },
2549 deviation,
2550 lane,
2551 })
2552}
2553
2554