1use crate::stl_conversion::{segmentation_options, StlConversionOptions};
20use crate::{
21 reconstruct_surface, AnalyticSurface, Mesh, RecognitionOptions, SurfaceHint, SurfaceType, Vec3,
22};
23use brep_kernel::{RegionCarrier, UNASSIGNED_REGION};
24use serde::{Deserialize, Serialize};
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
28#[serde(rename_all = "snake_case")]
29pub enum RegionKind {
30 Plane,
31 Cylinder,
32 Cone,
33 Sphere,
34 Torus,
35 Nurbs,
37 Faceted,
39}
40
41impl RegionKind {
42 pub fn label(self) -> &'static str {
43 match self {
44 Self::Plane => "plane",
45 Self::Cylinder => "cylinder",
46 Self::Cone => "cone",
47 Self::Sphere => "sphere",
48 Self::Torus => "torus",
49 Self::Nurbs => "NURBS surface",
50 Self::Faceted => "triangles",
51 }
52 }
53
54 fn of_carrier(carrier: &RegionCarrier) -> Self {
55 match carrier {
56 RegionCarrier::Plane { .. } => Self::Plane,
57 RegionCarrier::Cylinder { .. } => Self::Cylinder,
58 RegionCarrier::Cone { .. } => Self::Cone,
59 RegionCarrier::Sphere { .. } => Self::Sphere,
60 RegionCarrier::Torus { .. } => Self::Torus,
61 RegionCarrier::Freeform => Self::Faceted,
62 }
63 }
64}
65
66#[derive(Clone, Debug, Deserialize, Serialize)]
68pub struct ImportRegion {
69 pub id: u32,
71 pub triangles: usize,
73 pub area: f64,
74 pub kind: RegionKind,
76 pub carrier: RegionCarrier,
78 pub max_deviation: f64,
81 pub rms_deviation: f64,
82 pub max_normal_angle_deg: f64,
83 pub fit_bar: f64,
86 pub diagonal: f64,
88 pub reason: Option<String>,
91}
92
93#[derive(Clone, Debug, Default, Deserialize, Serialize)]
95pub struct ImportRegions {
96 pub triangle_regions: Vec<u32>,
99 pub regions: Vec<ImportRegion>,
100 pub fit_tolerance: f64,
102 pub normal_tolerance_deg: f64,
103}
104
105impl ImportRegions {
106 pub fn triangles_of(&self, id: u32) -> Vec<u32> {
108 self.triangle_regions
109 .iter()
110 .enumerate()
111 .filter(|(_, ®ion)| region == id)
112 .map(|(t, _)| t as u32)
113 .collect()
114 }
115}
116
117pub fn analyze_regions(
123 positions: &[f64],
124 indices: &[u32],
125 options: &StlConversionOptions,
126) -> Result<ImportRegions, String> {
127 let segment_options = segmentation_options(positions, options);
128 let segmentation = crate::segment_for_hybrid(positions, indices, &segment_options)?;
129 let triangle_count = indices.len() / 3;
130 if segmentation.triangle_region_ids.len() != triangle_count {
131 return Err(format!(
132 "the segmentation labelled {} triangles of {triangle_count}",
133 segmentation.triangle_region_ids.len()
134 ));
135 }
136 let regions = segmentation
137 .regions
138 .iter()
139 .filter(|region| region.triangle_count > 0)
140 .map(|region| {
141 let (low, high) = (region.bbox_min, region.bbox_max);
142 let diagonal = ((high.x - low.x).powi(2) + (high.y - low.y).powi(2) + (high.z - low.z).powi(2)).sqrt();
143 let fit_bar = segment_options.fit_tolerance * diagonal;
144 let kind = RegionKind::of_carrier(®ion.carrier);
145 let reason = (kind == RegionKind::Faceted).then(|| {
146 if region.triangle_count < segment_options.min_region_triangles {
147 format!(
148 "too few triangles to fit ({} < {})",
149 region.triangle_count, segment_options.min_region_triangles
150 )
151 } else {
152 format!(
153 "no plane, cylinder, cone, sphere or torus within {} mm ({:.3}% of the region size) \
154 and {:.1}° of the triangle normals",
155 short(fit_bar),
156 segment_options.fit_tolerance * 100.0,
157 segment_options.normal_tolerance_deg
158 )
159 }
160 });
161 ImportRegion {
162 id: region.id,
163 triangles: region.triangle_count,
164 area: region.area,
165 kind,
166 carrier: region.carrier.clone(),
167 max_deviation: region.max_deviation,
168 rms_deviation: region.rms_deviation,
169 max_normal_angle_deg: region.max_normal_angle_deg,
170 fit_bar,
171 diagonal,
172 reason,
173 }
174 })
175 .collect();
176 Ok(ImportRegions {
177 triangle_regions: segmentation.triangle_region_ids,
178 regions,
179 fit_tolerance: segment_options.fit_tolerance,
180 normal_tolerance_deg: segment_options.normal_tolerance_deg,
181 })
182}
183
184fn short(value: f64) -> String {
186 if value == 0.0 || !value.is_finite() {
187 return format!("{value}");
188 }
189 let digits = (2 - value.abs().log10().floor() as i32).max(0) as usize;
190 format!("{value:.digits$}")
191}
192
193#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Deserialize, Serialize)]
195#[serde(rename_all = "snake_case")]
196pub enum FitKind {
197 #[default]
199 Auto,
200 Plane,
201 Cylinder,
202 Cone,
203 Sphere,
204 Torus,
205 Nurbs,
206}
207
208impl FitKind {
209 pub const ALL: [FitKind; 7] = [
210 FitKind::Auto,
211 FitKind::Plane,
212 FitKind::Cylinder,
213 FitKind::Cone,
214 FitKind::Sphere,
215 FitKind::Torus,
216 FitKind::Nurbs,
217 ];
218
219 pub fn label(self) -> &'static str {
220 match self {
221 Self::Auto => "Best fit",
222 Self::Plane => "Plane",
223 Self::Cylinder => "Cylinder",
224 Self::Cone => "Cone",
225 Self::Sphere => "Sphere",
226 Self::Torus => "Torus",
227 Self::Nurbs => "NURBS surface",
228 }
229 }
230
231 fn surface_type(self) -> Option<SurfaceType> {
232 match self {
233 Self::Plane => Some(SurfaceType::Plane),
234 Self::Cylinder => Some(SurfaceType::Cylinder),
235 Self::Cone => Some(SurfaceType::Cone),
236 Self::Sphere => Some(SurfaceType::Sphere),
237 Self::Torus => Some(SurfaceType::Torus),
238 Self::Auto | Self::Nurbs => None,
239 }
240 }
241}
242
243#[derive(Clone, Debug, Deserialize, Serialize)]
245pub struct RegionFitRequest {
246 pub triangles: Vec<u32>,
248 pub kind: FitKind,
249 pub tolerance: f64,
251 pub normal_tolerance_deg: f64,
254}
255
256#[derive(Clone, Debug, Deserialize, Serialize)]
258pub struct FitAttempt {
259 pub kind: RegionKind,
260 pub max_deviation: Option<f64>,
262 pub max_normal_angle_deg: Option<f64>,
263 pub within_tolerance: bool,
264 pub note: Option<String>,
265}
266
267#[derive(Clone, Debug, Deserialize, Serialize)]
269pub struct RegionFit {
270 pub kind: Option<RegionKind>,
273 pub within_tolerance: bool,
274 pub max_deviation: f64,
275 pub rms_deviation: f64,
276 pub max_normal_angle_deg: f64,
277 pub tolerance: f64,
278 pub surface: Option<AnalyticSurface>,
279 pub nurbs: Option<NurbsFit>,
280 pub attempts: Vec<FitAttempt>,
282 pub message: String,
284}
285
286fn region_mesh(positions: &[f64], indices: &[u32], triangles: &[u32]) -> Result<Mesh, String> {
288 let vertex_count = positions.len() / 3;
289 let mut map = std::collections::HashMap::<u32, u32>::new();
290 let mut vertices = Vec::new();
291 let mut local = Vec::with_capacity(triangles.len());
292 for &t in triangles {
293 let t = t as usize;
294 if 3 * t + 2 >= indices.len() {
295 return Err(format!("triangle {t} is outside the mesh"));
296 }
297 let mut corners = [0u32; 3];
298 for (k, corner) in corners.iter_mut().enumerate() {
299 let v = indices[3 * t + k];
300 if v as usize >= vertex_count {
301 return Err(format!("triangle {t} names vertex {v} outside the mesh"));
302 }
303 *corner = *map.entry(v).or_insert_with(|| {
304 let p = &positions[3 * v as usize..3 * v as usize + 3];
305 vertices.push(Vec3::new(p[0], p[1], p[2]));
306 (vertices.len() - 1) as u32
307 });
308 }
309 local.push(corners);
310 }
311 if local.is_empty() {
312 return Err("the region has no triangles".into());
313 }
314 Ok(Mesh::new(vertices, local))
315}
316
317fn triangle_normal(mesh: &Mesh, triangle: [u32; 3]) -> Option<(Vec3, Vec3)> {
318 let [a, b, c] = triangle.map(|v| mesh.vertices[v as usize]);
319 let normal = (b - a).cross(c - a).normalized()?;
320 Some(((a + b + c) * (1.0 / 3.0), normal))
321}
322
323fn analytic_deviation(mesh: &Mesh, surface: AnalyticSurface) -> (f64, f64, f64) {
326 let mut max = 0.0f64;
327 let mut sum = 0.0;
328 for &p in &mesh.vertices {
329 let d = surface.signed_distance(p).abs();
330 max = max.max(d);
331 sum += d * d;
332 }
333 let rms = (sum / mesh.vertices.len().max(1) as f64).sqrt();
334 let mut angle = 0.0f64;
335 for &triangle in &mesh.triangles {
336 let Some((centroid, normal)) = triangle_normal(mesh, triangle) else { continue };
337 if let Some(surface_normal) = surface.normal_at(centroid) {
338 let cos = normal.dot(surface_normal).abs().min(1.0);
339 angle = angle.max(cos.acos().to_degrees());
340 }
341 }
342 (max, rms, angle)
343}
344
345fn mesh_diagonal(mesh: &Mesh) -> f64 {
346 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
347 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
348 for p in &mesh.vertices {
349 low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
350 high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
351 }
352 (high - low).length()
353}
354
355fn fit_analytic(mesh: &Mesh, kind: SurfaceType) -> Result<AnalyticSurface, String> {
359 let diagonal = mesh_diagonal(mesh).max(f64::MIN_POSITIVE);
360 let mut options = RecognitionOptions::default();
361 options.distance_tolerance = diagonal;
362 options.relative_tolerance = 0.0;
363 options.normal_tolerance = 89.0f64.to_radians();
364 options.minimum_support = 1;
365 options.minimum_support_area = 0.0;
366 options.deterministic_seed = Some(0);
367 options.respect_features = false;
368 options.collect_phase_timings = false;
369 options.threads = 1;
370 options.sampling = crate::SamplingMode::Vertices;
373 let triangles: Vec<usize> = (0..mesh.triangles.len()).collect();
374 reconstruct_surface(mesh, &triangles, &SurfaceHint::KnownType { surface_type: kind }, &options)
375 .map(|fit| fit.surface)
376 .map_err(|error| error.to_string())
377}
378
379fn kind_of(surface_type: SurfaceType) -> RegionKind {
380 match surface_type {
381 SurfaceType::Plane => RegionKind::Plane,
382 SurfaceType::Cylinder => RegionKind::Cylinder,
383 SurfaceType::Cone => RegionKind::Cone,
384 SurfaceType::Sphere => RegionKind::Sphere,
385 SurfaceType::Torus => RegionKind::Torus,
386 }
387}
388
389pub fn fit_region(positions: &[f64], indices: &[u32], request: &RegionFitRequest) -> RegionFit {
391 let mut fit = RegionFit {
392 kind: None,
393 within_tolerance: false,
394 max_deviation: f64::INFINITY,
395 rms_deviation: f64::INFINITY,
396 max_normal_angle_deg: 0.0,
397 tolerance: request.tolerance,
398 surface: None,
399 nurbs: None,
400 attempts: Vec::new(),
401 message: String::new(),
402 };
403 if !(request.tolerance.is_finite() && request.tolerance > 0.0) {
404 fit.message = "the tolerance must be a positive distance".into();
405 return fit;
406 }
407 let mesh = match region_mesh(positions, indices, &request.triangles) {
408 Ok(mesh) => mesh,
409 Err(error) => {
410 fit.message = error;
411 return fit;
412 }
413 };
414 let analytic: Vec<SurfaceType> = match request.kind {
415 FitKind::Auto => vec![
416 SurfaceType::Plane,
417 SurfaceType::Cylinder,
418 SurfaceType::Cone,
419 SurfaceType::Sphere,
420 SurfaceType::Torus,
421 ],
422 FitKind::Nurbs => Vec::new(),
423 kind => vec![kind.surface_type().unwrap()],
424 };
425 let mut best: Option<(bool, f64)> = None;
427 for surface_type in analytic {
428 let kind = kind_of(surface_type);
429 match fit_analytic(&mesh, surface_type) {
430 Ok(surface) => {
431 let (max, rms, angle) = analytic_deviation(&mesh, surface);
432 let within = max <= request.tolerance && angle <= request.normal_tolerance_deg;
433 fit.attempts.push(FitAttempt {
434 kind,
435 max_deviation: Some(max),
436 max_normal_angle_deg: Some(angle),
437 within_tolerance: within,
438 note: None,
439 });
440 let better = match best {
441 None => true,
442 Some((best_within, best_max)) => (within && !best_within) || (within == best_within && !best_within && max < best_max),
443 };
444 if better {
445 best = Some((within, max));
446 fit.kind = Some(kind);
447 fit.surface = Some(surface);
448 fit.max_deviation = max;
449 fit.rms_deviation = rms;
450 fit.max_normal_angle_deg = angle;
451 fit.within_tolerance = within;
452 }
453 if within && request.kind == FitKind::Auto {
455 break;
456 }
457 }
458 Err(error) => fit.attempts.push(FitAttempt {
459 kind,
460 max_deviation: None,
461 max_normal_angle_deg: None,
462 within_tolerance: false,
463 note: Some(error),
464 }),
465 }
466 }
467 let try_nurbs = request.kind == FitKind::Nurbs || (request.kind == FitKind::Auto && !fit.within_tolerance);
468 if try_nurbs {
469 match fit_nurbs_region(&mesh, request.tolerance) {
470 Ok(nurbs) => {
471 let within = nurbs.max_deviation <= request.tolerance;
472 fit.attempts.push(FitAttempt {
473 kind: RegionKind::Nurbs,
474 max_deviation: Some(nurbs.max_deviation),
475 max_normal_angle_deg: None,
476 within_tolerance: within,
477 note: Some(format!("{} × {} control points", nurbs.count_u, nurbs.count_v)),
478 });
479 let better = match best {
480 None => true,
481 Some((best_within, best_max)) => (within && !best_within) || (within == best_within && !best_within && nurbs.max_deviation < best_max),
482 } || request.kind == FitKind::Nurbs;
483 if better {
484 fit.kind = Some(RegionKind::Nurbs);
485 fit.surface = None;
486 fit.max_deviation = nurbs.max_deviation;
487 fit.rms_deviation = nurbs.rms_deviation;
488 fit.max_normal_angle_deg = 0.0;
489 fit.within_tolerance = within;
490 fit.nurbs = Some(nurbs);
491 }
492 }
493 Err(error) => fit.attempts.push(FitAttempt {
494 kind: RegionKind::Nurbs,
495 max_deviation: None,
496 max_normal_angle_deg: None,
497 within_tolerance: false,
498 note: Some(error),
499 }),
500 }
501 }
502 fit.message = match fit.kind {
503 None => {
504 let notes = fit
505 .attempts
506 .iter()
507 .filter_map(|attempt| attempt.note.as_ref().map(|note| format!("{}: {note}", attempt.kind.label())))
508 .collect::<Vec<_>>();
509 format!("No surface could be fitted. {}", notes.join("; "))
510 }
511 Some(kind) if fit.within_tolerance => format!(
512 "Fits as a {} — largest deviation {} mm (tolerance {} mm).",
513 kind.label(),
514 short(fit.max_deviation),
515 short(request.tolerance)
516 ),
517 Some(kind) => {
518 let normal = kind != RegionKind::Nurbs
519 && fit.max_deviation <= request.tolerance
520 && fit.max_normal_angle_deg > request.normal_tolerance_deg;
521 if normal {
522 format!(
523 "Closest is a {}, within {} mm, but its normals differ from the triangles' by {:.2}° (limit {:.2}°).",
524 kind.label(),
525 short(fit.max_deviation),
526 fit.max_normal_angle_deg,
527 request.normal_tolerance_deg
528 )
529 } else {
530 format!(
531 "Does not fit: the closest is a {} at {} mm, over the {} mm tolerance.",
532 kind.label(),
533 short(fit.max_deviation),
534 short(request.tolerance)
535 )
536 }
537 }
538 };
539 fit
540}
541
542#[derive(Clone, Debug, Deserialize, Serialize)]
544pub struct NurbsFit {
545 pub degree: usize,
546 pub count_u: usize,
547 pub count_v: usize,
548 pub knots_u: Vec<f64>,
549 pub knots_v: Vec<f64>,
550 pub control_points: Vec<[f64; 3]>,
552 pub max_deviation: f64,
555 pub rms_deviation: f64,
556}
557
558impl NurbsFit {
559 pub fn to_kernel_surface(&self) -> Result<brep_kernel::NurbsSurface, String> {
561 let rows = (0..self.count_u)
562 .map(|i| {
563 (0..self.count_v)
564 .map(|j| {
565 let p = self.control_points[i * self.count_v + j];
566 brep_kernel::Vec4::from_point(brep_kernel::Vec3::new(p[0], p[1], p[2]), 1.0)
567 })
568 .collect()
569 })
570 .collect();
571 brep_kernel::NurbsSurface::new(
572 self.degree,
573 self.degree,
574 self.knots_u.clone(),
575 self.knots_v.clone(),
576 rows,
577 )
578 }
579
580 pub fn evaluate(&self, u: f64, v: f64) -> [f64; 3] {
582 let (span_u, basis_u) = basis(&self.knots_u, self.degree, self.count_u, u);
583 let (span_v, basis_v) = basis(&self.knots_v, self.degree, self.count_v, v);
584 let mut point = [0.0; 3];
585 for (a, bu) in basis_u.iter().enumerate() {
586 for (b, bv) in basis_v.iter().enumerate() {
587 let p = self.control_points[(span_u - self.degree + a) * self.count_v + span_v - self.degree + b];
588 for k in 0..3 {
589 point[k] += bu * bv * p[k];
590 }
591 }
592 }
593 point
594 }
595}
596
597fn clamped_knots(count: usize, degree: usize) -> Vec<f64> {
599 let interior = count - degree - 1;
600 let mut knots = vec![0.0; degree + 1];
601 knots.extend((1..=interior).map(|k| k as f64 / (interior + 1) as f64));
602 knots.extend(std::iter::repeat(1.0).take(degree + 1));
603 knots
604}
605
606fn basis(knots: &[f64], degree: usize, count: usize, t: f64) -> (usize, Vec<f64>) {
609 let t = t.clamp(knots[degree], knots[count]);
610 let span = if t >= knots[count] {
611 count - 1
612 } else {
613 let mut low = degree;
614 let mut high = count;
615 while high - low > 1 {
616 let mid = (low + high) / 2;
617 if t < knots[mid] {
618 high = mid;
619 } else {
620 low = mid;
621 }
622 }
623 low
624 };
625 let mut values = vec![0.0; degree + 1];
626 let mut left = vec![0.0; degree + 1];
627 let mut right = vec![0.0; degree + 1];
628 values[0] = 1.0;
629 for j in 1..=degree {
630 left[j] = t - knots[span + 1 - j];
631 right[j] = knots[span + j] - t;
632 let mut saved = 0.0;
633 for r in 0..j {
634 let denominator = right[r + 1] + left[j - r];
635 let temp = if denominator == 0.0 { 0.0 } else { values[r] / denominator };
636 values[r] = saved + right[r + 1] * temp;
637 saved = left[j - r] * temp;
638 }
639 values[j] = saved;
640 }
641 (span, values)
642}
643
644fn principal_frame(points: &[Vec3]) -> (Vec3, [Vec3; 3]) {
647 let n = points.len().max(1) as f64;
648 let centroid = points.iter().fold(Vec3::new(0.0, 0.0, 0.0), |sum, &p| sum + p) * (1.0 / n);
649 let mut c = [[0.0f64; 3]; 3];
650 for &p in points {
651 let d = [p.x - centroid.x, p.y - centroid.y, p.z - centroid.z];
652 for i in 0..3 {
653 for j in 0..3 {
654 c[i][j] += d[i] * d[j];
655 }
656 }
657 }
658 let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
659 for _ in 0..50 {
660 let (mut p, mut q, mut largest) = (0, 1, 0.0f64);
661 for i in 0..3 {
662 for j in i + 1..3 {
663 if c[i][j].abs() > largest {
664 largest = c[i][j].abs();
665 p = i;
666 q = j;
667 }
668 }
669 }
670 if largest <= 1e-300 {
671 break;
672 }
673 let theta = 0.5 * (2.0 * c[p][q]).atan2(c[q][q] - c[p][p]);
674 let (s, co) = theta.sin_cos();
675 let mut next = c;
676 for k in 0..3 {
677 next[k][p] = co * c[k][p] - s * c[k][q];
678 next[k][q] = s * c[k][p] + co * c[k][q];
679 }
680 let rotated = next;
681 for k in 0..3 {
682 next[p][k] = co * rotated[p][k] - s * rotated[q][k];
683 next[q][k] = s * rotated[p][k] + co * rotated[q][k];
684 }
685 c = next;
686 for row in &mut v {
687 let (a, b) = (row[p], row[q]);
688 row[p] = co * a - s * b;
689 row[q] = s * a + co * b;
690 }
691 }
692 let mut order = [0usize, 1, 2];
693 order.sort_by(|&a, &b| c[b][b].total_cmp(&c[a][a]));
694 let axis = |k: usize| Vec3::new(v[0][k], v[1][k], v[2][k]);
695 (centroid, [axis(order[0]), axis(order[1]), axis(order[2])])
696}
697
698fn cholesky_solve(mut matrix: Vec<f64>, n: usize, rhs: &mut [Vec<f64>]) -> Result<(), String> {
701 for j in 0..n {
702 let mut diagonal = matrix[j * n + j];
703 for k in 0..j {
704 diagonal -= matrix[j * n + k] * matrix[j * n + k];
705 }
706 if !(diagonal > 0.0) {
707 return Err("the least-squares system is singular".into());
708 }
709 let diagonal = diagonal.sqrt();
710 matrix[j * n + j] = diagonal;
711 for i in j + 1..n {
712 let mut value = matrix[i * n + j];
713 for k in 0..j {
714 value -= matrix[i * n + k] * matrix[j * n + k];
715 }
716 matrix[i * n + j] = value / diagonal;
717 }
718 }
719 for b in rhs.iter_mut() {
720 for i in 0..n {
721 let mut value = b[i];
722 for k in 0..i {
723 value -= matrix[i * n + k] * b[k];
724 }
725 b[i] = value / matrix[i * n + i];
726 }
727 for i in (0..n).rev() {
728 let mut value = b[i];
729 for k in i + 1..n {
730 value -= matrix[k * n + i] * b[k];
731 }
732 b[i] = value / matrix[i * n + i];
733 }
734 }
735 Ok(())
736}
737
738const NURBS_NETS: [usize; 9] = [4, 5, 6, 8, 10, 12, 16, 20, 24];
741const NURBS_DEGREE: usize = 3;
742
743pub fn fit_nurbs_region(mesh: &Mesh, tolerance: f64) -> Result<NurbsFit, String> {
752 let points = &mesh.vertices;
753 if points.len() < 4 {
754 return Err("a NURBS surface needs at least four vertices".into());
755 }
756 let (centroid, [e1, e2, e3]) = principal_frame(points);
757 let uv: Vec<[f64; 2]> = points
758 .iter()
759 .map(|&p| {
760 let d = p - centroid;
761 [d.dot(e1), d.dot(e2)]
762 })
763 .collect();
764 let mut positive = 0.0;
768 let mut negative = 0.0;
769 for triangle in &mesh.triangles {
770 let [a, b, c] = triangle.map(|v| uv[v as usize]);
771 let area = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
772 if area > 0.0 {
773 positive += area;
774 } else {
775 negative -= area;
776 }
777 }
778 let total = positive + negative;
779 let surface_area: f64 = mesh
780 .triangles
781 .iter()
782 .map(|triangle| {
783 let [a, b, c] = triangle.map(|v| points[v as usize]);
784 0.5 * (b - a).cross(c - a).length()
785 })
786 .sum();
787 if total <= 1e-3 * 2.0 * surface_area || positive.min(negative) > 1e-3 * total {
790 return Err(
791 "the region folds over itself seen from its best-fit plane (a tube or a deep cup); \
792 split it, or fit it as a cylinder, cone, sphere or torus"
793 .into(),
794 );
795 }
796 let _ = e3;
797 let (mut low, mut high) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
798 for p in &uv {
799 for k in 0..2 {
800 low[k] = low[k].min(p[k]);
801 high[k] = high[k].max(p[k]);
802 }
803 }
804 let span = [(high[0] - low[0]).max(f64::MIN_POSITIVE), (high[1] - low[1]).max(f64::MIN_POSITIVE)];
805 let params: Vec<[f64; 2]> = uv
806 .iter()
807 .map(|p| [(p[0] - low[0]) / span[0], (p[1] - low[1]) / span[1]])
808 .collect();
809 let mut last: Option<NurbsFit> = None;
810 for &net in &NURBS_NETS {
811 let fit = nurbs_least_squares(points, ¶ms, net, net)?;
812 let done = fit.max_deviation <= tolerance;
813 last = Some(fit);
814 if done {
815 break;
816 }
817 }
818 Ok(last.unwrap())
819}
820
821fn nurbs_least_squares(points: &[Vec3], params: &[[f64; 2]], count_u: usize, count_v: usize) -> Result<NurbsFit, String> {
822 let degree = NURBS_DEGREE.min(count_u - 1).min(count_v - 1);
823 let knots_u = clamped_knots(count_u, degree);
824 let knots_v = clamped_knots(count_v, degree);
825 let n = count_u * count_v;
826 let mut normal = vec![0.0; n * n];
827 let mut rhs = vec![vec![0.0; n]; 3];
828 let mut rows = Vec::with_capacity(points.len());
829 for (point, param) in points.iter().zip(params) {
830 let (span_u, basis_u) = basis(&knots_u, degree, count_u, param[0]);
831 let (span_v, basis_v) = basis(&knots_v, degree, count_v, param[1]);
832 let mut row = Vec::with_capacity((degree + 1) * (degree + 1));
833 for (a, bu) in basis_u.iter().enumerate() {
834 for (b, bv) in basis_v.iter().enumerate() {
835 row.push(((span_u - degree + a) * count_v + span_v - degree + b, bu * bv));
836 }
837 }
838 for &(i, wi) in &row {
839 for &(j, wj) in &row {
840 normal[i * n + j] += wi * wj;
841 }
842 rhs[0][i] += wi * point.x;
843 rhs[1][i] += wi * point.y;
844 rhs[2][i] += wi * point.z;
845 }
846 rows.push(row);
847 }
848 let trace = (0..n).map(|i| normal[i * n + i]).sum::<f64>() / n as f64;
851 let weight = 1e-6 * trace.max(1e-12);
852 let mut add_difference = |cells: [usize; 3]| {
853 let coefficients = [1.0, -2.0, 1.0];
854 for (a, &i) in cells.iter().enumerate() {
855 for (b, &j) in cells.iter().enumerate() {
856 normal[i * n + j] += weight * coefficients[a] * coefficients[b];
857 }
858 }
859 };
860 for i in 0..count_u {
861 for j in 1..count_v - 1 {
862 add_difference([i * count_v + j - 1, i * count_v + j, i * count_v + j + 1]);
863 }
864 }
865 for j in 0..count_v {
866 for i in 1..count_u - 1 {
867 add_difference([(i - 1) * count_v + j, i * count_v + j, (i + 1) * count_v + j]);
868 }
869 }
870 for i in 0..n {
872 normal[i * n + i] += 1e-12 * trace.max(1e-12);
873 }
874 cholesky_solve(normal, n, &mut rhs)?;
875 let control_points: Vec<[f64; 3]> = (0..n).map(|i| [rhs[0][i], rhs[1][i], rhs[2][i]]).collect();
876 let mut max = 0.0f64;
877 let mut sum = 0.0;
878 for (point, row) in points.iter().zip(&rows) {
879 let mut s = [0.0; 3];
880 for &(i, w) in row {
881 for k in 0..3 {
882 s[k] += w * control_points[i][k];
883 }
884 }
885 let d = Vec3::new(s[0], s[1], s[2]).distance(*point);
886 max = max.max(d);
887 sum += d * d;
888 }
889 Ok(NurbsFit {
890 degree,
891 count_u,
892 count_v,
893 knots_u,
894 knots_v,
895 control_points,
896 max_deviation: max,
897 rms_deviation: (sum / points.len() as f64).sqrt(),
898 })
899}
900
901#[path = "import_session/nurbs_face.rs"]
902mod nurbs_face;
903pub use nurbs_face::{apply_nurbs_faces, NurbsFaceChoice, NurbsFaceOutcome};
904
905fn principal_frame_kernel(points: &[brep_kernel::Vec3]) -> (brep_kernel::Vec3, [brep_kernel::Vec3; 3]) {
907 let converted: Vec<Vec3> = points.iter().map(|p| Vec3::new(p.x, p.y, p.z)).collect();
908 let (c, axes) = principal_frame(&converted);
909 let k = |v: Vec3| brep_kernel::Vec3::new(v.x, v.y, v.z);
910 (k(c), axes.map(k))
911}
912
913pub fn apply_region_choices(
919 step_text: &str,
920 positions: &[f64],
921 indices: &[u32],
922 choices: &[NurbsFaceChoice],
923) -> Result<(String, Vec<NurbsFaceOutcome>), String> {
924 let mut solids = brep_kernel::import_step(step_text)?;
925 if solids.len() != 1 {
926 let reason = format!("the import holds {} bodies; NURBS faces are placed on single-body imports", solids.len());
927 return Ok((step_text.to_string(), choices.iter().map(|_| NurbsFaceOutcome { applied: false, max_deviation: None, control_points: None, reason: Some(reason.clone()) }).collect()));
928 }
929 let outcomes = apply_nurbs_faces(&mut solids[0], positions, indices, choices);
930 if !outcomes.iter().any(|outcome| outcome.applied) {
931 return Ok((step_text.to_string(), outcomes));
932 }
933 let report = brep_kernel::export_step_report(&solids, "Imported mesh", "MM", "")?;
934 let text = report.text;
935 let back = brep_kernel::import_step(&text)?;
937 if back.len() != 1 || !back[0].validate().is_empty() {
938 return Err("the STEP written with the NURBS faces does not read back as one valid body".into());
939 }
940 Ok((text, outcomes))
941}
942
943pub const UNASSIGNED: u32 = UNASSIGNED_REGION;
945