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
311thread_local! {
312 static EXACT_TRIM_BOX_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
313}
314
315fn exact_trim_box_on() -> bool {
316 if let Some(forced) = EXACT_TRIM_BOX_FOR_TEST.with(|forced| forced.get()) {
317 return forced;
318 }
319 match std::env::var("BREP_OFFSET_EXACT_TRIM_BOX").as_deref() {
320 Ok("0") | Ok("off") => false,
321 Ok("1") | Ok("on") => true,
322 _ => EXACT_TRIM_BOX_DEFAULT,
323 }
324}
325
326
327const TRIM_EXTREME_STEPS: usize = 64;
331
332fn refine_trim_extreme(
341 pcurve: &NurbsCurve,
342 samples: &[(f64, Vec3)],
343 axis: usize,
344 sign: f64,
345) -> Result<f64, KernelRefusal> {
346 let read = |uv: Vec3| sign * if axis == 0 { uv.x } else { uv.y };
347 let at = |parameter: f64| -> Result<f64, KernelRefusal> {
348 Ok(read(pcurve.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?))
349 };
350 let best = (0..samples.len())
351 .max_by(|&a, &b| read(samples[a].1).total_cmp(&read(samples[b].1)))
352 .expect("a pcurve has samples");
353 let mut found = read(samples[best].1);
354 let (mut a, mut b) = (samples[best.saturating_sub(1)].0, samples[(best + 1).min(samples.len() - 1)].0);
355 const GOLDEN: f64 = 0.618_033_988_749_894_9;
356 let mut c = b - GOLDEN * (b - a);
357 let mut d = a + GOLDEN * (b - a);
358 let (mut fc, mut fd) = (at(c)?, at(d)?);
359 for _ in 0..TRIM_EXTREME_STEPS {
360 found = found.max(fc).max(fd);
361 if fc >= fd {
362 (b, d, fd) = (d, c, fc);
363 c = b - GOLDEN * (b - a);
364 fc = at(c)?;
365 } else {
366 (a, c, fc) = (c, d, fd);
367 d = a + GOLDEN * (b - a);
368 fd = at(d)?;
369 }
370 }
371 Ok(sign * found.max(fc).max(fd))
372}
373
374fn trim_domain_fractions(
379 face: &FaceRecord,
380 [u0, u1]: [f64; 2],
381 [v0, v1]: [f64; 2],
382) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
383 let mut low = Vec2 {
384 x: f64::INFINITY,
385 y: f64::INFINITY,
386 };
387 let mut high = Vec2 {
388 x: f64::NEG_INFINITY,
389 y: f64::NEG_INFINITY,
390 };
391 for coedge in face
392 .loops
393 .iter()
394 .flat_map(|loop_record| &loop_record.coedges)
395 {
396 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
397 let mut samples = Vec::with_capacity(25);
398 for sample in 0..=24 {
399 let parameter = p0 + (p1 - p0) * sample as f64 / 24.0;
400 let uv = coedge
401 .pcurve
402 .evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?;
403 low.x = low.x.min(uv.x);
404 low.y = low.y.min(uv.y);
405 high.x = high.x.max(uv.x);
406 high.y = high.y.max(uv.y);
407 samples.push((parameter, uv));
408 }
409 if coedge.pcurve.degree > 1 && exact_trim_box_on() {
410 low.x = low.x.min(refine_trim_extreme(&coedge.pcurve, &samples, 0, -1.0)?);
411 high.x = high.x.max(refine_trim_extreme(&coedge.pcurve, &samples, 0, 1.0)?);
412 low.y = low.y.min(refine_trim_extreme(&coedge.pcurve, &samples, 1, -1.0)?);
413 high.y = high.y.max(refine_trim_extreme(&coedge.pcurve, &samples, 1, 1.0)?);
414 }
415 }
416 let fraction = |value: f64, start: f64, end: f64| {
417 let span = end - start;
418 if span.abs() <= f64::EPSILON || !value.is_finite() {
419 return None;
420 }
421 Some(((value - start) / span).clamp(0.0, 1.0))
422 };
423 let u = match (fraction(low.x, u0, u1), fraction(high.x, u0, u1)) {
424 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
425 _ => [0.0, 1.0],
426 };
427 let v = match (fraction(low.y, v0, v1), fraction(high.y, v0, v1)) {
428 (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
429 _ => [0.0, 1.0],
430 };
431 Ok((u, v))
432}
433
434fn domain_end_moves(grow_min: f64, grow_max: f64, [ta, tb]: [f64; 2]) -> (f64, f64) {
444 let span = tb - ta;
445 if span <= 1e-6 {
446 return (grow_min, grow_max);
447 }
448 let rate = (grow_min + grow_max) / span;
449 let back = grow_min + ta * rate;
450 let forward = (1.0 - ta) * rate - grow_min;
451 (back.max(0.0), forward.max(0.0))
452}
453
454pub fn offset_surface(
458 face: &FaceRecord,
459 distance: f64,
460 planar_extension: f64,
461) -> Result<NurbsSurface, KernelRefusal> {
462 offset_surface_with_lane(face, distance, &CarrierExtension::uniform(planar_extension))
463 .map(|(surface, _)| surface)
464}
465
466pub fn offset_surface_measured(
475 face: &FaceRecord,
476 distance: f64,
477 planar_extension: f64,
478 band: f64,
479) -> Result<MeasuredOffsetSurface, KernelRefusal> {
480 offset_surface_measured_sided(
481 face,
482 distance,
483 &CarrierExtension::uniform(planar_extension),
484 band,
485 )
486}
487
488pub fn offset_surface_measured_sided(
490 face: &FaceRecord,
491 distance: f64,
492 extension: &CarrierExtension,
493 band: f64,
494) -> Result<MeasuredOffsetSurface, KernelRefusal> {
495 offset_surface_measured_sided_mode(face, distance, extension, band,
496 std::env::var("BREP_OFFSET_SPHERE_READING").map_or(true, |v| v != "0" && v != "off"))
497}
498
499fn offset_surface_measured_sided_mode(
500 face: &FaceRecord, distance: f64, extension: &CarrierExtension, band: f64, reuse: bool,
501) -> Result<MeasuredOffsetSurface, KernelRefusal> {
502 let mut sphere_reading = None;
503 let (surface, lane, extended) = offset_surface_constructed(face, distance, extension, &mut sphere_reading)?;
504 let fit = match lane {
505 OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
506 OffsetSurfaceLane::Analytic if reuse && sphere_reading.is_some() => {
511 if std::env::var_os("BREP_PROFILE").is_some() {
515 eprintln!("offset.sphere_reading.profile reused=1");
516 }
517 Some(MeasuredTolerance::new(sphere_reading.unwrap(), band))
518 }
519 OffsetSurfaceLane::Analytic | OffsetSurfaceLane::Fit => {
520 Some(measure_surface_fit_against_pointwise_offset(
521 extended.as_ref().unwrap_or(&face.surface),
522 face.same_sense,
523 &surface,
524 distance,
525 band,
526 )?)
527 }
528 OffsetSurfaceLane::Reparameterised => None,
529 };
530 Ok(MeasuredOffsetSurface { surface, lane, fit })
531}
532
533fn sphere_offset_surface(face: &FaceRecord, distance: f64, reading: &mut Option<f64>) -> Result<Option<NurbsSurface>, KernelRefusal> {
562 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
566 return Ok(None);
567 }
568 let source = &face.surface;
569 let Some((centre, radius)) = source.analytic().and_then(|shape| shape.sphere_geometry()) else {
570 return Ok(None);
571 };
572 let ([u0, u1], [v0, v1]) = domains(source)?;
573 let Ok(sample) = OffsetEvaluator::new(
580 "offset_surface",
581 source,
582 OffsetNormal::FaceStable {
583 same_sense: face.same_sense,
584 },
585 )
586 .at((u0 + u1) / 2.0, (v0 + v1) / 2.0, -distance)
587 else {
588 return Ok(None);
589 };
590 let source_radial = sample.source.sub(centre);
591 let offset_radial = sample.point.sub(centre);
592 let offset_radius = offset_radial.length();
593 let scale = radius.max(1.0);
594 if radius <= 1e-12
595 || offset_radius <= 1e-9 * scale
596 || offset_radial.dot(source_radial) <= 0.0
599 || ((offset_radius - radius).abs() - distance.abs()).abs() > 1e-9 * scale
602 {
603 return Ok(None);
604 }
605 let factor = offset_radius / radius;
606 let Ok(controls) = source
609 .control_points
610 .iter()
611 .map(|row| {
612 row.iter()
613 .map(|control| {
614 Ok(Vec4::from_point(
615 centre.add(control.point().or_refuse(KernelStage::Refine, "point")?.sub(centre).scale(factor)),
616 control.w,
617 ))
618 })
619 .collect::<Result<Vec<_>, KernelRefusal>>()
620 })
621 .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
622 else {
623 return Ok(None);
624 };
625 let Ok(surface) = NurbsSurface::new(
626 source.degree_u,
627 source.degree_v,
628 source.knots_u.clone(),
629 source.knots_v.clone(),
630 controls,
631 ) else {
632 return Ok(None);
633 };
634 match crate::offset_measure::sphere_fit_reading(
638 source,
639 face.same_sense,
640 &surface,
641 distance,
642 1e-9 * scale,
643 ) {
644 Ok(measured) if measured.within_band() => {
645 *reading = Some(measured.deviation());
646 Ok(Some(surface))
647 },
648 _ => Ok(None),
649 }
650}
651
652fn revolved_arc_offset_surface(
685 face: &FaceRecord,
686 distance: f64,
687) -> Result<Option<NurbsSurface>, KernelRefusal> {
688 if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
689 return Ok(None);
690 }
691 let source = &face.surface;
692 match source.analytic() {
693 Some(crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. }) => {}
694 _ => return Ok(None),
695 }
696 let rows = &source.control_points;
697 let Some(first_row) = rows.first() else {
698 return Ok(None);
699 };
700 if rows.iter().flatten().any(|control| !(control.w > 0.0)) {
701 return Ok(None);
702 }
703 let Ok(generatrix) = NurbsCurve::new(source.degree_v, source.knots_v.clone(), first_row.clone())
704 else {
705 return Ok(None);
706 };
707 let [t0, t1] = generatrix.domain().or_refuse(KernelStage::Refine, "domain")?;
708 let at = |fraction: f64| generatrix.evaluate(t0 + (t1 - t0) * fraction);
709 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")?);
711 let (ab, ae) = (b.sub(a), e.sub(a));
712 let normal = ab.cross(ae);
713 let normal_length_squared = normal.dot(normal);
714 if normal_length_squared <= 1e-24 {
715 return Ok(None);
716 }
717 let centre = a.add(
718 ae.cross(normal)
719 .scale(ab.dot(ab))
720 .add(normal.cross(ab).scale(ae.dot(ae)))
721 .scale(0.5 / normal_length_squared),
722 );
723 let radius = a.sub(centre).length();
724 let scale = (centre.length() + radius).max(1.0);
725 let band = 1e-9 * scale;
726 let unit_normal = normal.scale(1.0 / normal_length_squared.sqrt());
727 let samples = 16 * generatrix.control_points.len();
728 for index in 0..=samples {
729 let point = at(index as f64 / samples as f64).or_refuse(KernelStage::Refine, "at")?;
730 let radial = point.sub(centre);
731 if (radial.length() - radius).abs() > band || radial.dot(unit_normal).abs() > band {
732 return Ok(None);
733 }
734 }
735 let controls = first_row
738 .iter()
739 .map(|control| control.point().or_refuse(KernelStage::Refine, "point"))
740 .collect::<Result<Vec<_>, KernelRefusal>>()?;
741 let origin = controls[0];
742 let mut best = (0usize, 0usize, 0.0f64);
743 for first in 1..controls.len() {
744 for second in first + 1..controls.len() {
745 let area = controls[first]
746 .sub(origin)
747 .cross(controls[second].sub(origin))
748 .length();
749 if area > best.2 {
750 best = (first, second, area);
751 }
752 }
753 }
754 let (first, second, area) = best;
755 if area <= 1e-9 * scale * scale {
756 return Ok(None);
757 }
758 let (along_first, along_second) = (controls[first].sub(origin), controls[second].sub(origin));
759 let offset_from_origin = centre.sub(origin);
760 let (g11, g12, g22) = (
761 along_first.dot(along_first),
762 along_first.dot(along_second),
763 along_second.dot(along_second),
764 );
765 let determinant = g11 * g22 - g12 * g12;
766 let (r1, r2) = (along_first.dot(offset_from_origin), along_second.dot(offset_from_origin));
767 let s = (r1 * g22 - r2 * g12) / determinant;
768 let t = (r2 * g11 - r1 * g12) / determinant;
769 if origin
770 .add(along_first.scale(s))
771 .add(along_second.scale(t))
772 .sub(centre)
773 .length()
774 > band
775 {
776 return Ok(None);
777 }
778 let ([u0, _], [v0, v1]) = domains(source)?;
781 let Ok(sample) = OffsetEvaluator::new(
782 "offset_surface",
783 source,
784 OffsetNormal::FaceStable {
785 same_sense: face.same_sense,
786 },
787 )
788 .at(u0, (v0 + v1) / 2.0, -distance)
789 else {
790 return Ok(None);
791 };
792 let source_radial = sample.source.sub(centre);
793 let offset_radial = sample.point.sub(centre);
794 let offset_radius = offset_radial.length();
795 if (source_radial.length() - radius).abs() > band
796 || offset_radius <= band
797 || offset_radial.dot(source_radial) <= 0.0
798 || ((offset_radius - radius).abs() - distance.abs()).abs() > band
799 {
800 return Ok(None);
801 }
802 let factor = offset_radius / radius;
803 let Ok(offset_rows) = rows
804 .iter()
805 .map(|row| {
806 let points = row.iter().map(|control| control.point().or_refuse(KernelStage::Refine, "point")).collect::<Result<Vec<_>, KernelRefusal>>()?;
807 let row_origin = points[0];
808 let row_centre = row_origin
809 .add(points[first].sub(row_origin).scale(s))
810 .add(points[second].sub(row_origin).scale(t));
811 Ok(points
812 .iter()
813 .zip(row)
814 .map(|(point, control)| {
815 Vec4::from_point(row_centre.add(point.sub(row_centre).scale(factor)), control.w)
816 })
817 .collect::<Vec<_>>())
818 })
819 .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
820 else {
821 return Ok(None);
822 };
823 let Ok(surface) = NurbsSurface::new(
824 source.degree_u,
825 source.degree_v,
826 source.knots_u.clone(),
827 source.knots_v.clone(),
828 offset_rows,
829 ) else {
830 return Ok(None);
831 };
832 match measure_surface_fit_against_pointwise_offset(
833 source,
834 face.same_sense,
835 &surface,
836 distance,
837 band,
838 ) {
839 Ok(measured) if measured.within_band() => Ok(Some(surface)),
840 _ => Ok(None),
841 }
842}
843
844pub(crate) fn offset_surface_with_lane(
845 face: &FaceRecord,
846 distance: f64,
847 extension: &CarrierExtension,
848) -> Result<(NurbsSurface, OffsetSurfaceLane), KernelRefusal> {
849 offset_surface_constructed(face, distance, extension, &mut None).map(|(surface, lane, _)| (surface, lane))
850}
851
852pub(crate) fn transpose_offset_surface(surface: &NurbsSurface) -> Result<NurbsSurface, KernelRefusal> {
856 let controls = (0..surface.control_points[0].len())
857 .map(|column| surface.control_points.iter().map(|row| row[column]).collect())
858 .collect();
859 NurbsSurface::new(surface.degree_v, surface.degree_u, surface.knots_v.clone(),
860 surface.knots_u.clone(), controls).or_refuse(KernelStage::Refine, "offset_transpose")
861}
862
863fn offset_surface_constructed(
864 face: &FaceRecord,
865 distance: f64,
866 extension: &CarrierExtension,
867 sphere_reading: &mut Option<f64>,
868) -> Result<(NurbsSurface, OffsetSurfaceLane, Option<NurbsSurface>), KernelRefusal> {
869 let source = &face.surface;
870 let extension_active = extension.is_active();
871 let normalize_ruling = std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0")
877 && source.degree_u == 1 && source.control_points.len() == 2;
878 let normalize_sphere = std::env::var("BREP_OFFSET_FOLD_CHART").as_deref() != Ok("0")
879 && source.degree_u == 2 && source.degree_v == 2 && source.analytic().is_none();
880 if normalize_ruling || normalize_sphere {
881 let transposed = transpose_offset_surface(source)?;
882 let ruled = normalize_ruling && matches!(transposed.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. }));
883 let spherical = normalize_sphere && transposed.analytic().and_then(|shape| shape.sphere_geometry()).is_some();
884 if ruled || spherical {
885 let mut turned = face.clone();
886 turned.surface = transposed;
887 turned.same_sense = !turned.same_sense;
888 for coedge in turned.loops.iter_mut().flat_map(|lp| &mut lp.coedges) {
889 let controls = coedge.pcurve.control_points.iter().map(|point| {
890 let mut swapped = *point;
891 std::mem::swap(&mut swapped.x, &mut swapped.y);
892 swapped
893 }).collect();
894 coedge.pcurve = NurbsCurve::new(coedge.pcurve.degree,
895 coedge.pcurve.knots.clone(), controls).or_refuse(KernelStage::Refine, "offset_transpose_pcurve")?;
896 }
897 let turned_extension = CarrierExtension {
898 u_min: extension.v_min, u_max: extension.v_max,
899 v_min: extension.u_min, v_max: extension.u_max,
900 };
901 let (surface, lane, extended) = offset_surface_constructed(&turned, distance, &turned_extension, &mut None)?;
902 return Ok((transpose_offset_surface(&surface)?, lane,
903 extended.as_ref().map(transpose_offset_surface).transpose()?));
904 }
905 }
906 if source.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
907 let ([u0, u1], [v0, v1]) = domains(source)?;
908 let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
909 let shift = normal.scale(-distance);
910 let mut points = source
911 .control_points
912 .iter()
913 .map(|row| {
914 row.iter()
915 .map(|control| Ok(control.point().or_refuse(KernelStage::Refine, "point")?.add(shift)))
916 .collect::<Result<Vec<_>, KernelRefusal>>()
917 })
918 .collect::<Result<Vec<_>, KernelRefusal>>()?;
919 if extension_active {
920 let p00 = points[0][0];
921 let p01 = points[0][1];
922 let p10 = points[1][0];
923 let direction_u = p10.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
924 let direction_v = p01.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
925 let (trim_u, trim_v) = trim_domain_fractions(face, [u0, u1], [v0, v1])?;
931 let (back_u, forward_u) = domain_end_moves(extension.u_min, extension.u_max, trim_u);
932 let (back_v, forward_v) = domain_end_moves(extension.v_min, extension.v_max, trim_v);
933 points[0][0] = p00
934 .sub(direction_u.scale(back_u))
935 .sub(direction_v.scale(back_v));
936 points[0][1] = p01
937 .sub(direction_u.scale(back_u))
938 .add(direction_v.scale(forward_v));
939 points[1][0] = p10
940 .add(direction_u.scale(forward_u))
941 .sub(direction_v.scale(back_v));
942 points[1][1] = points[1][1]
943 .add(direction_u.scale(forward_u))
944 .add(direction_v.scale(forward_v));
945 }
946 let controls = points
947 .into_iter()
948 .enumerate()
949 .map(|(row, points)| {
950 points
951 .into_iter()
952 .enumerate()
953 .map(|(column, point)| {
954 Vec4::from_point(point, source.control_points[row][column].w)
955 })
956 .collect()
957 })
958 .collect();
959 let lane = if extension_active {
963 OffsetSurfaceLane::Reparameterised
964 } else {
965 OffsetSurfaceLane::Affine
966 };
967 return Ok((
968 NurbsSurface::new(
969 source.degree_u,
970 source.degree_v,
971 source.knots_u.clone(),
972 source.knots_v.clone(),
973 controls,
974 ).or_refuse(KernelStage::Refine, "new")?,
975 lane,
976 None,
977 ));
978 }
979
980 if let Some(surface) = sphere_offset_surface(face, distance, sphere_reading)? {
990 return Ok((surface, OffsetSurfaceLane::Analytic, None));
991 }
992 if let Some(surface) = revolved_arc_offset_surface(face, distance)? {
995 return Ok((surface, OffsetSurfaceLane::Analytic, None));
996 }
997
998 let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u).or_refuse(KernelStage::Refine, "new")?;
999 let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v).or_refuse(KernelStage::Refine, "new")?;
1000 let parameters_u = greville_parameters(&knot_u);
1001 let parameters_v = greville_parameters(&knot_v);
1002 let evaluator = OffsetEvaluator::new(
1008 "offset_surface",
1009 source,
1010 OffsetNormal::FaceStable {
1011 same_sense: face.same_sense,
1012 },
1013 );
1014 let mut samples = Vec::new();
1015 for &u in ¶meters_u {
1016 let mut row = Vec::new();
1017 for &v in ¶meters_v {
1018 row.push(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point);
1019 }
1020 samples.push(row);
1021 }
1022 let mut reparameterised = false;
1037 let mut extended_source: Option<NurbsSurface> = None;
1038 if parameters_v.len() == 2 && parameters_u.len() >= 2 {
1045 let centroid = |column: usize| {
1046 let mut sum = Vec3::default();
1047 for row in &samples {
1048 sum = sum.add(row[column]);
1049 }
1050 sum.scale(1.0 / samples.len() as f64)
1051 };
1052 let near_centroid = centroid(0);
1053 let far_centroid = centroid(1);
1054 let mut inverted = true;
1055 let mut pinch_fraction = 0.0f64;
1056 let mut near_mean = 0.0f64;
1057 let mut far_mean = 0.0f64;
1058 for row in &samples {
1059 let near_radial = row[0].sub(near_centroid);
1060 let far_radial = row[1].sub(far_centroid);
1061 let near_len = near_radial.length();
1062 let far_len = far_radial.length();
1063 if near_len <= 1e-9 || far_len <= 1e-9 {
1064 inverted = false;
1065 break;
1066 }
1067 if near_radial.dot(far_radial) >= 0.0 {
1068 inverted = false;
1069 break;
1070 }
1071 pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
1072 near_mean += near_len / samples.len() as f64;
1073 far_mean += far_len / samples.len() as f64;
1074 }
1075 if inverted {
1076 let retrim_far = far_mean <= near_mean;
1077 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1101 let trim_reaches_the_pinch = if retrim_far {
1102 trim_v[1] > pinch_fraction + 1e-9
1103 } else {
1104 trim_v[0] < pinch_fraction - 1e-9
1105 };
1106 if trim_reaches_the_pinch {
1107 reparameterised = true;
1110 for row in &mut samples {
1111 let near = row[0];
1112 let far = row[1];
1113 let pinch = near.add(far.sub(near).scale(pinch_fraction));
1114 if retrim_far {
1115 row[1] = pinch;
1116 } else {
1117 row[0] = pinch;
1118 }
1119 }
1120 }
1121 }
1122 if extension.extends_v() && !inverted {
1142 let mut min_ruling = f64::MAX;
1143 let mut back_allowance = f64::MAX;
1144 let mut forward_allowance = f64::MAX;
1145 let mut extendable = true;
1146 for row in &samples {
1147 let ruling = row[1].sub(row[0]);
1148 let length = ruling.length();
1149 min_ruling = min_ruling.min(length);
1150 let near_radial = row[0].sub(near_centroid).length();
1154 let far_radial = row[1].sub(far_centroid).length();
1155 if (far_radial - near_radial).abs() > 1e-9 {
1156 let apex_at = near_radial / (near_radial - far_radial);
1157 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1158 extendable = false;
1160 break;
1161 }
1162 if apex_at < 0.0 {
1163 back_allowance = back_allowance.min(0.9 * -apex_at);
1164 } else {
1165 forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
1166 }
1167 }
1168 }
1169 let mut source_back = f64::MAX;
1177 let mut source_forward = f64::MAX;
1178 if extendable {
1179 let source_centroid = |column: usize| {
1180 let mut sum = Vec3::default();
1181 for row in &source.control_points {
1182 if let Ok(point) = row[column].point() {
1183 sum = sum.add(point);
1184 }
1185 }
1186 sum.scale(1.0 / source.control_points.len() as f64)
1187 };
1188 let (near_source, far_source) = (source_centroid(0), source_centroid(1));
1189 for row in &source.control_points {
1190 let (near, far) = (row[0].point().or_refuse(KernelStage::Refine, "point")?, row[1].point().or_refuse(KernelStage::Refine, "point")?);
1191 let near_radial = near.sub(near_source).length();
1192 let far_radial = far.sub(far_source).length();
1193 if (far_radial - near_radial).abs() > 1e-9 {
1194 let apex_at = near_radial / (near_radial - far_radial);
1195 if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1196 extendable = false;
1197 break;
1198 }
1199 if apex_at < 0.0 {
1200 source_back = source_back.min(0.9 * -apex_at);
1201 } else {
1202 source_forward = source_forward.min(0.9 * (apex_at - 1.0));
1203 }
1204 }
1205 }
1206 }
1207 let (back_allowance, forward_allowance) =
1208 (back_allowance.min(source_back), forward_allowance.min(source_forward));
1209 if extendable && min_ruling > 1e-9 {
1210 let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1217 let (back_world, forward_world) =
1218 domain_end_moves(extension.v_min, extension.v_max, trim_v);
1219 let (requested_back, requested_forward) = (back_world / min_ruling, forward_world / min_ruling);
1220 let apex_end = analytic_offset_apex(source, &samples, offset_fit_contract(source)).and_then(
1232 |(at, apex)| {
1233 if at > 1.0 && at - 1.0 <= requested_forward && at - 1.0 <= source_forward {
1234 Some((false, at - 1.0, apex))
1235 } else if at < 0.0 && -at <= requested_back && -at <= source_back {
1236 Some((true, -at, apex))
1237 } else {
1238 None
1239 }
1240 },
1241 );
1242 let back = match apex_end {
1243 Some((true, reach, _)) => reach,
1244 _ => requested_back.min(back_allowance),
1245 };
1246 let forward = match apex_end {
1247 Some((false, reach, _)) => reach,
1248 _ => requested_forward.min(forward_allowance),
1249 };
1250 let stretched = samples
1251 .iter()
1252 .map(|row| {
1253 let ruling = row[1].sub(row[0]);
1254 let (near, far) = (row[0].sub(ruling.scale(back)), row[1].add(ruling.scale(forward)));
1255 match apex_end {
1256 Some((true, _, apex)) => vec![apex, far],
1257 Some((false, _, apex)) => vec![near, apex],
1258 None => vec![near, far],
1259 }
1260 })
1261 .collect::<Vec<_>>();
1262 let extended = extend_along_rulings(source, back, forward)?;
1263 let extended_offsets = OffsetEvaluator::new(
1264 "offset_surface",
1265 &extended,
1266 OffsetNormal::FaceStable {
1267 same_sense: face.same_sense,
1268 },
1269 );
1270 let [v0, v1] = knot_v.domain();
1271 let mut developability = 0.0f64;
1272 for (row, &u) in stretched.iter().zip(¶meters_u) {
1273 for fraction in [0.0, 0.5, 1.0] {
1274 let exact = extended_offsets.at(u, v0 + (v1 - v0) * fraction, -distance).or_refuse(KernelStage::Refine, "at")?.point;
1275 let along = row[0].add(row[1].sub(row[0]).scale(fraction));
1276 developability = developability.max(exact.sub(along).length());
1277 }
1278 }
1279 let contract = offset_fit_contract(source);
1280 let stretch_kept = developability <= contract;
1281 samples = if stretch_kept {
1282 stretched
1283 } else {
1284 parameters_u
1285 .iter()
1286 .map(|&u| {
1287 parameters_v
1288 .iter()
1289 .map(|&v| Ok(extended_offsets.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1290 .collect::<Result<Vec<_>, KernelRefusal>>()
1291 })
1292 .collect::<Result<Vec<_>, KernelRefusal>>()?
1293 };
1294 if let Ok(path) = std::env::var("BREP_OFFSET_FIT_TRACE") {
1295 use std::io::Write;
1296 let line = format!(
1297 "extension deg=({},{}) net=({},{}) d={distance:.6} back={back:.6} forward={forward:.6} \
1298 developability={developability:.3e} contract={contract:.3e} lane={} thread={} args={}\n",
1299 source.degree_u,
1300 source.degree_v,
1301 source.control_points.len(),
1302 source.control_points[0].len(),
1303 if stretch_kept { "stretch" } else { "pointwise" },
1304 std::thread::current().name().unwrap_or("-"),
1305 std::env::args().skip(1).collect::<Vec<_>>().join(" "),
1306 );
1307 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1308 let _ = file.write_all(line.as_bytes());
1309 }
1310 }
1311 extended_source = Some(extended);
1312 }
1313 }
1314 }
1315 let reference = extended_source.as_ref().unwrap_or(source);
1316 let weights = reference
1317 .control_points
1318 .iter()
1319 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1320 .collect::<Vec<_>>();
1321 let fitted = NurbsSurface::new(
1322 source.degree_u,
1323 source.degree_v,
1324 source.knots_u.clone(),
1325 source.knots_v.clone(),
1326 interpolate_tensor(
1327 &knot_u,
1328 &knot_v,
1329 ¶meters_u,
1330 ¶meters_v,
1331 &samples,
1332 &weights,
1333 )?,
1334 ).or_refuse(KernelStage::Refine, "new")?;
1335 if reparameterised {
1336 return Ok((fitted, OffsetSurfaceLane::Reparameterised, None));
1337 }
1338 let reference_offsets = OffsetEvaluator::new(
1339 "offset_surface",
1340 reference,
1341 OffsetNormal::FaceStable {
1342 same_sense: face.same_sense,
1343 },
1344 );
1345 Ok((
1346 refine_offset_fit(reference, &reference_offsets, distance, fitted)?,
1347 OffsetSurfaceLane::Fit,
1348 extended_source,
1349 ))
1350}
1351
1352
1353fn analytic_offset_apex(source: &NurbsSurface, samples: &[Vec<Vec3>], contract: f64) -> Option<(f64, Vec3)> {
1362 let Some(crate::AnalyticSurface::RuledRevolution { frame, .. }) = crate::analytic_surface::recognize(source) else {
1363 return None;
1364 };
1365 let radial = |point: Vec3| {
1366 let offset = point.sub(frame.origin);
1367 offset.sub(frame.axis.scale(offset.dot(frame.axis)))
1368 };
1369 let mut crossings = Vec::with_capacity(samples.len());
1370 for row in samples {
1371 let (near, far) = (radial(row[0]), radial(row[1]));
1372 let step = far.sub(near);
1373 let at = -near.dot(step) / step.dot(step);
1374 if !at.is_finite() {
1375 return None;
1376 }
1377 crossings.push((at, row[0].add(row[1].sub(row[0]).scale(at))));
1378 }
1379 let count = crossings.len() as f64;
1382 let at = crossings.iter().map(|(at, _)| at).sum::<f64>() / count;
1383 let first = crossings.first()?.1;
1384 let spread = crossings.iter().fold(Vec3::default(), |sum, (_, point)| sum.add(point.sub(first)));
1385 let centre = first.add(spread.scale(1.0 / count));
1386 let apex = frame.origin.add(frame.axis.scale(centre.sub(frame.origin).dot(frame.axis)));
1387 crossings
1390 .iter()
1391 .all(|(_, point)| radial(*point).length() <= contract && point.sub(apex).length() <= contract)
1392 .then_some((at, apex))
1393}
1394
1395fn extend_along_rulings(surface: &NurbsSurface, back: f64, forward: f64) -> Result<NurbsSurface, KernelRefusal> {
1402 let rows = surface
1403 .control_points
1404 .iter()
1405 .map(|row| {
1406 let step = row[1].add(row[0].scale(-1.0));
1407 vec![row[0].add(step.scale(-back)), row[1].add(step.scale(forward))]
1408 })
1409 .collect::<Vec<_>>();
1410 if rows.iter().flatten().any(|point| point.w <= 1e-12) {
1411 return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_ruled_extension_weight", format!(
1412 "offset_surface: extending a ruled face by ({back:.6}, {forward:.6}) of its rulings \
1413 carries a weight through zero; the extended face is not this surface"
1414 )));
1415 }
1416 NurbsSurface::new(surface.degree_u, 1, surface.knots_u.clone(), surface.knots_v.clone(), rows)
1417 .or_refuse(KernelStage::Refine, "new")
1418}
1419
1420fn offset_fit_contract(source: &NurbsSurface) -> f64 {
1423 let scale = crate::model_scale(
1424 source
1425 .control_points
1426 .iter()
1427 .flatten()
1428 .filter_map(|point| point.point().ok()),
1429 );
1430 crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit
1431}
1432
1433const OFFSET_FIT_REFINEMENTS: usize = 6;
1435
1436const OFFSET_FIT_MAX_CONTROLS_SEPARABLE: usize = 16_384;
1439const OFFSET_FIT_MAX_CONTROLS_TENSOR: usize = 900;
1440
1441#[derive(Clone, Debug)]
1446struct OffsetFitDeviation {
1447 along_u: f64,
1448 along_v: f64,
1449 between: f64,
1450 u_sites: Vec<(f64, f64)>,
1451 v_sites: Vec<(f64, f64)>,
1452 between_uv: (f64, f64),
1453}
1454
1455impl OffsetFitDeviation {
1456 fn worst(&self) -> f64 {
1457 self.along_u.max(self.along_v).max(self.between)
1458 }
1459
1460 fn refinement_sites(&self, bar: f64) -> (Vec<f64>, Vec<f64>) {
1461 let mut u = self.u_sites.iter().filter(|(_, miss)| *miss > bar)
1462 .map(|(parameter, _)| *parameter).collect::<Vec<_>>();
1463 let mut v = self.v_sites.iter().filter(|(_, miss)| *miss > bar)
1464 .map(|(parameter, _)| *parameter).collect::<Vec<_>>();
1465 if u.is_empty() && v.is_empty() && self.between > bar {
1466 u.push(self.between_uv.0);
1467 v.push(self.between_uv.1);
1468 }
1469 (u, v)
1470 }
1471}
1472
1473fn greville_midpoints(parameters: &[f64]) -> Vec<f64> {
1474 parameters
1475 .windows(2)
1476 .filter(|pair| pair[1] - pair[0] > 1e-12)
1477 .map(|pair| 0.5 * (pair[0] + pair[1]))
1478 .collect()
1479}
1480
1481fn offset_fit_deviation(
1482 fitted: &NurbsSurface,
1483 evaluator: &OffsetEvaluator,
1484 distance: f64,
1485) -> Result<OffsetFitDeviation, KernelRefusal> {
1486 let parameters_u = greville_parameters(&KnotVector::new(fitted.knots_u.clone(), fitted.degree_u).or_refuse(KernelStage::Refine, "new")?);
1487 let parameters_v = greville_parameters(&KnotVector::new(fitted.knots_v.clone(), fitted.degree_v).or_refuse(KernelStage::Refine, "new")?);
1488 let (middle_u, middle_v) = (greville_midpoints(¶meters_u), greville_midpoints(¶meters_v));
1489 let residual_locus = std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0");
1490 let gap = |u: f64, v: f64| -> Result<f64, KernelRefusal> {
1491 Ok(fitted.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?.sub(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point).length())
1492 };
1493 let mut deviation = OffsetFitDeviation {
1494 along_u: 0.0,
1495 along_v: 0.0,
1496 between: 0.0,
1497 u_sites: Vec::new(),
1498 v_sites: if residual_locus { middle_v.iter().map(|&v| (v, 0.0)).collect() } else { Vec::new() },
1499 between_uv: (0.0, 0.0),
1500 };
1501 for &u in &middle_u {
1502 let mut along = 0.0f64;
1503 for &v in ¶meters_v {
1504 let miss = gap(u, v)?;
1505 along = along.max(miss);
1506 deviation.along_u = deviation.along_u.max(miss);
1507 }
1508 if residual_locus { deviation.u_sites.push((u, along)); }
1509 for &v in &middle_v {
1510 let miss = gap(u, v)?;
1511 if miss > deviation.between { deviation.between_uv = (u, v); }
1512 deviation.between = deviation.between.max(miss);
1513 }
1514 }
1515 for &u in ¶meters_u {
1516 for (index, &v) in middle_v.iter().enumerate() {
1517 let miss = gap(u, v)?;
1518 deviation.along_v = deviation.along_v.max(miss);
1519 if residual_locus { deviation.v_sites[index].1 = deviation.v_sites[index].1.max(miss); }
1520 }
1521 }
1522 Ok(deviation)
1523}
1524
1525fn raise_linear_directions(
1535 surface: &NurbsSurface,
1536 in_u: bool,
1537 in_v: bool,
1538) -> Result<Option<NurbsSurface>, KernelRefusal> {
1539 fn raise(knots: &[f64], row: &[Vec4]) -> Option<(Vec<f64>, Vec<Vec4>)> {
1540 let count = row.len();
1542 if knots.len() != count + 2 {
1543 return None;
1544 }
1545 let breaks = &knots[1..=count];
1546 if breaks.windows(2).any(|pair| pair[1] - pair[0] <= 1e-12) {
1547 return None;
1548 }
1549 let mut raised_knots = vec![breaks[0]; 4];
1550 for &interior in &breaks[1..count - 1] {
1551 raised_knots.extend([interior; 3]);
1552 }
1553 raised_knots.extend([breaks[count - 1]; 4]);
1554 let mut points = Vec::with_capacity(3 * (count - 1) + 1);
1555 for pair in row.windows(2) {
1556 let step = pair[1].add(pair[0].scale(-1.0));
1557 points.push(pair[0]);
1558 points.push(pair[0].add(step.scale(1.0 / 3.0)));
1559 points.push(pair[0].add(step.scale(2.0 / 3.0)));
1560 }
1561 points.push(row[count - 1]);
1562 Some((raised_knots, points))
1563 }
1564 let mut knots_u = surface.knots_u.clone();
1565 let mut knots_v = surface.knots_v.clone();
1566 let mut degree_u = surface.degree_u;
1567 let mut degree_v = surface.degree_v;
1568 let mut net = surface.control_points.clone();
1569 if in_v {
1570 if degree_v != 1 {
1571 return Ok(None);
1572 }
1573 let mut rows = Vec::with_capacity(net.len());
1574 for row in &net {
1575 let Some((knots, points)) = raise(&surface.knots_v, row) else {
1576 return Ok(None);
1577 };
1578 knots_v = knots;
1579 rows.push(points);
1580 }
1581 net = rows;
1582 degree_v = 3;
1583 }
1584 if in_u {
1585 if degree_u != 1 {
1586 return Ok(None);
1587 }
1588 let count_v = net[0].len();
1589 let mut columns = Vec::with_capacity(count_v);
1590 for column in 0..count_v {
1591 let row = net.iter().map(|row| row[column]).collect::<Vec<_>>();
1592 let Some((knots, points)) = raise(&surface.knots_u, &row) else {
1593 return Ok(None);
1594 };
1595 knots_u = knots;
1596 columns.push(points);
1597 }
1598 let count_u = columns[0].len();
1599 net = (0..count_u)
1600 .map(|row| columns.iter().map(|column| column[row]).collect())
1601 .collect();
1602 degree_u = 3;
1603 }
1604 Ok(Some(NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")?))
1605}
1606
1607fn halve_knot_spans(surface: &NurbsSurface, in_u: bool, in_v: bool) -> Result<NurbsSurface, KernelRefusal> {
1611 fn midpoints(knots: &[f64], degree: usize) -> Vec<f64> {
1612 let last = knots.len() - 1 - degree;
1613 knots[degree..=last]
1614 .windows(2)
1615 .filter(|pair| pair[1] - pair[0] > 1e-12)
1616 .map(|pair| 0.5 * (pair[0] + pair[1]))
1617 .collect()
1618 }
1619 insert_offset_fit_knots(surface,
1620 &if in_u { midpoints(&surface.knots_u, surface.degree_u) } else { Vec::new() },
1621 &if in_v { midpoints(&surface.knots_v, surface.degree_v) } else { Vec::new() })
1622}
1623
1624fn insert_offset_fit_knots(surface: &NurbsSurface, u: &[f64], v: &[f64]) -> Result<NurbsSurface, KernelRefusal> {
1629 let refine = |degree: usize, knots: &[f64], net: Vec<Vec<Vec4>>, inserted: &[f64]| -> Result<(Vec<f64>, Vec<Vec<Vec4>>), KernelRefusal> {
1630 let mut inserted = inserted.to_vec();
1631 inserted.sort_by(f64::total_cmp);
1632 inserted.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
1633 inserted.retain(|parameter| knots.iter().filter(|knot| (**knot - *parameter).abs() <= 1e-12).count() < degree);
1634 let mut refined_knots = knots.to_vec();
1635 let mut rows = Vec::with_capacity(net.len());
1636 for row in net {
1637 let mut curve = NurbsCurve::new(degree, knots.to_vec(), row).or_refuse(KernelStage::Refine, "new")?;
1638 for ¶meter in &inserted {
1639 curve = curve.insert_knot(parameter, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
1640 }
1641 refined_knots = curve.knots.clone();
1642 rows.push(curve.control_points);
1643 }
1644 Ok((refined_knots, rows))
1645 };
1646 let mut knots_u = surface.knots_u.clone();
1647 let mut knots_v = surface.knots_v.clone();
1648 let mut net = surface.control_points.clone();
1649 if !v.is_empty() {
1650 let (knots, rows) = refine(surface.degree_v, &knots_v, net, v)?;
1651 knots_v = knots;
1652 net = rows;
1653 }
1654 if !u.is_empty() {
1655 let count_v = net[0].len();
1657 let columns = (0..count_v)
1658 .map(|column| net.iter().map(|row| row[column]).collect::<Vec<_>>())
1659 .collect::<Vec<_>>();
1660 let (knots, refined) = refine(surface.degree_u, &knots_u, columns, u)?;
1661 knots_u = knots;
1662 let count_u = refined[0].len();
1663 net = (0..count_u)
1664 .map(|row| refined.iter().map(|column| column[row]).collect())
1665 .collect();
1666 }
1667 NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
1668}
1669
1670fn refine_offset_fit(
1706 source: &NurbsSurface,
1707 evaluator: &OffsetEvaluator,
1708 distance: f64,
1709 fitted: NurbsSurface,
1710) -> Result<NurbsSurface, KernelRefusal> {
1711 let scale = crate::model_scale(
1712 source
1713 .control_points
1714 .iter()
1715 .flatten()
1716 .filter_map(|point| point.point().ok()),
1717 );
1718 let bar = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
1719 let trace = std::env::var("BREP_OFFSET_FIT_TRACE").ok();
1720 let started = web_time::Instant::now();
1721 let enabled = std::env::var("BREP_OFFSET_FIT_REFINE").as_deref() != Ok("0");
1722 let Ok(initial) = offset_fit_deviation(&fitted, evaluator, distance) else {
1723 return Ok(fitted);
1724 };
1725 let mut best = fitted;
1726 let mut best_deviation = initial.clone();
1727 let mut refined_source = source.clone();
1728 let mut rounds = 0usize;
1729 let mut residual_mode = false;
1730 let mut stop = "under bar";
1731 if enabled {
1732 while best_deviation.worst() > bar {
1733 if rounds == OFFSET_FIT_REFINEMENTS {
1734 stop = "refinement limit";
1735 break;
1736 }
1737 let (along_u, along_v) = (best_deviation.along_u > bar, best_deviation.along_v > bar);
1738 let (in_u, in_v) = if along_u || along_v {
1739 (along_u, along_v)
1740 } else {
1741 (true, true)
1742 };
1743 let (raise_u, raise_v) = (in_u && refined_source.degree_u == 1, in_v && refined_source.degree_v == 1);
1744 let raised = if raise_u || raise_v {
1745 raise_linear_directions(&refined_source, raise_u, raise_v)?
1746 } else {
1747 None
1748 };
1749 let (halve_u, halve_v) = match raised {
1750 Some(_) => (in_u && !raise_u, in_v && !raise_v),
1751 None => (in_u, in_v),
1752 };
1753 let raised = raised.unwrap_or_else(|| refined_source.clone());
1754 let candidate_source = if halve_u || halve_v {
1755 if residual_mode {
1756 let (u, v) = best_deviation.refinement_sites(bar);
1757 insert_offset_fit_knots(&raised,
1758 if halve_u { &u } else { &[] }, if halve_v { &v } else { &[] })?
1759 } else {
1760 halve_knot_spans(&raised, halve_u, halve_v)?
1761 }
1762 } else {
1763 raised
1764 };
1765 let weights = candidate_source
1766 .control_points
1767 .iter()
1768 .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1769 .collect::<Vec<_>>();
1770 let controls = weights.len() * weights[0].len();
1771 let ceiling = if separable_weights(&weights).is_some() {
1772 OFFSET_FIT_MAX_CONTROLS_SEPARABLE
1773 } else {
1774 OFFSET_FIT_MAX_CONTROLS_TENSOR
1775 };
1776 if controls > ceiling {
1777 stop = "net ceiling";
1778 break;
1779 }
1780 let knot_u = KnotVector::new(candidate_source.knots_u.clone(), candidate_source.degree_u).or_refuse(KernelStage::Refine, "new")?;
1781 let knot_v = KnotVector::new(candidate_source.knots_v.clone(), candidate_source.degree_v).or_refuse(KernelStage::Refine, "new")?;
1782 let parameters_u = greville_parameters(&knot_u);
1783 let parameters_v = greville_parameters(&knot_v);
1784 let samples = parameters_u
1785 .iter()
1786 .map(|&u| {
1787 parameters_v
1788 .iter()
1789 .map(|&v| Ok(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1790 .collect::<Result<Vec<_>, KernelRefusal>>()
1791 })
1792 .collect::<Result<Vec<_>, KernelRefusal>>();
1793 let Ok(samples) = samples else {
1794 stop = "unevaluable sample";
1795 break;
1796 };
1797 let candidate = NurbsSurface::new(
1798 candidate_source.degree_u,
1799 candidate_source.degree_v,
1800 candidate_source.knots_u.clone(),
1801 candidate_source.knots_v.clone(),
1802 interpolate_tensor(&knot_u, &knot_v, ¶meters_u, ¶meters_v, &samples, &weights)?,
1803 ).or_refuse(KernelStage::Refine, "new")?;
1804 let Ok(deviation) = offset_fit_deviation(&candidate, evaluator, distance) else {
1805 stop = "unmeasurable round";
1806 break;
1807 };
1808 rounds += 1;
1809 if deviation.worst() >= best_deviation.worst() {
1810 let first_quadratic_split = rounds == 1
1817 && ((halve_u && source.degree_u == 2 && source.knots_u.len() == 6)
1818 || (halve_v && source.degree_v == 2 && source.knots_v.len() == 6));
1819 if first_quadratic_split {
1820 refined_source = candidate_source;
1821 continue;
1822 }
1823 if !residual_mode && best_deviation.worst() > offset_construction_band(scale)
1828 && std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0")
1829 {
1830 residual_mode = true;
1831 continue;
1832 }
1833 stop = "stopped improving";
1834 break;
1835 }
1836 best = candidate;
1837 best_deviation = deviation;
1838 refined_source = candidate_source;
1839 }
1840 } else if best_deviation.worst() > bar {
1841 stop = "refinement disabled";
1842 }
1843 let band = offset_construction_band(scale);
1848 let refused = enabled && best_deviation.worst() > band;
1849 if let Some(path) = trace {
1850 use std::io::Write;
1851 let caller = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
1852 let thread = std::thread::current().name().unwrap_or("-").to_string();
1853 let line = format!(
1854 "fit deg=({},{}) net=({},{})->({},{}) d={distance:.6} scale={scale:.4} bar={bar:.3e} \
1855 initial=(u {:.3e}, v {:.3e}, uv {:.3e}) final={:.3e} rounds={rounds} stop={stop} \
1856 refused={refused} seconds={:.4} thread={thread} args={caller}\n",
1857 source.degree_u,
1858 source.degree_v,
1859 source.control_points.len(),
1860 source.control_points[0].len(),
1861 best.control_points.len(),
1862 best.control_points[0].len(),
1863 initial.along_u,
1864 initial.along_v,
1865 initial.between,
1866 best_deviation.worst(),
1867 started.elapsed().as_secs_f64(),
1868 );
1869 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1870 let _ = file.write_all(line.as_bytes());
1871 }
1872 }
1873 if refused {
1874 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "offset_carrier_fit", format!(
1875 "offset_surface: the fitted offset carrier stays {:.3e} off its pointwise offset at \
1876 distance {distance} after {rounds} refinement round(s) ({stop}), past the offset \
1877 construction band {band:.3e}",
1878 best_deviation.worst()
1879 )));
1880 }
1881 Ok(best)
1882}
1883
1884pub(crate) fn unify_offset_sheet_bases(
1894 first: &NurbsSurface,
1895 second: &NurbsSurface,
1896) -> Result<(NurbsSurface, NurbsSurface), KernelRefusal> {
1897 fn raise_to(surface: &NurbsSurface, degree_u: usize, degree_v: usize) -> Result<NurbsSurface, KernelRefusal> {
1898 let raise_u = surface.degree_u == 1 && degree_u == 3;
1899 let raise_v = surface.degree_v == 1 && degree_v == 3;
1900 if !raise_u && !raise_v {
1901 return Ok(surface.clone());
1902 }
1903 raise_linear_directions(surface, raise_u, raise_v)?
1904 .ok_or_else(|| {
1905 KernelRefusal::internal(
1906 KernelStage::Refine,
1907 "offset_raise_linear",
1908 "offset sheets: a linear direction could not be raised",
1909 )
1910 })
1911 }
1912 let degree_u = first.degree_u.max(second.degree_u);
1913 let degree_v = first.degree_v.max(second.degree_v);
1914 let mut first = raise_to(first, degree_u, degree_v)?;
1915 let mut second = raise_to(second, degree_u, degree_v)?;
1916 if first.degree_u != second.degree_u || first.degree_v != second.degree_v {
1917 return Err(KernelRefusal::internal(KernelStage::Refine, "offset_common_basis", format!(
1918 "offset sheets: degrees ({},{}) and ({},{}) have no common basis",
1919 first.degree_u, first.degree_v, second.degree_u, second.degree_v
1920 )));
1921 }
1922 fn missing(from: &[f64], into: &[f64]) -> Vec<f64> {
1924 let mut extra = Vec::new();
1925 let mut index = 0usize;
1926 for &knot in from {
1927 while index < into.len() && into[index] < knot - 1e-12 {
1928 index += 1;
1929 }
1930 if index < into.len() && (into[index] - knot).abs() <= 1e-12 {
1931 index += 1;
1932 } else {
1933 extra.push(knot);
1934 }
1935 }
1936 extra
1937 }
1938 fn insert(surface: &NurbsSurface, knots: &[f64], along_u: bool) -> Result<NurbsSurface, KernelRefusal> {
1939 if knots.is_empty() {
1940 return Ok(surface.clone());
1941 }
1942 let (degree, vector) = if along_u {
1943 (surface.degree_u, &surface.knots_u)
1944 } else {
1945 (surface.degree_v, &surface.knots_v)
1946 };
1947 let lines = if along_u {
1948 (0..surface.control_points[0].len())
1949 .map(|column| surface.control_points.iter().map(|row| row[column]).collect::<Vec<_>>())
1950 .collect::<Vec<_>>()
1951 } else {
1952 surface.control_points.clone()
1953 };
1954 let mut refined_knots = vector.clone();
1955 let mut refined = Vec::with_capacity(lines.len());
1956 for line in lines {
1957 let mut curve = NurbsCurve::new(degree, vector.clone(), line).or_refuse(KernelStage::Refine, "new")?;
1958 for &knot in knots {
1959 curve = curve.insert_knot(knot, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
1960 }
1961 refined_knots = curve.knots.clone();
1962 refined.push(curve.control_points);
1963 }
1964 let net = if along_u {
1965 (0..refined[0].len())
1966 .map(|row| refined.iter().map(|column| column[row]).collect())
1967 .collect()
1968 } else {
1969 refined
1970 };
1971 let (knots_u, knots_v) = if along_u {
1972 (refined_knots, surface.knots_v.clone())
1973 } else {
1974 (surface.knots_u.clone(), refined_knots)
1975 };
1976 NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
1977 }
1978 let (to_first_u, to_second_u) = (
1979 missing(&second.knots_u, &first.knots_u),
1980 missing(&first.knots_u, &second.knots_u),
1981 );
1982 first = insert(&first, &to_first_u, true)?;
1983 second = insert(&second, &to_second_u, true)?;
1984 let (to_first_v, to_second_v) = (
1985 missing(&second.knots_v, &first.knots_v),
1986 missing(&first.knots_v, &second.knots_v),
1987 );
1988 first = insert(&first, &to_first_v, false)?;
1989 second = insert(&second, &to_second_v, false)?;
1990 Ok((first, second))
1991}
1992
1993fn carrier_edge_curve(
2015 surface: &NurbsSurface,
2016 pcurve: &NurbsCurve,
2017 points: &[Vec3],
2018 parameters: &[f64],
2019 fit_tolerance: f64,
2020) -> Result<(NurbsCurve, f64, f64), KernelRefusal> {
2021 let image = if std::env::var("BREP_CARRIER_POLYLINE").as_deref() == Ok("1") {
2023 Err("BREP_CARRIER_POLYLINE set".to_string())
2024 } else {
2025 crate::image_curve::image_curve(surface, pcurve, fit_tolerance, "offset_face_carrier")
2026 };
2027 match image {
2028 Ok(image) if image.t0 <= image.t1 => Ok((image.curve, image.t0, image.t1)),
2029 Ok(image) => {
2030 let [start, end] = image.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2033 let curve = image.curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
2034 Ok((curve, start + end - image.t0, start + end - image.t1))
2035 }
2036 Err(_) => {
2037 let curve = interpolate_curve(points, 1, parameters).or_refuse(KernelStage::Refine, "interpolate_curve")?;
2038 let [t0, t1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2039 Ok((curve, t0, t1))
2040 }
2041 }
2042}
2043
2044fn mapped_pcurve_polyline(
2045 surface: &NurbsSurface,
2046 pcurve: &NurbsCurve,
2047 degenerate: bool,
2048) -> Result<(Vec<Vec3>, Vec<f64>), KernelRefusal> {
2049 let [start, end] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2050 let evaluate = |fraction: f64| -> Result<Vec3, KernelRefusal> {
2051 let uv = pcurve.evaluate(start + (end - start) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
2052 surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")
2053 };
2054 let first = evaluate(0.0)?;
2055 let last = evaluate(1.0)?;
2056 if degenerate {
2057 return Ok((vec![first, last], vec![0.0, 1.0]));
2058 }
2059 fn append(
2060 evaluate: &impl Fn(f64) -> Result<Vec3, KernelRefusal>,
2061 a_fraction: f64,
2062 a: Vec3,
2063 b_fraction: f64,
2064 b: Vec3,
2065 depth: usize,
2066 parameters: &mut Vec<f64>,
2067 points: &mut Vec<Vec3>,
2068 ) -> Result<(), KernelRefusal> {
2069 let fractions =
2070 [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
2071 let samples = fractions
2072 .map(evaluate)
2073 .into_iter()
2074 .collect::<Result<Vec<_>, KernelRefusal>>()?;
2075 let deviation = samples
2076 .iter()
2077 .enumerate()
2078 .map(|(index, point)| {
2079 point
2080 .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
2081 .length()
2082 })
2083 .fold(0.0, f64::max);
2084 if deviation <= 5e-4 || depth >= 10 {
2085 parameters.push(b_fraction);
2086 points.push(b);
2087 return Ok(());
2088 }
2089 append(
2090 evaluate,
2091 a_fraction,
2092 a,
2093 fractions[1],
2094 samples[1],
2095 depth + 1,
2096 parameters,
2097 points,
2098 )?;
2099 append(
2100 evaluate,
2101 fractions[1],
2102 samples[1],
2103 b_fraction,
2104 b,
2105 depth + 1,
2106 parameters,
2107 points,
2108 )
2109 }
2110 let mut parameters = vec![0.0];
2111 let mut points = vec![first];
2112 append(
2113 &evaluate,
2114 0.0,
2115 first,
2116 1.0,
2117 last,
2118 0,
2119 &mut parameters,
2120 &mut points,
2121 )?;
2122 Ok((points, parameters))
2123}
2124
2125#[derive(Clone, Debug)]
2139pub struct CarrierDeviation {
2140 pub band: f64,
2143 pub lane: OffsetSurfaceLane,
2145 pub surface: Option<MeasuredTolerance>,
2147 pub edges: Vec<(u64, MeasuredTolerance)>,
2152 pub vertices: Vec<(u64, f64)>,
2156}
2157
2158impl CarrierDeviation {
2159 pub fn worst(&self) -> Option<MeasuredTolerance> {
2162 MeasuredTolerance::worst(
2163 self.surface
2164 .into_iter()
2165 .chain(self.edges.iter().map(|(_, measured)| *measured))
2166 .chain(
2167 self.vertices
2168 .iter()
2169 .map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
2170 ),
2171 )
2172 }
2173
2174 pub fn exceedances(&self) -> Vec<String> {
2177 let mut out = Vec::new();
2178 if let Some(surface) = self.surface {
2179 if surface.exceeds_band() {
2180 out.push(format!("carrier surface fit {}", surface.describe()));
2181 }
2182 }
2183 for (id, measured) in &self.edges {
2184 if measured.exceeds_band() {
2185 out.push(format!("edge {id} {}", measured.describe()));
2186 }
2187 }
2188 for (id, gap) in &self.vertices {
2189 let measured = MeasuredTolerance::new(*gap, self.band);
2190 if measured.exceeds_band() {
2191 out.push(format!("vertex {id} {}", measured.describe()));
2192 }
2193 }
2194 out
2195 }
2196}
2197
2198#[derive(Clone, Debug, Serialize)]
2199pub struct OffsetFaceCarrier {
2200 pub vertices: Vec<VertexRecord>,
2201 pub edges: Vec<EdgeRecord>,
2202 pub face: FaceRecord,
2203 #[serde(skip)]
2211 pub deviation: Option<CarrierDeviation>,
2212 #[serde(skip)]
2216 pub lane: OffsetSurfaceLane,
2217}
2218
2219fn claim_vertex_image(
2230 source_id: u64,
2231 point: Vec3,
2232 split: f64,
2233 vertex_images: &mut HashMap<u64, Vec<u64>>,
2234 vertices: &mut Vec<VertexRecord>,
2235 next_id: &mut u64,
2236) -> u64 {
2237 if let Some(ids) = vertex_images.get(&source_id) {
2238 let near = ids.iter().copied().find(|id| {
2239 vertices
2240 .iter()
2241 .find(|vertex| vertex.id == *id)
2242 .is_some_and(|vertex| vertex.point.sub(point).length() <= split)
2243 });
2244 if let Some(id) = near {
2245 return id;
2246 }
2247 }
2248 let id = *next_id;
2249 *next_id += 1;
2250 vertices.push(VertexRecord { id, point });
2251 vertex_images.entry(source_id).or_default().push(id);
2252 id
2253}
2254
2255pub fn offset_face_carrier(
2256 solid: &BrepSolid,
2257 face_id: u64,
2258 distance: f64,
2259 planar_extension: f64,
2260) -> Result<OffsetFaceCarrier, KernelRefusal> {
2261 offset_face_carrier_impl(
2262 solid,
2263 face_id,
2264 distance,
2265 &CarrierExtension::uniform(planar_extension),
2266 false,
2267 )
2268}
2269
2270pub fn offset_face_carrier_sided(
2272 solid: &BrepSolid,
2273 face_id: u64,
2274 distance: f64,
2275 extension: &CarrierExtension,
2276) -> Result<OffsetFaceCarrier, KernelRefusal> {
2277 offset_face_carrier_impl(solid, face_id, distance, extension, false)
2278}
2279
2280pub fn offset_face_carrier_measured(
2300 solid: &BrepSolid,
2301 face_id: u64,
2302 distance: f64,
2303 planar_extension: f64,
2304) -> Result<OffsetFaceCarrier, KernelRefusal> {
2305 offset_face_carrier_impl(
2306 solid,
2307 face_id,
2308 distance,
2309 &CarrierExtension::uniform(planar_extension),
2310 true,
2311 )
2312}
2313
2314fn measure_carrier(
2325 surface: &NurbsSurface,
2326 vertices: &[VertexRecord],
2327 edges: &[EdgeRecord],
2328 loops: &[LoopRecord],
2329 lane: OffsetSurfaceLane,
2330 surface_fit: Option<MeasuredTolerance>,
2331 band: f64,
2332) -> Result<CarrierDeviation, KernelRefusal> {
2333 let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
2334 let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
2338 for loop_record in loops {
2339 for coedge in &loop_record.coedges {
2340 let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
2341 continue;
2342 };
2343 let measured = measure_edge_against_pcurve_image(
2344 surface,
2345 &coedge.pcurve,
2346 edge,
2347 coedge.forward,
2348 band,
2349 )?;
2350 per_edge
2351 .entry(edge.id)
2352 .and_modify(|existing| *existing = existing.worse_of(measured))
2353 .or_insert(measured);
2354 }
2355 }
2356
2357 let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
2358 measured_edges.sort_by_key(|(id, _)| *id);
2359 let deviation_of: HashMap<u64, f64> = measured_edges
2360 .iter()
2361 .map(|(id, measured)| (*id, measured.deviation()))
2362 .collect();
2363
2364 let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
2368 for edge in edges {
2369 ends_at
2370 .entry(edge.start_vertex_id)
2371 .or_default()
2372 .push((edge, edge.t0));
2373 ends_at
2374 .entry(edge.end_vertex_id)
2375 .or_default()
2376 .push((edge, edge.t1));
2377 }
2378
2379 let mut measured_vertices = Vec::with_capacity(vertices.len());
2380 for vertex in vertices {
2381 let mut gaps = Vec::new();
2382 let mut incident = Vec::new();
2383 for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
2384 gaps.push(vertex_endpoint_gap(
2385 vertex.point,
2386 edge.curve.evaluate(*parameter).or_refuse(KernelStage::Refine, "evaluate")?,
2387 ));
2388 incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
2389 }
2390 measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
2391 }
2392
2393 Ok(CarrierDeviation {
2394 band,
2395 lane,
2396 surface: surface_fit,
2397 edges: measured_edges,
2398 vertices: measured_vertices,
2399 })
2400}
2401
2402fn offset_face_carrier_impl(
2403 solid: &BrepSolid,
2404 face_id: u64,
2405 distance: f64,
2406 extension: &CarrierExtension,
2407 measure: bool,
2408) -> Result<OffsetFaceCarrier, KernelRefusal> {
2409 let source = solid
2410 .shells
2411 .iter()
2412 .flat_map(|shell| &shell.faces)
2413 .find(|face| face.id == face_id)
2414 .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}")).or_refuse(KernelStage::Refine, "offset_missing_face")?;
2415 let band = offset_construction_band(solid_model_scale(solid));
2423 let fit_tolerance =
2430 crate::KernelTolerances::for_scale(solid_model_scale(solid), 1e-7).intersection_fit;
2431 let (surface, lane, surface_fit) = if measure {
2432 let measured = offset_surface_measured_sided(source, distance, extension, band)?;
2433 (measured.surface, measured.lane, measured.fit)
2434 } else {
2435 let (surface, lane) = offset_surface_with_lane(source, distance, extension)?;
2436 (surface, lane, None)
2437 };
2438 let source_edges = solid
2439 .edges
2440 .iter()
2441 .map(|edge| (edge.id, edge))
2442 .collect::<HashMap<_, _>>();
2443 let source_vertices = solid
2444 .vertices
2445 .iter()
2446 .map(|vertex| (vertex.id, vertex))
2447 .collect::<HashMap<_, _>>();
2448 let mut vertices = Vec::new();
2449 let mut vertex_images = HashMap::default();
2450 let vertex_split = (distance.abs() * 0.1).max(band);
2454 let mut edges = Vec::new();
2455 let mut edge_images = HashMap::default();
2456 let mut loops = Vec::new();
2457 let mut next_id = 1u64;
2458
2459 for source_loop in &source.loops {
2460 let mut coedges = Vec::new();
2461 for source_coedge in &source_loop.coedges {
2462 let source_edge = source_edges
2463 .get(&source_coedge.edge_id)
2464 .ok_or_else(|| "offset_face_carrier: missing source edge".to_string()).or_refuse(KernelStage::Refine, "offset_missing_edge")?;
2465 let (source_start, source_end) = if source_coedge.forward {
2466 (source_edge.start_vertex_id, source_edge.end_vertex_id)
2467 } else {
2468 (source_edge.end_vertex_id, source_edge.start_vertex_id)
2469 };
2470 if !source_vertices.contains_key(&source_start)
2471 || !source_vertices.contains_key(&source_end)
2472 {
2473 return Err(KernelRefusal::internal(KernelStage::Refine, "offset_missing_vertex", "offset_face_carrier: missing source vertex"));
2474 }
2475 let (points, parameters) =
2493 mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
2494 let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
2495 let image_collapsed = points
2496 .iter()
2497 .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
2498 let (edge_id, forward) =
2499 if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
2500 edge_images.get(&source_edge.id)
2501 {
2502 if source_start == source_end {
2503 (*edge_id, source_coedge.forward == *creator_forward)
2526 } else {
2527 (
2530 *edge_id,
2531 vertex_images
2532 .get(&source_start)
2533 .is_some_and(|ids: &Vec<u64>| ids.contains(edge_start_vertex_id)),
2534 )
2535 }
2536 } else {
2537 let (curve, t0, t1) = if image_collapsed {
2538 let curve = NurbsCurve::new(
2539 1,
2540 vec![0.0, 0.0, 1.0, 1.0],
2541 vec![
2542 Vec4::from_point(points[0], 1.0),
2543 Vec4::from_point(points[0], 1.0),
2544 ],
2545 ).or_refuse(KernelStage::Refine, "new")?;
2546 (curve, 0.0, 1.0)
2547 } else {
2548 carrier_edge_curve(
2549 &surface,
2550 &source_coedge.pcurve,
2551 &points,
2552 ¶meters,
2553 fit_tolerance,
2554 )?
2555 };
2556 let start_vertex_id = claim_vertex_image(
2557 source_start,
2558 points[0],
2559 vertex_split,
2560 &mut vertex_images,
2561 &mut vertices,
2562 &mut next_id,
2563 );
2564 let end_vertex_id = claim_vertex_image(
2565 source_end,
2566 points[points.len() - 1],
2567 vertex_split,
2568 &mut vertex_images,
2569 &mut vertices,
2570 &mut next_id,
2571 );
2572 let id = next_id;
2573 next_id += 1;
2574 edges.push(EdgeRecord {
2575 id,
2576 curve,
2577 t0,
2578 t1,
2579 start_vertex_id,
2580 end_vertex_id,
2581 degenerate: image_collapsed,
2595 name: source_edge
2599 .name
2600 .as_ref()
2601 .map(|name| format!("{name}_Offset")),
2602 });
2603 edge_images.insert(
2604 source_edge.id,
2605 (id, start_vertex_id, source_coedge.forward),
2606 );
2607 (id, true)
2608 };
2609 let id = next_id;
2610 next_id += 1;
2611 coedges.push(CoedgeRecord {
2612 id,
2613 edge_id,
2614 forward,
2615 pcurve: source_coedge.pcurve.clone(),
2616 });
2617 }
2618 let id = next_id;
2619 next_id += 1;
2620 loops.push(LoopRecord { id, coedges });
2621 }
2622 let deviation = if measure {
2623 Some(measure_carrier(
2624 &surface,
2625 &vertices,
2626 &edges,
2627 &loops,
2628 lane,
2629 surface_fit,
2630 band,
2631 )?)
2632 } else {
2633 None
2634 };
2635 Ok(OffsetFaceCarrier {
2636 vertices,
2637 edges,
2638 face: FaceRecord {
2639 id: next_id,
2640 surface,
2641 same_sense: source.same_sense,
2642 loops,
2643 name: source.name.as_ref().map(|name| format!("{name}_Offset")),
2644 },
2645 deviation,
2646 lane,
2647 })
2648}
2649
2650