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
301const EXACT_TRIM_BOX_DEFAULT: bool = true;
310
311fn exact_trim_box_on() -> bool {
312 switch!(live "BREP_OFFSET_EXACT_TRIM_BOX", EXACT_TRIM_BOX_DEFAULT)
313}
314
315
316const TRIM_EXTREME_STEPS: usize = 64;
320
321fn refine_trim_extreme(
330 pcurve: &NurbsCurve,
331 samples: &[(f64, Vec3)],
332 axis: usize,
333 sign: f64,
334) -> Result<f64, KernelRefusal> {
335 let read = |uv: Vec3| sign * if axis == 0 { uv.x } else { uv.y };
336 let at = |parameter: f64| -> Result<f64, KernelRefusal> {
337 Ok(read(pcurve.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?))
338 };
339 let best = (0..samples.len())
340 .max_by(|&a, &b| read(samples[a].1).total_cmp(&read(samples[b].1)))
341 .expect("a pcurve has samples");
342 let mut found = read(samples[best].1);
343 let (mut a, mut b) = (samples[best.saturating_sub(1)].0, samples[(best + 1).min(samples.len() - 1)].0);
344 const GOLDEN: f64 = 0.618_033_988_749_894_9;
345 let mut c = b - GOLDEN * (b - a);
346 let mut d = a + GOLDEN * (b - a);
347 let (mut fc, mut fd) = (at(c)?, at(d)?);
348 for _ in 0..TRIM_EXTREME_STEPS {
349 found = found.max(fc).max(fd);
350 if fc >= fd {
351 (b, d, fd) = (d, c, fc);
352 c = b - GOLDEN * (b - a);
353 fc = at(c)?;
354 } else {
355 (a, c, fc) = (c, d, fd);
356 d = a + GOLDEN * (b - a);
357 fd = at(d)?;
358 }
359 }
360 Ok(sign * found.max(fc).max(fd))
361}
362
363fn trim_domain_fractions(
368 face: &FaceRecord,
369 [u0, u1]: [f64; 2],
370 [v0, v1]: [f64; 2],
371) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
372 let mut low = Vec2 {
373 x: f64::INFINITY,
374 y: f64::INFINITY,
375 };
376 let mut high = Vec2 {
377 x: f64::NEG_INFINITY,
378 y: f64::NEG_INFINITY,
379 };
380 for coedge in face
381 .loops
382 .iter()
383 .flat_map(|loop_record| &loop_record.coedges)
384 {
385 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
386 let mut samples = Vec::with_capacity(25);
387 for sample in 0..=24 {
388 let parameter = p0 + (p1 - p0) * sample as f64 / 24.0;
389 let uv = coedge
390 .pcurve
391 .evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?;
392 low.x = low.x.min(uv.x);
393 low.y = low.y.min(uv.y);
394 high.x = high.x.max(uv.x);
395 high.y = high.y.max(uv.y);
396 samples.push((parameter, uv));
397 }
398 if coedge.pcurve.degree > 1 && exact_trim_box_on() {
399 low.x = low.x.min(refine_trim_extreme(&coedge.pcurve, &samples, 0, -1.0)?);
400 high.x = high.x.max(refine_trim_extreme(&coedge.pcurve, &samples, 0, 1.0)?);
401 low.y = low.y.min(refine_trim_extreme(&coedge.pcurve, &samples, 1, -1.0)?);
402 high.y = high.y.max(refine_trim_extreme(&coedge.pcurve, &samples, 1, 1.0)?);
403 }
404 }
405 let fraction = |value: f64, start: f64, end: f64| {
406 let span = end - start;
407 if span.abs() <= f64::EPSILON || !value.is_finite() {
408 return None;
409 }
410 Some(((value - start) / span).clamp(0.0, 1.0))
411 };
412 let u = match (fraction(low.x, u0, u1), fraction(high.x, u0, u1)) {
413 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
414 _ => [0.0, 1.0],
415 };
416 let v = match (fraction(low.y, v0, v1), fraction(high.y, v0, v1)) {
417 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
418 _ => [0.0, 1.0],
419 };
420 Ok((u, v))
421}
422
423fn domain_end_moves(grow_min: f64, grow_max: f64, [ta, tb]: [f64; 2]) -> (f64, f64) {
433 let span = tb - ta;
434 if span <= 1e-6 {
435 return (grow_min, grow_max);
436 }
437 let rate = (grow_min + grow_max) / span;
438 let back = grow_min + ta * rate;
439 let forward = (1.0 - ta) * rate - grow_min;
440 (back.max(0.0), forward.max(0.0))
441}
442
443pub fn offset_surface(
447 face: &FaceRecord,
448 distance: f64,
449 planar_extension: f64,
450) -> Result<NurbsSurface, KernelRefusal> {
451 offset_surface_with_lane(face, distance, &CarrierExtension::uniform(planar_extension))
452 .map(|(surface, _)| surface)
453}
454
455pub fn offset_surface_measured(
464 face: &FaceRecord,
465 distance: f64,
466 planar_extension: f64,
467 band: f64,
468) -> Result<MeasuredOffsetSurface, KernelRefusal> {
469 offset_surface_measured_sided(
470 face,
471 distance,
472 &CarrierExtension::uniform(planar_extension),
473 band,
474 )
475}
476
477pub fn offset_surface_measured_sided(
479 face: &FaceRecord,
480 distance: f64,
481 extension: &CarrierExtension,
482 band: f64,
483) -> Result<MeasuredOffsetSurface, KernelRefusal> {
484 offset_surface_measured_sided_mode(face, distance, extension, band,
485 std::env::var("BREP_OFFSET_SPHERE_READING").map_or(true, |v| v != "0" && v != "off"))
486}
487
488fn offset_surface_measured_sided_mode(
489 face: &FaceRecord, distance: f64, extension: &CarrierExtension, band: f64, reuse: bool,
490) -> Result<MeasuredOffsetSurface, KernelRefusal> {
491 let mut sphere_reading = None;
492 let (surface, lane, extended) = offset_surface_constructed(face, distance, extension, &mut sphere_reading)?;
493 let fit = match lane {
494 OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
495 OffsetSurfaceLane::Analytic if reuse && sphere_reading.is_some() => {
500 if crate::profile::profile_enabled() {
504 profile_line!("offset.sphere_reading.profile reused=1");
505 }
506 Some(MeasuredTolerance::new(sphere_reading.unwrap(), band))
507 }
508 OffsetSurfaceLane::Analytic | OffsetSurfaceLane::Fit => {
509 Some(measure_surface_fit_against_pointwise_offset(
510 extended.as_ref().unwrap_or(&face.surface),
511 face.same_sense,
512 &surface,
513 distance,
514 band,
515 )?)
516 }
517 OffsetSurfaceLane::Reparameterised => None,
518 };
519 Ok(MeasuredOffsetSurface { surface, lane, fit })
520}
521
522fn sphere_offset_surface(face: &FaceRecord, distance: f64, reading: &mut Option<f64>) -> Result<Option<NurbsSurface>, KernelRefusal> {
551 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
555 return Ok(None);
556 }
557 let source = &face.surface;
558 let Some((centre, radius)) = source.analytic().and_then(|shape| shape.sphere_geometry()) else {
559 return Ok(None);
560 };
561 let ([u0, u1], [v0, v1]) = domains(source)?;
562 let Ok(sample) = OffsetEvaluator::new(
569 "offset_surface",
570 source,
571 OffsetNormal::FaceStable {
572 same_sense: face.same_sense,
573 },
574 )
575 .at((u0 + u1) / 2.0, (v0 + v1) / 2.0, -distance)
576 else {
577 return Ok(None);
578 };
579 let source_radial = sample.source.sub(centre);
580 let offset_radial = sample.point.sub(centre);
581 let offset_radius = offset_radial.length();
582 let scale = radius.max(1.0);
583 if radius <= 1e-12
584 || offset_radius <= 1e-9 * scale
585 || offset_radial.dot(source_radial) <= 0.0
588 || ((offset_radius - radius).abs() - distance.abs()).abs() > 1e-9 * scale
591 {
592 return Ok(None);
593 }
594 let factor = offset_radius / radius;
595 let Ok(controls) = source
598 .control_points
599 .iter()
600 .map(|row| {
601 row.iter()
602 .map(|control| {
603 Ok(Vec4::from_point(
604 centre.add(control.point().or_refuse(KernelStage::Refine, "point")?.sub(centre).scale(factor)),
605 control.w,
606 ))
607 })
608 .collect::<Result<Vec<_>, KernelRefusal>>()
609 })
610 .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
611 else {
612 return Ok(None);
613 };
614 let Ok(surface) = NurbsSurface::new(
615 source.degree_u,
616 source.degree_v,
617 source.knots_u.clone(),
618 source.knots_v.clone(),
619 controls,
620 ) else {
621 return Ok(None);
622 };
623 match crate::offset_measure::sphere_fit_reading(
627 source,
628 face.same_sense,
629 &surface,
630 distance,
631 1e-9 * scale,
632 ) {
633 Ok(measured) if measured.within_band() => {
634 *reading = Some(measured.deviation());
635 Ok(Some(surface))
636 },
637 _ => Ok(None),
638 }
639}
640
641fn revolved_arc_offset_surface(
674 face: &FaceRecord,
675 distance: f64,
676) -> Result<Option<NurbsSurface>, KernelRefusal> {
677 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
678 return Ok(None);
679 }
680 let source = &face.surface;
681 match source.analytic() {
682 Some(crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. }) => {}
683 _ => return Ok(None),
684 }
685 let rows = &source.control_points;
686 let Some(first_row) = rows.first() else {
687 return Ok(None);
688 };
689 if rows.iter().flatten().any(|control| !(control.w > 0.0)) {
690 return Ok(None);
691 }
692 let Ok(generatrix) = NurbsCurve::new(source.degree_v, source.knots_v.clone(), first_row.clone())
693 else {
694 return Ok(None);
695 };
696 let [t0, t1] = generatrix.domain().or_refuse(KernelStage::Refine, "domain")?;
697 let at = |fraction: f64| generatrix.evaluate(t0 + (t1 - t0) * fraction);
698 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")?);
700 let (ab, ae) = (b.sub(a), e.sub(a));
701 let normal = ab.cross(ae);
702 let normal_length_squared = normal.dot(normal);
703 let Some(centre) = crate::circumcenter(a, b, e) else { return Ok(None); };
704 let radius = a.sub(centre).length();
705 let scale = (centre.length() + radius).max(1.0);
706 let band = 1e-9 * scale;
707 let unit_normal = normal.scale(1.0 / normal_length_squared.sqrt());
708 let samples = 16 * generatrix.control_points.len();
709 for index in 0..=samples {
710 let point = at(index as f64 / samples as f64).or_refuse(KernelStage::Refine, "at")?;
711 let radial = point.sub(centre);
712 if (radial.length() - radius).abs() > band || radial.dot(unit_normal).abs() > band {
713 return Ok(None);
714 }
715 }
716 let controls = first_row
719 .iter()
720 .map(|control| control.point().or_refuse(KernelStage::Refine, "point"))
721 .collect::<Result<Vec<_>, KernelRefusal>>()?;
722 let origin = controls[0];
723 let mut best = (0usize, 0usize, 0.0f64);
724 for first in 1..controls.len() {
725 for second in first + 1..controls.len() {
726 let area = controls[first]
727 .sub(origin)
728 .cross(controls[second].sub(origin))
729 .length();
730 if area > best.2 {
731 best = (first, second, area);
732 }
733 }
734 }
735 let (first, second, area) = best;
736 if area <= 1e-9 * scale * scale {
737 return Ok(None);
738 }
739 let (along_first, along_second) = (controls[first].sub(origin), controls[second].sub(origin));
740 let offset_from_origin = centre.sub(origin);
741 let (g11, g12, g22) = (
742 along_first.dot(along_first),
743 along_first.dot(along_second),
744 along_second.dot(along_second),
745 );
746 let determinant = g11 * g22 - g12 * g12;
747 let (r1, r2) = (along_first.dot(offset_from_origin), along_second.dot(offset_from_origin));
748 let s = (r1 * g22 - r2 * g12) / determinant;
749 let t = (r2 * g11 - r1 * g12) / determinant;
750 if origin
751 .add(along_first.scale(s))
752 .add(along_second.scale(t))
753 .sub(centre)
754 .length()
755 > band
756 {
757 return Ok(None);
758 }
759 let ([u0, _], [v0, v1]) = domains(source)?;
762 let Ok(sample) = OffsetEvaluator::new(
763 "offset_surface",
764 source,
765 OffsetNormal::FaceStable {
766 same_sense: face.same_sense,
767 },
768 )
769 .at(u0, (v0 + v1) / 2.0, -distance)
770 else {
771 return Ok(None);
772 };
773 let source_radial = sample.source.sub(centre);
774 let offset_radial = sample.point.sub(centre);
775 let offset_radius = offset_radial.length();
776 if (source_radial.length() - radius).abs() > band
777 || offset_radius <= band
778 || offset_radial.dot(source_radial) <= 0.0
779 || ((offset_radius - radius).abs() - distance.abs()).abs() > band
780 {
781 return Ok(None);
782 }
783 let factor = offset_radius / radius;
784 let Ok(offset_rows) = rows
785 .iter()
786 .map(|row| {
787 let points = row.iter().map(|control| control.point().or_refuse(KernelStage::Refine, "point")).collect::<Result<Vec<_>, KernelRefusal>>()?;
788 let row_origin = points[0];
789 let row_centre = row_origin
790 .add(points[first].sub(row_origin).scale(s))
791 .add(points[second].sub(row_origin).scale(t));
792 Ok(points
793 .iter()
794 .zip(row)
795 .map(|(point, control)| {
796 Vec4::from_point(row_centre.add(point.sub(row_centre).scale(factor)), control.w)
797 })
798 .collect::<Vec<_>>())
799 })
800 .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
801 else {
802 return Ok(None);
803 };
804 let Ok(surface) = NurbsSurface::new(
805 source.degree_u,
806 source.degree_v,
807 source.knots_u.clone(),
808 source.knots_v.clone(),
809 offset_rows,
810 ) else {
811 return Ok(None);
812 };
813 match measure_surface_fit_against_pointwise_offset(
814 source,
815 face.same_sense,
816 &surface,
817 distance,
818 band,
819 ) {
820 Ok(measured) if measured.within_band() => Ok(Some(surface)),
821 _ => Ok(None),
822 }
823}
824
825pub(crate) fn exact_cylinder_offsets_on() -> bool {
830 switch!("BREP_OFFSET_EXACT_CYLINDER", true)
831}
832
833
834pub(crate) fn cylinder_offset_factor(
841 surface: &NurbsSurface,
842 same_sense: bool,
843 distance: f64,
844) -> Result<Option<(Vec3, Vec3, f64, f64)>, KernelRefusal> {
845 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
846 return Ok(None);
847 }
848 let Some(crate::AnalyticSurface::RuledRevolution { frame, rho0, rho1, height }) = surface.analytic() else {
849 return Ok(None);
850 };
851 let radius = 0.5 * (rho0 + rho1);
852 if !(radius > 0.0) || (rho0 - rho1).abs() > 1e-9 * radius.max(1.0) {
853 return Ok(None);
854 }
855 let Ok(axis) = frame.axis.normalized() else {
856 return Ok(None);
857 };
858 let origin = frame.origin;
859 let band = 1e-9 * (origin.length() + radius + height.abs()).max(1.0);
860 let perpendicular = |point: Vec3| {
861 let along = point.sub(origin);
862 along.sub(axis.scale(along.dot(axis)))
863 };
864 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
865 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
866 let Ok(sample) = OffsetEvaluator::new("offset_surface", surface, OffsetNormal::FaceStable { same_sense })
867 .at((u0 + u1) * 0.5, (v0 + v1) * 0.5, -distance)
868 else {
869 return Ok(None);
870 };
871 let (source_radial, offset_radial) = (perpendicular(sample.source), perpendicular(sample.point));
872 let offset_radius = offset_radial.length();
873 if (source_radial.length() - radius).abs() > band
874 || offset_radius <= band
875 || offset_radial.dot(source_radial) <= 0.0
876 || ((offset_radius - radius).abs() - distance.abs()).abs() > band
877 || sample.point.sub(sample.source).dot(axis).abs() > band
878 {
879 return Ok(None);
880 }
881 Ok(Some((origin, axis, radius, offset_radius / radius)))
882}
883
884fn cylinder_offset_surface(
897 reference: &NurbsSurface,
898 same_sense: bool,
899 distance: f64,
900) -> Result<Option<NurbsSurface>, KernelRefusal> {
901 let Some((origin, axis, radius, factor)) = cylinder_offset_factor(reference, same_sense, distance)? else {
902 return Ok(None);
903 };
904 if reference.control_points.iter().flatten().any(|control| !(control.w > 0.0)) {
905 return Ok(None);
906 }
907 let Ok(rows) = reference
908 .control_points
909 .iter()
910 .map(|row| {
911 row.iter()
912 .map(|control| {
913 let point = control.point()?;
914 let foot = origin.add(axis.scale(point.sub(origin).dot(axis)));
915 Ok(Vec4::from_point(foot.add(point.sub(foot).scale(factor)), control.w))
916 })
917 .collect::<Result<Vec<_>, String>>()
918 })
919 .collect::<Result<Vec<Vec<_>>, String>>()
920 else {
921 return Ok(None);
922 };
923 let Ok(surface) = NurbsSurface::new(
924 reference.degree_u,
925 reference.degree_v,
926 reference.knots_u.clone(),
927 reference.knots_v.clone(),
928 rows,
929 ) else {
930 return Ok(None);
931 };
932 let band = 1e-9 * (origin.length() + radius * factor.max(1.0)).max(1.0);
938 let evaluator = OffsetEvaluator::new("offset_surface", reference, OffsetNormal::FaceStable { same_sense });
939 let [u0, u1] = reference.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
940 let [v0, v1] = reference.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
941 for i in 0..=4 {
942 for j in 0..=2 {
943 let (u, v) = (u0 + (u1 - u0) * i as f64 / 4.0, v0 + (v1 - v0) * j as f64 / 2.0);
944 let (Ok(want), Ok(got)) = (evaluator.at(u, v, -distance), surface.evaluate(u, v)) else {
945 return Ok(None);
946 };
947 if !(want.point.sub(got).length() <= band) {
948 return Ok(None);
949 }
950 }
951 }
952 Ok(Some(surface))
953}
954
955pub(crate) fn offset_surface_with_lane(
956 face: &FaceRecord,
957 distance: f64,
958 extension: &CarrierExtension,
959) -> Result<(NurbsSurface, OffsetSurfaceLane), KernelRefusal> {
960 offset_surface_constructed(face, distance, extension, &mut None).map(|(surface, lane, _)| (surface, lane))
961}
962
963pub(crate) fn transpose_offset_surface(surface: &NurbsSurface) -> Result<NurbsSurface, KernelRefusal> {
967 let controls = (0..surface.control_points[0].len())
968 .map(|column| surface.control_points.iter().map(|row| row[column]).collect())
969 .collect();
970 NurbsSurface::new(surface.degree_v, surface.degree_u, surface.knots_v.clone(),
971 surface.knots_u.clone(), controls).or_refuse(KernelStage::Refine, "offset_transpose")
972}
973
974fn offset_surface_constructed(
975 face: &FaceRecord,
976 distance: f64,
977 extension: &CarrierExtension,
978 sphere_reading: &mut Option<f64>,
979) -> Result<(NurbsSurface, OffsetSurfaceLane, Option<NurbsSurface>), KernelRefusal> {
980 let source = &face.surface;
981 let extension_active = extension.is_active();
982 let normalize_ruling = std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0")
988 && source.degree_u == 1 && source.control_points.len() == 2;
989 let normalize_sphere = std::env::var("BREP_OFFSET_FOLD_CHART").as_deref() != Ok("0")
990 && source.degree_u == 2 && source.degree_v == 2 && source.analytic().is_none();
991 if normalize_ruling || normalize_sphere {
992 let transposed = transpose_offset_surface(source)?;
993 let ruled = normalize_ruling && matches!(transposed.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. }));
994 let spherical = normalize_sphere && transposed.analytic().and_then(|shape| shape.sphere_geometry()).is_some();
995 if ruled || spherical {
996 let mut turned = face.clone();
997 turned.surface = transposed;
998 turned.same_sense = !turned.same_sense;
999 for coedge in turned.loops.iter_mut().flat_map(|lp| &mut lp.coedges) {
1000 let controls = coedge.pcurve.control_points.iter().map(|point| {
1001 let mut swapped = *point;
1002 std::mem::swap(&mut swapped.x, &mut swapped.y);
1003 swapped
1004 }).collect();
1005 coedge.pcurve = NurbsCurve::new(coedge.pcurve.degree,
1006 coedge.pcurve.knots.clone(), controls).or_refuse(KernelStage::Refine, "offset_transpose_pcurve")?;
1007 }
1008 let turned_extension = CarrierExtension {
1009 u_min: extension.v_min, u_max: extension.v_max,
1010 v_min: extension.u_min, v_max: extension.u_max,
1011 };
1012 let (surface, lane, extended) = offset_surface_constructed(&turned, distance, &turned_extension, &mut None)?;
1013 return Ok((transpose_offset_surface(&surface)?, lane,
1014 extended.as_ref().map(transpose_offset_surface).transpose()?));
1015 }
1016 }
1017 if source.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
1018 let ([u0, u1], [v0, v1]) = domains(source)?;
1019 let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
1020 let shift = normal.scale(-distance);
1021 let mut points = source
1022 .control_points
1023 .iter()
1024 .map(|row| {
1025 row.iter()
1026 .map(|control| Ok(control.point().or_refuse(KernelStage::Refine, "point")?.add(shift)))
1027 .collect::<Result<Vec<_>, KernelRefusal>>()
1028 })
1029 .collect::<Result<Vec<_>, KernelRefusal>>()?;
1030 if extension_active {
1031 let p00 = points[0][0];
1032 let p01 = points[0][1];
1033 let p10 = points[1][0];
1034 let direction_u = p10.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
1035 let direction_v = p01.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
1036 let (trim_u, trim_v) = trim_domain_fractions(face, [u0, u1], [v0, v1])?;
1042 let (back_u, forward_u) = domain_end_moves(extension.u_min, extension.u_max, trim_u);
1043 let (back_v, forward_v) = domain_end_moves(extension.v_min, extension.v_max, trim_v);
1044 points[0][0] = p00
1045 .sub(direction_u.scale(back_u))
1046 .sub(direction_v.scale(back_v));
1047 points[0][1] = p01
1048 .sub(direction_u.scale(back_u))
1049 .add(direction_v.scale(forward_v));
1050 points[1][0] = p10
1051 .add(direction_u.scale(forward_u))
1052 .sub(direction_v.scale(back_v));
1053 points[1][1] = points[1][1]
1054 .add(direction_u.scale(forward_u))
1055 .add(direction_v.scale(forward_v));
1056 }
1057 let controls = points
1058 .into_iter()
1059 .enumerate()
1060 .map(|(row, points)| {
1061 points
1062 .into_iter()
1063 .enumerate()
1064 .map(|(column, point)| {
1065 Vec4::from_point(point, source.control_points[row][column].w)
1066 })
1067 .collect()
1068 })
1069 .collect();
1070 let lane = if extension_active {
1074 OffsetSurfaceLane::Reparameterised
1075 } else {
1076 OffsetSurfaceLane::Affine
1077 };
1078 return Ok((
1079 NurbsSurface::new(
1080 source.degree_u,
1081 source.degree_v,
1082 source.knots_u.clone(),
1083 source.knots_v.clone(),
1084 controls,
1085 ).or_refuse(KernelStage::Refine, "new")?,
1086 lane,
1087 None,
1088 ));
1089 }
1090
1091 if let Some(surface) = sphere_offset_surface(face, distance, sphere_reading)? {
1101 return Ok((surface, OffsetSurfaceLane::Analytic, None));
1102 }
1103 if let Some(surface) = revolved_arc_offset_surface(face, distance)? {
1106 return Ok((surface, OffsetSurfaceLane::Analytic, None));
1107 }
1108
1109 let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u).or_refuse(KernelStage::Refine, "new")?;
1110 let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v).or_refuse(KernelStage::Refine, "new")?;
1111 let parameters_u = greville_parameters(&knot_u);
1112 let parameters_v = greville_parameters(&knot_v);
1113 let evaluator = OffsetEvaluator::new(
1119 "offset_surface",
1120 source,
1121 OffsetNormal::FaceStable {
1122 same_sense: face.same_sense,
1123 },
1124 );
1125 let mut samples = Vec::new();
1126 for &u in ¶meters_u {
1127 let mut row = Vec::new();
1128 for &v in ¶meters_v {
1129 row.push(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point);
1130 }
1131 samples.push(row);
1132 }
1133 let mut reparameterised = false;
1148 let mut extended_source: Option<NurbsSurface> = None;
1149 if parameters_v.len() == 2 && parameters_u.len() >= 2 {
1156 let centroid = |column: usize| {
1157 let mut sum = Vec3::default();
1158 for row in &samples {
1159 sum = sum.add(row[column]);
1160 }
1161 sum.scale(1.0 / samples.len() as f64)
1162 };
1163 let near_centroid = centroid(0);
1164 let far_centroid = centroid(1);
1165 let mut inverted = true;
1166 let mut pinch_fraction = 0.0f64;
1167 let mut near_mean = 0.0f64;
1168 let mut far_mean = 0.0f64;
1169 for row in &samples {
1170 let near_radial = row[0].sub(near_centroid);
1171 let far_radial = row[1].sub(far_centroid);
1172 let near_len = near_radial.length();
1173 let far_len = far_radial.length();
1174 if near_len <= 1e-9 || far_len <= 1e-9 {
1175 inverted = false;
1176 break;
1177 }
1178 if near_radial.dot(far_radial) >= 0.0 {
1179 inverted = false;
1180 break;
1181 }
1182 pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
1183 near_mean += near_len / samples.len() as f64;
1184 far_mean += far_len / samples.len() as f64;
1185 }
1186 if inverted {
1187 let retrim_far = far_mean <= near_mean;
1188 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1212 let trim_reaches_the_pinch = if retrim_far {
1213 trim_v[1] > pinch_fraction + 1e-9
1214 } else {
1215 trim_v[0] < pinch_fraction - 1e-9
1216 };
1217 if trim_reaches_the_pinch {
1218 reparameterised = true;
1221 for row in &mut samples {
1222 let near = row[0];
1223 let far = row[1];
1224 let pinch = near.add(far.sub(near).scale(pinch_fraction));
1225 if retrim_far {
1226 row[1] = pinch;
1227 } else {
1228 row[0] = pinch;
1229 }
1230 }
1231 }
1232 }
1233 if extension.extends_v() && !inverted {
1253 let mut min_ruling = f64::MAX;
1254 let mut back_allowance = f64::MAX;
1255 let mut forward_allowance = f64::MAX;
1256 let mut extendable = true;
1257 for row in &samples {
1258 let ruling = row[1].sub(row[0]);
1259 let length = ruling.length();
1260 min_ruling = min_ruling.min(length);
1261 let near_radial = row[0].sub(near_centroid).length();
1265 let far_radial = row[1].sub(far_centroid).length();
1266 if (far_radial - near_radial).abs() > 1e-9 {
1267 let apex_at = near_radial / (near_radial - far_radial);
1268 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1269 extendable = false;
1271 break;
1272 }
1273 if apex_at < 0.0 {
1274 back_allowance = back_allowance.min(0.9 * -apex_at);
1275 } else {
1276 forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
1277 }
1278 }
1279 }
1280 let mut source_back = f64::MAX;
1288 let mut source_forward = f64::MAX;
1289 if extendable {
1290 let source_centroid = |column: usize| {
1291 let mut sum = Vec3::default();
1292 for row in &source.control_points {
1293 if let Ok(point) = row[column].point() {
1294 sum = sum.add(point);
1295 }
1296 }
1297 sum.scale(1.0 / source.control_points.len() as f64)
1298 };
1299 let (near_source, far_source) = (source_centroid(0), source_centroid(1));
1300 for row in &source.control_points {
1301 let (near, far) = (row[0].point().or_refuse(KernelStage::Refine, "point")?, row[1].point().or_refuse(KernelStage::Refine, "point")?);
1302 let near_radial = near.sub(near_source).length();
1303 let far_radial = far.sub(far_source).length();
1304 if (far_radial - near_radial).abs() > 1e-9 {
1305 let apex_at = near_radial / (near_radial - far_radial);
1306 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1307 extendable = false;
1308 break;
1309 }
1310 if apex_at < 0.0 {
1311 source_back = source_back.min(0.9 * -apex_at);
1312 } else {
1313 source_forward = source_forward.min(0.9 * (apex_at - 1.0));
1314 }
1315 }
1316 }
1317 }
1318 let (back_allowance, forward_allowance) =
1319 (back_allowance.min(source_back), forward_allowance.min(source_forward));
1320 if extendable && min_ruling > 1e-9 {
1321 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1328 let (back_world, forward_world) =
1329 domain_end_moves(extension.v_min, extension.v_max, trim_v);
1330 let (requested_back, requested_forward) = (back_world / min_ruling, forward_world / min_ruling);
1331 let apex_end = analytic_offset_apex(source, &samples, offset_fit_contract(source)).and_then(
1343 |(at, apex)| {
1344 if at > 1.0 && at - 1.0 <= requested_forward && at - 1.0 <= source_forward {
1345 Some((false, at - 1.0, apex))
1346 } else if at < 0.0 && -at <= requested_back && -at <= source_back {
1347 Some((true, -at, apex))
1348 } else {
1349 None
1350 }
1351 },
1352 );
1353 let back = match apex_end {
1354 Some((true, reach, _)) => reach,
1355 _ => requested_back.min(back_allowance),
1356 };
1357 let forward = match apex_end {
1358 Some((false, reach, _)) => reach,
1359 _ => requested_forward.min(forward_allowance),
1360 };
1361 let stretched = samples
1362 .iter()
1363 .map(|row| {
1364 let ruling = row[1].sub(row[0]);
1365 let (near, far) = (row[0].sub(ruling.scale(back)), row[1].add(ruling.scale(forward)));
1366 match apex_end {
1367 Some((true, _, apex)) => vec![apex, far],
1368 Some((false, _, apex)) => vec![near, apex],
1369 None => vec![near, far],
1370 }
1371 })
1372 .collect::<Vec<_>>();
1373 let extended = extend_along_rulings(source, back, forward)?;
1374 let extended_offsets = OffsetEvaluator::new(
1375 "offset_surface",
1376 &extended,
1377 OffsetNormal::FaceStable {
1378 same_sense: face.same_sense,
1379 },
1380 );
1381 let [v0, v1] = knot_v.domain();
1382 let mut developability = 0.0f64;
1383 for (row, &u) in stretched.iter().zip(¶meters_u) {
1384 for fraction in [0.0, 0.5, 1.0] {
1385 let exact = extended_offsets.at(u, v0 + (v1 - v0) * fraction, -distance).or_refuse(KernelStage::Refine, "at")?.point;
1386 let along = row[0].add(row[1].sub(row[0]).scale(fraction));
1387 developability = developability.max(exact.sub(along).length());
1388 }
1389 }
1390 let contract = offset_fit_contract(source);
1391 let stretch_kept = developability <= contract;
1392 samples = if stretch_kept {
1393 stretched
1394 } else {
1395 parameters_u
1396 .iter()
1397 .map(|&u| {
1398 parameters_v
1399 .iter()
1400 .map(|&v| Ok(extended_offsets.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1401 .collect::<Result<Vec<_>, KernelRefusal>>()
1402 })
1403 .collect::<Result<Vec<_>, KernelRefusal>>()?
1404 };
1405 if let Ok(path) = std::env::var("BREP_OFFSET_FIT_TRACE") {
1406 use std::io::Write;
1407 let line = format!(
1408 "extension deg=({},{}) net=({},{}) d={distance:.6} back={back:.6} forward={forward:.6} \
1409 developability={developability:.3e} contract={contract:.3e} lane={} thread={} args={}\n",
1410 source.degree_u,
1411 source.degree_v,
1412 source.control_points.len(),
1413 source.control_points[0].len(),
1414 if stretch_kept { "stretch" } else { "pointwise" },
1415 std::thread::current().name().unwrap_or("-"),
1416 std::env::args().skip(1).collect::<Vec<_>>().join(" "),
1417 );
1418 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1419 let _ = file.write_all(line.as_bytes());
1420 }
1421 }
1422 extended_source = Some(extended);
1423 }
1424 }
1425 }
1426 let reference = extended_source.as_ref().unwrap_or(source);
1427 if !reparameterised && exact_cylinder_offsets_on() {
1433 if let Some(surface) = cylinder_offset_surface(reference, face.same_sense, distance)? {
1434 return Ok((surface, OffsetSurfaceLane::Analytic, extended_source));
1435 }
1436 }
1437 let weights = reference
1438 .control_points
1439 .iter()
1440 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1441 .collect::<Vec<_>>();
1442 let fitted = NurbsSurface::new(
1443 source.degree_u,
1444 source.degree_v,
1445 source.knots_u.clone(),
1446 source.knots_v.clone(),
1447 interpolate_tensor(
1448 &knot_u,
1449 &knot_v,
1450 ¶meters_u,
1451 ¶meters_v,
1452 &samples,
1453 &weights,
1454 )?,
1455 ).or_refuse(KernelStage::Refine, "new")?;
1456 if reparameterised {
1457 return Ok((fitted, OffsetSurfaceLane::Reparameterised, None));
1458 }
1459 let reference_offsets = OffsetEvaluator::new(
1460 "offset_surface",
1461 reference,
1462 OffsetNormal::FaceStable {
1463 same_sense: face.same_sense,
1464 },
1465 );
1466 Ok((
1467 refine_offset_fit(reference, &reference_offsets, distance, fitted)?,
1468 OffsetSurfaceLane::Fit,
1469 extended_source,
1470 ))
1471}
1472
1473
1474fn analytic_offset_apex(source: &NurbsSurface, samples: &[Vec<Vec3>], contract: f64) -> Option<(f64, Vec3)> {
1483 let Some(crate::AnalyticSurface::RuledRevolution { frame, .. }) = crate::analytic_surface::recognize(source) else {
1484 return None;
1485 };
1486 let radial = |point: Vec3| {
1487 let offset = point.sub(frame.origin);
1488 offset.sub(frame.axis.scale(offset.dot(frame.axis)))
1489 };
1490 let mut crossings = Vec::with_capacity(samples.len());
1491 for row in samples {
1492 let (near, far) = (radial(row[0]), radial(row[1]));
1493 let step = far.sub(near);
1494 let at = -near.dot(step) / step.dot(step);
1495 if !at.is_finite() {
1496 return None;
1497 }
1498 crossings.push((at, row[0].add(row[1].sub(row[0]).scale(at))));
1499 }
1500 let count = crossings.len() as f64;
1503 let at = crossings.iter().map(|(at, _)| at).sum::<f64>() / count;
1504 let first = crossings.first()?.1;
1505 let spread = crossings.iter().fold(Vec3::default(), |sum, (_, point)| sum.add(point.sub(first)));
1506 let centre = first.add(spread.scale(1.0 / count));
1507 let apex = frame.origin.add(frame.axis.scale(centre.sub(frame.origin).dot(frame.axis)));
1508 crossings
1511 .iter()
1512 .all(|(_, point)| radial(*point).length() <= contract && point.sub(apex).length() <= contract)
1513 .then_some((at, apex))
1514}
1515
1516fn extend_along_rulings(surface: &NurbsSurface, back: f64, forward: f64) -> Result<NurbsSurface, KernelRefusal> {
1523 let rows = surface
1524 .control_points
1525 .iter()
1526 .map(|row| {
1527 let step = row[1].add(row[0].scale(-1.0));
1528 vec![row[0].add(step.scale(-back)), row[1].add(step.scale(forward))]
1529 })
1530 .collect::<Vec<_>>();
1531 if rows.iter().flatten().any(|point| point.w <= 1e-12) {
1532 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_ruled_extension_weight", format!(
1533 "offset_surface: extending a ruled face by ({back:.6}, {forward:.6}) of its rulings \
1534 carries a weight through zero; the extended face is not this surface"
1535 )));
1536 }
1537 NurbsSurface::new(surface.degree_u, 1, surface.knots_u.clone(), surface.knots_v.clone(), rows)
1538 .or_refuse(KernelStage::Refine, "new")
1539}
1540
1541fn offset_fit_contract(source: &NurbsSurface) -> f64 {
1544 let scale = crate::model_scale(
1545 source
1546 .control_points
1547 .iter()
1548 .flatten()
1549 .filter_map(|point| point.point().ok()),
1550 );
1551 crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit
1552}
1553
1554const OFFSET_FIT_REFINEMENTS: usize = 6;
1556
1557const OFFSET_FIT_MAX_CONTROLS_SEPARABLE: usize = 16_384;
1560const OFFSET_FIT_MAX_CONTROLS_TENSOR: usize = 900;
1561
1562#[derive(Clone, Debug)]
1567struct OffsetFitDeviation {
1568 along_u: f64,
1569 along_v: f64,
1570 between: f64,
1571 u_sites: Vec<(f64, f64)>,
1572 v_sites: Vec<(f64, f64)>,
1573 between_uv: (f64, f64),
1574}
1575
1576impl OffsetFitDeviation {
1577 fn worst(&self) -> f64 {
1578 self.along_u.max(self.along_v).max(self.between)
1579 }
1580
1581 fn refinement_sites(&self, bar: f64) -> (Vec<f64>, Vec<f64>) {
1582 let mut u = self.u_sites.iter().filter(|(_, miss)| *miss > bar)
1583 .map(|(parameter, _)| *parameter).collect::<Vec<_>>();
1584 let mut v = self.v_sites.iter().filter(|(_, miss)| *miss > bar)
1585 .map(|(parameter, _)| *parameter).collect::<Vec<_>>();
1586 if u.is_empty() && v.is_empty() && self.between > bar {
1587 u.push(self.between_uv.0);
1588 v.push(self.between_uv.1);
1589 }
1590 (u, v)
1591 }
1592}
1593
1594fn greville_midpoints(parameters: &[f64]) -> Vec<f64> {
1595 parameters
1596 .windows(2)
1597 .filter(|pair| pair[1] - pair[0] > 1e-12)
1598 .map(|pair| 0.5 * (pair[0] + pair[1]))
1599 .collect()
1600}
1601
1602fn offset_fit_deviation(
1603 fitted: &NurbsSurface,
1604 evaluator: &OffsetEvaluator,
1605 distance: f64,
1606) -> Result<OffsetFitDeviation, KernelRefusal> {
1607 let parameters_u = greville_parameters(&KnotVector::new(fitted.knots_u.clone(), fitted.degree_u).or_refuse(KernelStage::Refine, "new")?);
1608 let parameters_v = greville_parameters(&KnotVector::new(fitted.knots_v.clone(), fitted.degree_v).or_refuse(KernelStage::Refine, "new")?);
1609 let (middle_u, middle_v) = (greville_midpoints(¶meters_u), greville_midpoints(¶meters_v));
1610 let residual_locus = std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0");
1611 let gap = |u: f64, v: f64| -> Result<f64, KernelRefusal> {
1612 Ok(fitted.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?.sub(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point).length())
1613 };
1614 let mut deviation = OffsetFitDeviation {
1615 along_u: 0.0,
1616 along_v: 0.0,
1617 between: 0.0,
1618 u_sites: Vec::new(),
1619 v_sites: if residual_locus { middle_v.iter().map(|&v| (v, 0.0)).collect() } else { Vec::new() },
1620 between_uv: (0.0, 0.0),
1621 };
1622 for &u in &middle_u {
1623 let mut along = 0.0f64;
1624 for &v in ¶meters_v {
1625 let miss = gap(u, v)?;
1626 along = along.max(miss);
1627 deviation.along_u = deviation.along_u.max(miss);
1628 }
1629 if residual_locus { deviation.u_sites.push((u, along)); }
1630 for &v in &middle_v {
1631 let miss = gap(u, v)?;
1632 if miss > deviation.between { deviation.between_uv = (u, v); }
1633 deviation.between = deviation.between.max(miss);
1634 }
1635 }
1636 for &u in ¶meters_u {
1637 for (index, &v) in middle_v.iter().enumerate() {
1638 let miss = gap(u, v)?;
1639 deviation.along_v = deviation.along_v.max(miss);
1640 if residual_locus { deviation.v_sites[index].1 = deviation.v_sites[index].1.max(miss); }
1641 }
1642 }
1643 Ok(deviation)
1644}
1645
1646fn raise_linear_directions(
1656 surface: &NurbsSurface,
1657 in_u: bool,
1658 in_v: bool,
1659) -> Result<Option<NurbsSurface>, KernelRefusal> {
1660 fn raise(knots: &[f64], row: &[Vec4]) -> Option<(Vec<f64>, Vec<Vec4>)> {
1661 let count = row.len();
1663 if knots.len() != count + 2 {
1664 return None;
1665 }
1666 let breaks = &knots[1..=count];
1667 if breaks.windows(2).any(|pair| pair[1] - pair[0] <= 1e-12) {
1668 return None;
1669 }
1670 let mut raised_knots = vec![breaks[0]; 4];
1671 for &interior in &breaks[1..count - 1] {
1672 raised_knots.extend([interior; 3]);
1673 }
1674 raised_knots.extend([breaks[count - 1]; 4]);
1675 let mut points = Vec::with_capacity(3 * (count - 1) + 1);
1676 for pair in row.windows(2) {
1677 let step = pair[1].add(pair[0].scale(-1.0));
1678 points.push(pair[0]);
1679 points.push(pair[0].add(step.scale(1.0 / 3.0)));
1680 points.push(pair[0].add(step.scale(2.0 / 3.0)));
1681 }
1682 points.push(row[count - 1]);
1683 Some((raised_knots, points))
1684 }
1685 let mut knots_u = surface.knots_u.clone();
1686 let mut knots_v = surface.knots_v.clone();
1687 let mut degree_u = surface.degree_u;
1688 let mut degree_v = surface.degree_v;
1689 let mut net = surface.control_points.clone();
1690 if in_v {
1691 if degree_v != 1 {
1692 return Ok(None);
1693 }
1694 let mut rows = Vec::with_capacity(net.len());
1695 for row in &net {
1696 let Some((knots, points)) = raise(&surface.knots_v, row) else {
1697 return Ok(None);
1698 };
1699 knots_v = knots;
1700 rows.push(points);
1701 }
1702 net = rows;
1703 degree_v = 3;
1704 }
1705 if in_u {
1706 if degree_u != 1 {
1707 return Ok(None);
1708 }
1709 let count_v = net[0].len();
1710 let mut columns = Vec::with_capacity(count_v);
1711 for column in 0..count_v {
1712 let row = net.iter().map(|row| row[column]).collect::<Vec<_>>();
1713 let Some((knots, points)) = raise(&surface.knots_u, &row) else {
1714 return Ok(None);
1715 };
1716 knots_u = knots;
1717 columns.push(points);
1718 }
1719 let count_u = columns[0].len();
1720 net = (0..count_u)
1721 .map(|row| columns.iter().map(|column| column[row]).collect())
1722 .collect();
1723 degree_u = 3;
1724 }
1725 Ok(Some(NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")?))
1726}
1727
1728fn halve_knot_spans(surface: &NurbsSurface, in_u: bool, in_v: bool) -> Result<NurbsSurface, KernelRefusal> {
1732 fn midpoints(knots: &[f64], degree: usize) -> Vec<f64> {
1733 let last = knots.len() - 1 - degree;
1734 knots[degree..=last]
1735 .windows(2)
1736 .filter(|pair| pair[1] - pair[0] > 1e-12)
1737 .map(|pair| 0.5 * (pair[0] + pair[1]))
1738 .collect()
1739 }
1740 insert_offset_fit_knots(surface,
1741 &if in_u { midpoints(&surface.knots_u, surface.degree_u) } else { Vec::new() },
1742 &if in_v { midpoints(&surface.knots_v, surface.degree_v) } else { Vec::new() })
1743}
1744
1745fn insert_offset_fit_knots(surface: &NurbsSurface, u: &[f64], v: &[f64]) -> Result<NurbsSurface, KernelRefusal> {
1750 let refine = |degree: usize, knots: &[f64], net: Vec<Vec<Vec4>>, inserted: &[f64]| -> Result<(Vec<f64>, Vec<Vec<Vec4>>), KernelRefusal> {
1751 let mut inserted = inserted.to_vec();
1752 inserted.sort_by(f64::total_cmp);
1753 inserted.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
1754 inserted.retain(|parameter| knots.iter().filter(|knot| (**knot - *parameter).abs() <= 1e-12).count() < degree);
1755 let mut refined_knots = knots.to_vec();
1756 let mut rows = Vec::with_capacity(net.len());
1757 for row in net {
1758 let mut curve = NurbsCurve::new(degree, knots.to_vec(), row).or_refuse(KernelStage::Refine, "new")?;
1759 for ¶meter in &inserted {
1760 curve = curve.insert_knot(parameter, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
1761 }
1762 refined_knots = curve.knots.clone();
1763 rows.push(curve.control_points);
1764 }
1765 Ok((refined_knots, rows))
1766 };
1767 let mut knots_u = surface.knots_u.clone();
1768 let mut knots_v = surface.knots_v.clone();
1769 let mut net = surface.control_points.clone();
1770 if !v.is_empty() {
1771 let (knots, rows) = refine(surface.degree_v, &knots_v, net, v)?;
1772 knots_v = knots;
1773 net = rows;
1774 }
1775 if !u.is_empty() {
1776 let count_v = net[0].len();
1778 let columns = (0..count_v)
1779 .map(|column| net.iter().map(|row| row[column]).collect::<Vec<_>>())
1780 .collect::<Vec<_>>();
1781 let (knots, refined) = refine(surface.degree_u, &knots_u, columns, u)?;
1782 knots_u = knots;
1783 let count_u = refined[0].len();
1784 net = (0..count_u)
1785 .map(|row| refined.iter().map(|column| column[row]).collect())
1786 .collect();
1787 }
1788 NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
1789}
1790
1791fn refine_offset_fit(
1827 source: &NurbsSurface,
1828 evaluator: &OffsetEvaluator,
1829 distance: f64,
1830 fitted: NurbsSurface,
1831) -> Result<NurbsSurface, KernelRefusal> {
1832 let scale = crate::model_scale(
1833 source
1834 .control_points
1835 .iter()
1836 .flatten()
1837 .filter_map(|point| point.point().ok()),
1838 );
1839 let bar = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
1840 let trace = std::env::var("BREP_OFFSET_FIT_TRACE").ok();
1841 let started = web_time::Instant::now();
1842 let enabled = std::env::var("BREP_OFFSET_FIT_REFINE").as_deref() != Ok("0");
1843 let Ok(initial) = offset_fit_deviation(&fitted, evaluator, distance) else {
1844 return Ok(fitted);
1845 };
1846 let mut best = fitted;
1847 let mut best_deviation = initial.clone();
1848 let mut refined_source = source.clone();
1849 let mut rounds = 0usize;
1850 let mut residual_mode = false;
1851 let mut stop = "under bar";
1852 if enabled {
1853 while best_deviation.worst() > bar {
1854 if rounds == OFFSET_FIT_REFINEMENTS {
1855 stop = "refinement limit";
1856 break;
1857 }
1858 let (along_u, along_v) = (best_deviation.along_u > bar, best_deviation.along_v > bar);
1859 let (in_u, in_v) = if along_u || along_v {
1860 (along_u, along_v)
1861 } else {
1862 (true, true)
1863 };
1864 let (raise_u, raise_v) = (in_u && refined_source.degree_u == 1, in_v && refined_source.degree_v == 1);
1865 let raised = if raise_u || raise_v {
1866 raise_linear_directions(&refined_source, raise_u, raise_v)?
1867 } else {
1868 None
1869 };
1870 let (halve_u, halve_v) = match raised {
1871 Some(_) => (in_u && !raise_u, in_v && !raise_v),
1872 None => (in_u, in_v),
1873 };
1874 let raised = raised.unwrap_or_else(|| refined_source.clone());
1875 let candidate_source = if halve_u || halve_v {
1876 if residual_mode {
1877 let (u, v) = best_deviation.refinement_sites(bar);
1878 insert_offset_fit_knots(&raised,
1879 if halve_u { &u } else { &[] }, if halve_v { &v } else { &[] })?
1880 } else {
1881 halve_knot_spans(&raised, halve_u, halve_v)?
1882 }
1883 } else {
1884 raised
1885 };
1886 let weights = candidate_source
1887 .control_points
1888 .iter()
1889 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1890 .collect::<Vec<_>>();
1891 let controls = weights.len() * weights[0].len();
1892 let ceiling = if separable_weights(&weights).is_some() {
1893 OFFSET_FIT_MAX_CONTROLS_SEPARABLE
1894 } else {
1895 OFFSET_FIT_MAX_CONTROLS_TENSOR
1896 };
1897 if controls > ceiling {
1898 stop = "net ceiling";
1899 break;
1900 }
1901 let knot_u = KnotVector::new(candidate_source.knots_u.clone(), candidate_source.degree_u).or_refuse(KernelStage::Refine, "new")?;
1902 let knot_v = KnotVector::new(candidate_source.knots_v.clone(), candidate_source.degree_v).or_refuse(KernelStage::Refine, "new")?;
1903 let parameters_u = greville_parameters(&knot_u);
1904 let parameters_v = greville_parameters(&knot_v);
1905 let samples = parameters_u
1906 .iter()
1907 .map(|&u| {
1908 parameters_v
1909 .iter()
1910 .map(|&v| Ok(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1911 .collect::<Result<Vec<_>, KernelRefusal>>()
1912 })
1913 .collect::<Result<Vec<_>, KernelRefusal>>();
1914 let Ok(samples) = samples else {
1915 stop = "unevaluable sample";
1916 break;
1917 };
1918 let candidate = NurbsSurface::new(
1919 candidate_source.degree_u,
1920 candidate_source.degree_v,
1921 candidate_source.knots_u.clone(),
1922 candidate_source.knots_v.clone(),
1923 interpolate_tensor(&knot_u, &knot_v, ¶meters_u, ¶meters_v, &samples, &weights)?,
1924 ).or_refuse(KernelStage::Refine, "new")?;
1925 let Ok(deviation) = offset_fit_deviation(&candidate, evaluator, distance) else {
1926 stop = "unmeasurable round";
1927 break;
1928 };
1929 rounds += 1;
1930 if deviation.worst() >= best_deviation.worst() {
1931 let first_quadratic_split = rounds == 1
1938 && ((halve_u && source.degree_u == 2 && source.knots_u.len() == 6)
1939 || (halve_v && source.degree_v == 2 && source.knots_v.len() == 6));
1940 if first_quadratic_split {
1941 refined_source = candidate_source;
1942 continue;
1943 }
1944 if !residual_mode && best_deviation.worst() > offset_construction_band(scale)
1949 && std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0")
1950 {
1951 residual_mode = true;
1952 continue;
1953 }
1954 stop = "stopped improving";
1955 break;
1956 }
1957 best = candidate;
1958 best_deviation = deviation;
1959 refined_source = candidate_source;
1960 }
1961 } else if best_deviation.worst() > bar {
1962 stop = "refinement disabled";
1963 }
1964 let band = offset_construction_band(scale);
1969 let refused = enabled && best_deviation.worst() > band;
1970 if let Some(path) = trace {
1971 use std::io::Write;
1972 let caller = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
1973 let thread = std::thread::current().name().unwrap_or("-").to_string();
1974 let line = format!(
1975 "fit deg=({},{}) net=({},{})->({},{}) d={distance:.6} scale={scale:.4} bar={bar:.3e} \
1976 initial=(u {:.3e}, v {:.3e}, uv {:.3e}) final={:.3e} rounds={rounds} stop={stop} \
1977 refused={refused} seconds={:.4} thread={thread} args={caller}\n",
1978 source.degree_u,
1979 source.degree_v,
1980 source.control_points.len(),
1981 source.control_points[0].len(),
1982 best.control_points.len(),
1983 best.control_points[0].len(),
1984 initial.along_u,
1985 initial.along_v,
1986 initial.between,
1987 best_deviation.worst(),
1988 started.elapsed().as_secs_f64(),
1989 );
1990 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1991 let _ = file.write_all(line.as_bytes());
1992 }
1993 }
1994 if refused {
1995 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "offset_carrier_fit", format!(
1996 "offset_surface: the fitted offset carrier stays {:.3e} off its pointwise offset at \
1997 distance {distance} after {rounds} refinement round(s) ({stop}), past the offset \
1998 construction band {band:.3e}",
1999 best_deviation.worst()
2000 )));
2001 }
2002 Ok(best)
2003}
2004
2005pub(crate) fn unify_offset_sheet_bases(
2015 first: &NurbsSurface,
2016 second: &NurbsSurface,
2017) -> Result<(NurbsSurface, NurbsSurface), KernelRefusal> {
2018 fn raise_to(surface: &NurbsSurface, degree_u: usize, degree_v: usize) -> Result<NurbsSurface, KernelRefusal> {
2019 let raise_u = surface.degree_u == 1 && degree_u == 3;
2020 let raise_v = surface.degree_v == 1 && degree_v == 3;
2021 if !raise_u && !raise_v {
2022 return Ok(surface.clone());
2023 }
2024 raise_linear_directions(surface, raise_u, raise_v)?
2025 .ok_or_else(|| {
2026 KernelRefusal::internal(
2027 KernelStage::Refine,
2028 "offset_raise_linear",
2029 "offset sheets: a linear direction could not be raised",
2030 )
2031 })
2032 }
2033 let degree_u = first.degree_u.max(second.degree_u);
2034 let degree_v = first.degree_v.max(second.degree_v);
2035 let mut first = raise_to(first, degree_u, degree_v)?;
2036 let mut second = raise_to(second, degree_u, degree_v)?;
2037 if first.degree_u != second.degree_u || first.degree_v != second.degree_v {
2038 return Err(KernelRefusal::internal(KernelStage::Refine, "offset_common_basis", format!(
2039 "offset sheets: degrees ({},{}) and ({},{}) have no common basis",
2040 first.degree_u, first.degree_v, second.degree_u, second.degree_v
2041 )));
2042 }
2043 fn missing(from: &[f64], into: &[f64]) -> Vec<f64> {
2045 let mut extra = Vec::new();
2046 let mut index = 0usize;
2047 for &knot in from {
2048 while index < into.len() && into[index] < knot - 1e-12 {
2049 index += 1;
2050 }
2051 if index < into.len() && (into[index] - knot).abs() <= 1e-12 {
2052 index += 1;
2053 } else {
2054 extra.push(knot);
2055 }
2056 }
2057 extra
2058 }
2059 fn insert(surface: &NurbsSurface, knots: &[f64], along_u: bool) -> Result<NurbsSurface, KernelRefusal> {
2060 if knots.is_empty() {
2061 return Ok(surface.clone());
2062 }
2063 let (degree, vector) = if along_u {
2064 (surface.degree_u, &surface.knots_u)
2065 } else {
2066 (surface.degree_v, &surface.knots_v)
2067 };
2068 let lines = if along_u {
2069 (0..surface.control_points[0].len())
2070 .map(|column| surface.control_points.iter().map(|row| row[column]).collect::<Vec<_>>())
2071 .collect::<Vec<_>>()
2072 } else {
2073 surface.control_points.clone()
2074 };
2075 let mut refined_knots = vector.clone();
2076 let mut refined = Vec::with_capacity(lines.len());
2077 for line in lines {
2078 let mut curve = NurbsCurve::new(degree, vector.clone(), line).or_refuse(KernelStage::Refine, "new")?;
2079 for &knot in knots {
2080 curve = curve.insert_knot(knot, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
2081 }
2082 refined_knots = curve.knots.clone();
2083 refined.push(curve.control_points);
2084 }
2085 let net = if along_u {
2086 (0..refined[0].len())
2087 .map(|row| refined.iter().map(|column| column[row]).collect())
2088 .collect()
2089 } else {
2090 refined
2091 };
2092 let (knots_u, knots_v) = if along_u {
2093 (refined_knots, surface.knots_v.clone())
2094 } else {
2095 (surface.knots_u.clone(), refined_knots)
2096 };
2097 NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
2098 }
2099 let (to_first_u, to_second_u) = (
2100 missing(&second.knots_u, &first.knots_u),
2101 missing(&first.knots_u, &second.knots_u),
2102 );
2103 first = insert(&first, &to_first_u, true)?;
2104 second = insert(&second, &to_second_u, true)?;
2105 let (to_first_v, to_second_v) = (
2106 missing(&second.knots_v, &first.knots_v),
2107 missing(&first.knots_v, &second.knots_v),
2108 );
2109 first = insert(&first, &to_first_v, false)?;
2110 second = insert(&second, &to_second_v, false)?;
2111 Ok((first, second))
2112}
2113
2114fn carrier_edge_curve(
2136 surface: &NurbsSurface,
2137 pcurve: &NurbsCurve,
2138 points: &[Vec3],
2139 parameters: &[f64],
2140 fit_tolerance: f64,
2141) -> Result<(NurbsCurve, f64, f64), KernelRefusal> {
2142 if affine_image_trusted() && surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
2150 let [q0, q1] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2151 if (q1 - q0).is_finite() && (q1 - q0).abs() > 0.0 {
2152 let curve = crate::image_curve::affine_image_curve(surface, pcurve).or_refuse(KernelStage::Refine, "affine_image_curve")?;
2153 return Ok((curve, q0, q1));
2154 }
2155 }
2156 let image = if env_read!("BREP_CARRIER_POLYLINE").as_deref() == Some("1") {
2157 Err("BREP_CARRIER_POLYLINE set".to_string())
2158 } else {
2159 crate::image_curve::image_curve(surface, pcurve, fit_tolerance, "offset_face_carrier")
2160 };
2161 match image {
2162 Ok(image) if image.t0 <= image.t1 => Ok((image.curve, image.t0, image.t1)),
2163 Ok(image) => {
2164 let [start, end] = image.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2167 let curve = image.curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
2168 Ok((curve, start + end - image.t0, start + end - image.t1))
2169 }
2170 Err(_) => {
2171 let curve = interpolate_curve(points, 1, parameters).or_refuse(KernelStage::Refine, "interpolate_curve")?;
2172 let [t0, t1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2173 Ok((curve, t0, t1))
2174 }
2175 }
2176}
2177
2178fn affine_image_trusted() -> bool {
2179 switch!("BREP_CARRIER_AFFINE_IMAGE", true)
2180}
2181
2182
2183fn lazy_carrier_trace(
2191 surface: &NurbsSurface,
2192 pcurve: &NurbsCurve,
2193 collapse_tolerance: f64,
2194 fit_tolerance: f64,
2195) -> Option<(Vec<Vec3>, (NurbsCurve, f64, f64))> {
2196 if !switch!("BREP_OFFSET_LAZY_CARRIER_TRACE", true) {
2197 return None;
2198 }
2199 if pcurve.degree == 1 && pcurve.control_points.len() == 2 {
2202 let (a, b) = (pcurve.control_points[0], pcurve.control_points[1]);
2203 if (surface.degree_u <= 1 && surface.degree_v <= 1)
2204 || (a.w == b.w && a.y == b.y && surface.degree_u <= 1)
2205 || (a.w == b.w && a.x == b.x && surface.degree_v <= 1)
2206 {
2207 return None;
2208 }
2209 }
2210 fn knots_ok(knots: &[f64], degree: usize, count: usize) -> bool {
2211 degree <= 3 && count <= 64
2212 && KnotVector::new(knots.to_vec(), degree).ok().is_some_and(|k| k.control_point_count() == count)
2213 && knots.iter().all(|k| k.is_finite() && k.abs() <= 1e6)
2214 && knots.windows(2).all(|k| k[0] == k[1] || k[1] - k[0] >= 1e-6)
2215 }
2216 let bounded = |p: &Vec4| (1e-3..=1e3).contains(&p.w)
2217 && [p.x, p.y, p.z].iter().all(|x| x.is_finite() && x.abs() <= 1e12);
2218 let columns = surface.control_points.first()?.len();
2219 if !knots_ok(&surface.knots_u, surface.degree_u, surface.control_points.len())
2220 || !knots_ok(&surface.knots_v, surface.degree_v, columns)
2221 || surface.control_points.iter().any(|row| row.len() != columns || row.iter().any(|p| !bounded(p)))
2222 || !knots_ok(&pcurve.knots, pcurve.degree, pcurve.control_points.len())
2223 || pcurve.control_points.iter().any(|p| !bounded(p))
2224 {
2225 return None;
2226 }
2227 let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
2228 let uv = pcurve.control_points.iter().map(|p| p.point().ok()).collect::<Option<Vec<_>>>()?;
2229 if uv.iter().any(|p| p.x < u0 || p.x > u1 || p.y < v0 || p.y > v1) {
2230 return None;
2231 }
2232 if !surface.is_affine().ok()? && !uv.iter().all(|p| p.x == uv[0].x) && !uv.iter().all(|p| p.y == uv[0].y) {
2234 return None;
2235 }
2236 let [start, end] = pcurve.domain().ok()?;
2237 let evaluate = |fraction: f64| {
2238 let uv = pcurve.evaluate(start + (end - start) * fraction).ok()?;
2239 surface.evaluate(uv.x, uv.y).ok()
2240 };
2241 let first = evaluate(0.0)?;
2242 let last = evaluate(1.0)?;
2243 let samples = [evaluate(0.25)?, evaluate(0.5)?, evaluate(0.75)?];
2244 let deviation = samples.iter().enumerate().map(|(index, point)| {
2245 point.sub(first.add(last.sub(first).scale((index + 1) as f64 * 0.25))).length()
2246 }).fold(0.0, f64::max);
2247 if last.sub(first).length() <= collapse_tolerance
2248 && !(deviation > 5e-4 && samples[1].sub(first).length() > collapse_tolerance)
2249 {
2250 return None;
2251 }
2252 let image = carrier_edge_curve(surface, pcurve, &[], &[], fit_tolerance).ok()?;
2255 Some((vec![first, samples[1], last], image))
2256}
2257
2258fn mapped_pcurve_polyline(
2259 surface: &NurbsSurface,
2260 pcurve: &NurbsCurve,
2261 degenerate: bool,
2262) -> Result<(Vec<Vec3>, Vec<f64>), KernelRefusal> {
2263 let [start, end] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2264 let evaluate = |fraction: f64| -> Result<Vec3, KernelRefusal> {
2265 let uv = pcurve.evaluate(start + (end - start) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
2266 surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")
2267 };
2268 let first = evaluate(0.0)?;
2269 let last = evaluate(1.0)?;
2270 if degenerate {
2271 return Ok((vec![first, last], vec![0.0, 1.0]));
2272 }
2273 fn append(
2274 evaluate: &impl Fn(f64) -> Result<Vec3, KernelRefusal>,
2275 a_fraction: f64,
2276 a: Vec3,
2277 b_fraction: f64,
2278 b: Vec3,
2279 depth: usize,
2280 parameters: &mut Vec<f64>,
2281 points: &mut Vec<Vec3>,
2282 ) -> Result<(), KernelRefusal> {
2283 let fractions =
2284 [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
2285 let samples = fractions
2286 .map(evaluate)
2287 .into_iter()
2288 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2289 let deviation = samples
2290 .iter()
2291 .enumerate()
2292 .map(|(index, point)| {
2293 point
2294 .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
2295 .length()
2296 })
2297 .fold(0.0, f64::max);
2298 if deviation <= 5e-4 || depth >= 10 {
2299 parameters.push(b_fraction);
2300 points.push(b);
2301 return Ok(());
2302 }
2303 append(
2304 evaluate,
2305 a_fraction,
2306 a,
2307 fractions[1],
2308 samples[1],
2309 depth + 1,
2310 parameters,
2311 points,
2312 )?;
2313 append(
2314 evaluate,
2315 fractions[1],
2316 samples[1],
2317 b_fraction,
2318 b,
2319 depth + 1,
2320 parameters,
2321 points,
2322 )
2323 }
2324 let mut parameters = vec![0.0];
2325 let mut points = vec![first];
2326 append(
2327 &evaluate,
2328 0.0,
2329 first,
2330 1.0,
2331 last,
2332 0,
2333 &mut parameters,
2334 &mut points,
2335 )?;
2336 Ok((points, parameters))
2337}
2338
2339#[derive(Clone, Debug)]
2353pub struct CarrierDeviation {
2354 pub band: f64,
2357 pub lane: OffsetSurfaceLane,
2359 pub surface: Option<MeasuredTolerance>,
2361 pub edges: Vec<(u64, MeasuredTolerance)>,
2366 pub vertices: Vec<(u64, f64)>,
2370}
2371
2372impl CarrierDeviation {
2373 pub fn worst(&self) -> Option<MeasuredTolerance> {
2376 MeasuredTolerance::worst(
2377 self.surface
2378 .into_iter()
2379 .chain(self.edges.iter().map(|(_, measured)| *measured))
2380 .chain(
2381 self.vertices
2382 .iter()
2383 .map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
2384 ),
2385 )
2386 }
2387
2388 pub fn exceedances(&self) -> Vec<String> {
2391 let mut out = Vec::new();
2392 if let Some(surface) = self.surface {
2393 if surface.exceeds_band() {
2394 out.push(format!("carrier surface fit {}", surface.describe()));
2395 }
2396 }
2397 for (id, measured) in &self.edges {
2398 if measured.exceeds_band() {
2399 out.push(format!("edge {id} {}", measured.describe()));
2400 }
2401 }
2402 for (id, gap) in &self.vertices {
2403 let measured = MeasuredTolerance::new(*gap, self.band);
2404 if measured.exceeds_band() {
2405 out.push(format!("vertex {id} {}", measured.describe()));
2406 }
2407 }
2408 out
2409 }
2410}
2411
2412#[derive(Clone, Debug, Serialize)]
2413pub struct OffsetFaceCarrier {
2414 pub vertices: Vec<VertexRecord>,
2415 pub edges: Vec<EdgeRecord>,
2416 pub face: FaceRecord,
2417 #[serde(skip)]
2425 pub deviation: Option<CarrierDeviation>,
2426 #[serde(skip)]
2430 pub lane: OffsetSurfaceLane,
2431}
2432
2433fn claim_vertex_image(
2444 source_id: u64,
2445 point: Vec3,
2446 split: f64,
2447 vertex_images: &mut HashMap<u64, Vec<u64>>,
2448 vertices: &mut Vec<VertexRecord>,
2449 next_id: &mut u64,
2450) -> u64 {
2451 if let Some(ids) = vertex_images.get(&source_id) {
2452 let near = ids.iter().copied().find(|id| {
2453 vertices
2454 .iter()
2455 .find(|vertex| vertex.id == *id)
2456 .is_some_and(|vertex| vertex.point.sub(point).length() <= split)
2457 });
2458 if let Some(id) = near {
2459 return id;
2460 }
2461 }
2462 let id = *next_id;
2463 *next_id += 1;
2464 vertices.push(VertexRecord { id, point });
2465 vertex_images.entry(source_id).or_default().push(id);
2466 id
2467}
2468
2469pub fn offset_face_carrier(
2470 solid: &BrepSolid,
2471 face_id: u64,
2472 distance: f64,
2473 planar_extension: f64,
2474) -> Result<OffsetFaceCarrier, KernelRefusal> {
2475 offset_face_carrier_impl(
2476 solid,
2477 face_id,
2478 distance,
2479 &CarrierExtension::uniform(planar_extension),
2480 false,
2481 )
2482}
2483
2484pub fn offset_face_carrier_sided(
2486 solid: &BrepSolid,
2487 face_id: u64,
2488 distance: f64,
2489 extension: &CarrierExtension,
2490) -> Result<OffsetFaceCarrier, KernelRefusal> {
2491 offset_face_carrier_impl(solid, face_id, distance, extension, false)
2492}
2493
2494pub fn offset_face_carrier_measured(
2514 solid: &BrepSolid,
2515 face_id: u64,
2516 distance: f64,
2517 planar_extension: f64,
2518) -> Result<OffsetFaceCarrier, KernelRefusal> {
2519 offset_face_carrier_impl(
2520 solid,
2521 face_id,
2522 distance,
2523 &CarrierExtension::uniform(planar_extension),
2524 true,
2525 )
2526}
2527
2528fn measure_carrier(
2539 surface: &NurbsSurface,
2540 vertices: &[VertexRecord],
2541 edges: &[EdgeRecord],
2542 loops: &[LoopRecord],
2543 lane: OffsetSurfaceLane,
2544 surface_fit: Option<MeasuredTolerance>,
2545 band: f64,
2546) -> Result<CarrierDeviation, KernelRefusal> {
2547 let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
2548 let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
2552 for loop_record in loops {
2553 for coedge in &loop_record.coedges {
2554 let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
2555 continue;
2556 };
2557 let measured = measure_edge_against_pcurve_image(
2558 surface,
2559 &coedge.pcurve,
2560 edge,
2561 coedge.forward,
2562 band,
2563 )?;
2564 per_edge
2565 .entry(edge.id)
2566 .and_modify(|existing| *existing = existing.worse_of(measured))
2567 .or_insert(measured);
2568 }
2569 }
2570
2571 let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
2572 measured_edges.sort_by_key(|(id, _)| *id);
2573 let deviation_of: HashMap<u64, f64> = measured_edges
2574 .iter()
2575 .map(|(id, measured)| (*id, measured.deviation()))
2576 .collect();
2577
2578 let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
2582 for edge in edges {
2583 ends_at
2584 .entry(edge.start_vertex_id)
2585 .or_default()
2586 .push((edge, edge.t0));
2587 ends_at
2588 .entry(edge.end_vertex_id)
2589 .or_default()
2590 .push((edge, edge.t1));
2591 }
2592
2593 let mut measured_vertices = Vec::with_capacity(vertices.len());
2594 for vertex in vertices {
2595 let mut gaps = Vec::new();
2596 let mut incident = Vec::new();
2597 for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
2598 gaps.push(vertex_endpoint_gap(
2599 vertex.point,
2600 edge.curve.evaluate(*parameter).or_refuse(KernelStage::Refine, "evaluate")?,
2601 ));
2602 incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
2603 }
2604 measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
2605 }
2606
2607 Ok(CarrierDeviation {
2608 band,
2609 lane,
2610 surface: surface_fit,
2611 edges: measured_edges,
2612 vertices: measured_vertices,
2613 })
2614}
2615
2616fn offset_face_carrier_impl(
2617 solid: &BrepSolid,
2618 face_id: u64,
2619 distance: f64,
2620 extension: &CarrierExtension,
2621 measure: bool,
2622) -> Result<OffsetFaceCarrier, KernelRefusal> {
2623 let source = solid
2624 .shells
2625 .iter()
2626 .flat_map(|shell| &shell.faces)
2627 .find(|face| face.id == face_id)
2628 .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}")).or_refuse(KernelStage::Refine, "offset_missing_face")?;
2629 let band = offset_construction_band(solid_model_scale(solid));
2637 let fit_tolerance =
2644 crate::KernelTolerances::for_scale(solid_model_scale(solid), 1e-7).intersection_fit;
2645 let (surface, lane, surface_fit) = if measure {
2646 let measured = offset_surface_measured_sided(source, distance, extension, band)?;
2647 (measured.surface, measured.lane, measured.fit)
2648 } else {
2649 let (surface, lane) = offset_surface_with_lane(source, distance, extension)?;
2650 (surface, lane, None)
2651 };
2652 let source_edges = solid
2653 .edges
2654 .iter()
2655 .map(|edge| (edge.id, edge))
2656 .collect::<HashMap<_, _>>();
2657 let source_vertices = solid
2658 .vertices
2659 .iter()
2660 .map(|vertex| (vertex.id, vertex))
2661 .collect::<HashMap<_, _>>();
2662 let mut vertices = Vec::new();
2663 let mut vertex_images = HashMap::default();
2664 let vertex_split = (distance.abs() * 0.1).max(band);
2668 let mut edges = Vec::new();
2669 let mut edge_images = HashMap::default();
2670 let mut loops = Vec::new();
2671 let mut next_id = 1u64;
2672
2673 for source_loop in &source.loops {
2674 let mut coedges = Vec::new();
2675 for source_coedge in &source_loop.coedges {
2676 let source_edge = source_edges
2677 .get(&source_coedge.edge_id)
2678 .ok_or_else(|| "offset_face_carrier: missing source edge".to_string()).or_refuse(KernelStage::Refine, "offset_missing_edge")?;
2679 let (source_start, source_end) = if source_coedge.forward {
2680 (source_edge.start_vertex_id, source_edge.end_vertex_id)
2681 } else {
2682 (source_edge.end_vertex_id, source_edge.start_vertex_id)
2683 };
2684 if !source_vertices.contains_key(&source_start)
2685 || !source_vertices.contains_key(&source_end)
2686 {
2687 return Err(KernelRefusal::internal(KernelStage::Refine, "offset_missing_vertex", "offset_face_carrier: missing source vertex"));
2688 }
2689 let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
2707 let (points, parameters, exact_image) = match lazy_carrier_trace(
2708 &surface, &source_coedge.pcurve, collapse_tolerance, fit_tolerance,
2709 ) {
2710 Some((points, image)) => (points, Vec::new(), Some(image)),
2711 None => {
2712 let (points, parameters) = mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
2713 (points, parameters, None)
2714 }
2715 };
2716 let image_collapsed = points
2717 .iter()
2718 .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
2719 let (edge_id, forward) =
2720 if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
2721 edge_images.get(&source_edge.id)
2722 {
2723 if source_start == source_end {
2724 (*edge_id, source_coedge.forward == *creator_forward)
2747 } else {
2748 (
2751 *edge_id,
2752 vertex_images
2753 .get(&source_start)
2754 .is_some_and(|ids: &Vec<u64>| ids.contains(edge_start_vertex_id)),
2755 )
2756 }
2757 } else {
2758 let (curve, t0, t1) = if image_collapsed {
2759 let curve = NurbsCurve::new(
2760 1,
2761 vec![0.0, 0.0, 1.0, 1.0],
2762 vec![
2763 Vec4::from_point(points[0], 1.0),
2764 Vec4::from_point(points[0], 1.0),
2765 ],
2766 ).or_refuse(KernelStage::Refine, "new")?;
2767 (curve, 0.0, 1.0)
2768 } else if let Some(image) = exact_image {
2769 image
2770 } else {
2771 carrier_edge_curve(
2772 &surface,
2773 &source_coedge.pcurve,
2774 &points,
2775 ¶meters,
2776 fit_tolerance,
2777 )?
2778 };
2779 let start_vertex_id = claim_vertex_image(
2780 source_start,
2781 points[0],
2782 vertex_split,
2783 &mut vertex_images,
2784 &mut vertices,
2785 &mut next_id,
2786 );
2787 let end_vertex_id = claim_vertex_image(
2788 source_end,
2789 points[points.len() - 1],
2790 vertex_split,
2791 &mut vertex_images,
2792 &mut vertices,
2793 &mut next_id,
2794 );
2795 let id = next_id;
2796 next_id += 1;
2797 edges.push(EdgeRecord {
2798 id,
2799 curve,
2800 t0,
2801 t1,
2802 start_vertex_id,
2803 end_vertex_id,
2804 degenerate: image_collapsed,
2818 name: source_edge
2822 .name
2823 .as_ref()
2824 .map(|name| format!("{name}_Offset")),
2825 });
2826 edge_images.insert(
2827 source_edge.id,
2828 (id, start_vertex_id, source_coedge.forward),
2829 );
2830 (id, true)
2831 };
2832 let id = next_id;
2833 next_id += 1;
2834 coedges.push(CoedgeRecord {
2835 id,
2836 edge_id,
2837 forward,
2838 pcurve: source_coedge.pcurve.clone(),
2839 });
2840 }
2841 let id = next_id;
2842 next_id += 1;
2843 loops.push(LoopRecord { id, coedges });
2844 }
2845 let deviation = if measure {
2846 Some(measure_carrier(
2847 &surface,
2848 &vertices,
2849 &edges,
2850 &loops,
2851 lane,
2852 surface_fit,
2853 band,
2854 )?)
2855 } else {
2856 None
2857 };
2858 Ok(OffsetFaceCarrier {
2859 vertices,
2860 edges,
2861 face: FaceRecord {
2862 id: next_id,
2863 surface,
2864 same_sense: source.same_sense,
2865 loops,
2866 name: source.name.as_ref().map(|name| format!("{name}_Offset")),
2867 },
2868 deviation,
2869 lane,
2870 })
2871}
2872
2873
2874
2875