1use crate::{KernelRefusal, KernelStage, RefusalClass};
28use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, VertexRecord};
29use crate::{
30 build_pcurve_on_surface_range, intersect_analytic_pair, make_arc, make_line, make_plane,
31 make_revolution, project_point_to_curve, solid_model_scale, NurbsCurve, NurbsSurface, Vec3,
32};
33use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
34use serde::{Deserialize, Serialize};
35use std::f64::consts::{PI, TAU};
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
39#[serde(rename_all = "lowercase")]
40pub enum RefitSurfaceKind {
41 Auto,
43 Plane,
44 Cylinder,
45 Cone,
46 Sphere,
47 Torus,
48}
49
50impl RefitSurfaceKind {
51 pub fn parse(text: &str) -> Option<Self> {
53 match text.trim().to_ascii_lowercase().as_str() {
54 "" | "auto" => Some(Self::Auto),
55 "plane" | "planar" => Some(Self::Plane),
56 "cylinder" | "cylindrical" => Some(Self::Cylinder),
57 "cone" | "conical" => Some(Self::Cone),
58 "sphere" | "spherical" => Some(Self::Sphere),
59 "torus" | "toroidal" => Some(Self::Torus),
60 _ => None,
61 }
62 }
63
64 pub fn name(self) -> &'static str {
65 match self {
66 Self::Auto => "auto",
67 Self::Plane => "plane",
68 Self::Cylinder => "cylinder",
69 Self::Cone => "cone",
70 Self::Sphere => "sphere",
71 Self::Torus => "torus",
72 }
73 }
74}
75
76pub mod refit_refusal_phrases {
79 pub const NOT_EDGE_CONNECTED: &str = "not edge-connected";
80 pub const RESIDUAL_OVER_TOLERANCE: &str = "fit residual";
81 pub const NOT_DISK_OR_ANNULUS: &str = "not a disk or an annulus";
82 pub const NO_EXACT_BOUNDARY: &str = "no exact boundary";
83 pub const NO_FIT: &str = "no surface fits";
84 pub const RESULT_INVALID: &str = "result does not validate";
85 pub const FACETS_TOO_COARSE: &str = "too coarse";
86 pub const CLOSED_SHELL_KIND: &str = "only a sphere or a torus closes on itself";
87 pub const CLOSED_SHELL_GENUS: &str = "does not match the shell's topology";
88}
89use refit_refusal_phrases::*;
90
91#[derive(Clone, Copy, Debug, Serialize)]
93#[serde(tag = "type", rename_all = "lowercase")]
94pub enum RefitCarrier {
95 Plane { origin: Vec3, normal: Vec3 },
96 Cylinder { axis_origin: Vec3, axis: Vec3, radius: f64 },
97 Cone { apex: Vec3, axis: Vec3, half_angle: f64 },
98 Sphere { center: Vec3, radius: f64 },
99 Torus { center: Vec3, axis: Vec3, major_radius: f64, minor_radius: f64 },
100}
101
102impl RefitCarrier {
103 pub fn kind(&self) -> RefitSurfaceKind {
104 match self {
105 Self::Plane { .. } => RefitSurfaceKind::Plane,
106 Self::Cylinder { .. } => RefitSurfaceKind::Cylinder,
107 Self::Cone { .. } => RefitSurfaceKind::Cone,
108 Self::Sphere { .. } => RefitSurfaceKind::Sphere,
109 Self::Torus { .. } => RefitSurfaceKind::Torus,
110 }
111 }
112
113 pub fn distance(&self, point: Vec3) -> f64 {
115 match *self {
116 Self::Plane { origin, normal } => point.sub(origin).dot(normal).abs(),
117 Self::Cylinder { axis_origin, axis, radius } => {
118 let rel = point.sub(axis_origin);
119 (rel.sub(axis.scale(rel.dot(axis))).length() - radius).abs()
120 }
121 Self::Cone { apex, axis, half_angle } => {
122 let rel = point.sub(apex);
123 let along = rel.dot(axis);
124 let radial = rel.sub(axis.scale(along)).length();
125 let (s, c) = half_angle.sin_cos();
127 let normal_offset = radial * c - along * s;
128 let foot = along * c + radial * s;
129 if foot >= 0.0 {
130 normal_offset.abs()
131 } else {
132 rel.length()
133 }
134 }
135 Self::Sphere { center, radius } => (point.sub(center).length() - radius).abs(),
136 Self::Torus { center, axis, major_radius, minor_radius } => {
137 let rel = point.sub(center);
138 let along = rel.dot(axis);
139 let radial = rel.sub(axis.scale(along)).length();
140 ((radial - major_radius).hypot(along) - minor_radius).abs()
141 }
142 }
143 }
144
145 pub fn describe(&self) -> String {
147 let v = |p: Vec3| format!("({:.6}, {:.6}, {:.6})", p.x, p.y, p.z);
148 match *self {
149 Self::Plane { origin, normal } => {
150 format!("plane through {} normal {}", v(origin), v(normal))
151 }
152 Self::Cylinder { axis_origin, axis, radius } => format!(
153 "cylinder radius {radius:.6} axis {} through {}",
154 v(axis),
155 v(axis_origin)
156 ),
157 Self::Cone { apex, axis, half_angle } => format!(
158 "cone half-angle {:.4} deg apex {} axis {}",
159 half_angle.to_degrees(),
160 v(apex),
161 v(axis)
162 ),
163 Self::Sphere { center, radius } => {
164 format!("sphere radius {radius:.6} center {}", v(center))
165 }
166 Self::Torus { center, axis, major_radius, minor_radius } => format!(
167 "torus radii {major_radius:.6}/{minor_radius:.6} center {} axis {}",
168 v(center),
169 v(axis)
170 ),
171 }
172 }
173}
174
175#[derive(Clone, Debug, Serialize)]
177pub struct RefitReport {
178 pub carrier: RefitCarrier,
179 pub rms_residual: f64,
181 pub max_residual: f64,
183 pub tolerance: f64,
184 pub samples: usize,
185 pub replaced_faces: usize,
186 pub boundary_edges_before: usize,
188 pub boundary_edges_after: usize,
189 pub boundary_loops: usize,
190 pub new_face_id: u64,
191}
192
193fn refuse_input(what: &str, message: String) -> KernelRefusal {
194 KernelRefusal::input(KernelStage::Collect, what, format!("Refit Faces: {message}"))
195}
196
197fn refuse_unsupported(stage: KernelStage, what: &str, message: String) -> KernelRefusal {
198 KernelRefusal::unsupported(stage, what, format!("Refit Faces: {message}"))
199}
200
201fn face_label(face: &FaceRecord) -> String {
202 face.name.clone().unwrap_or_else(|| format!("face {}", face.id))
203}
204
205#[derive(Clone, Copy, Debug)]
206struct BoundaryUse {
207 edge_id: u64,
208 start: u64,
209 end: u64,
210 forward: bool,
211 neighbour: usize,
213}
214
215struct Chain {
217 uses: Vec<BoundaryUse>,
218 closed: bool,
219 neighbour: usize,
220}
221
222struct BuiltEdge {
225 curve: NurbsCurve,
226 t0: f64,
227 t1: f64,
228 start: u64,
229 end: u64,
230 region_forward: bool,
231 region_pcurve: Option<NurbsCurve>,
234}
235
236pub fn refit_faces_as_one(
240 solid: &BrepSolid,
241 face_ids: &[u64],
242 kind: RefitSurfaceKind,
243 tolerance: f64,
244) -> Result<(BrepSolid, RefitReport), KernelRefusal> {
245 let (result, report) = refit_faces_as_one_impl(solid, face_ids, kind, tolerance)?;
246 let result = crate::accept_sound(result, "refitFaces")?;
247 Ok((result, report))
248}
249
250fn refit_faces_as_one_impl(
251 solid: &BrepSolid,
252 face_ids: &[u64],
253 kind: RefitSurfaceKind,
254 tolerance: f64,
255) -> Result<(BrepSolid, RefitReport), KernelRefusal> {
256 if !(tolerance.is_finite() && tolerance > 0.0) {
257 return Err(refuse_input(
258 "tolerance",
259 format!("tolerance must be a positive number, got {tolerance}"),
260 ));
261 }
262 let mut unique: Vec<u64> = Vec::new();
263 for id in face_ids {
264 if !unique.contains(id) {
265 unique.push(*id);
266 }
267 }
268 if unique.is_empty() {
269 return Err(refuse_input("face_ids", "no faces selected".into()));
270 }
271 let mut shell_index = None;
273 let mut selected_indices: Vec<usize> = Vec::new();
274 for id in &unique {
275 let found = solid.shells.iter().enumerate().find_map(|(s, shell)| {
276 shell.faces.iter().position(|face| face.id == *id).map(|f| (s, f))
277 });
278 let Some((s, f)) = found else {
279 return Err(refuse_input("face_ids", format!("no face with id {id}")));
280 };
281 if *shell_index.get_or_insert(s) != s {
282 return Err(refuse_input(
283 "face_ids",
284 format!("the selected faces are {NOT_EDGE_CONNECTED}: they lie on different shells"),
285 ));
286 }
287 selected_indices.push(f);
288 }
289 let shell_index = shell_index.unwrap_or(0);
290 let shell = &solid.shells[shell_index];
291 let selected: HashSet<usize> = selected_indices.iter().copied().collect();
292 let scale = solid_model_scale(solid).max(1e-9);
293 let fit_tol = (scale * 1e-7).max(1e-9);
294
295 let edge_by_id: HashMap<u64, &EdgeRecord> =
296 solid.edges.iter().map(|edge| (edge.id, edge)).collect();
297 let vertex_by_id: HashMap<u64, Vec3> = solid
298 .vertices
299 .iter()
300 .map(|vertex| (vertex.id, vertex.point))
301 .collect();
302
303 let mut uses: HashMap<u64, Vec<(usize, usize, usize)>> = HashMap::default();
305 for (f, face) in shell.faces.iter().enumerate() {
306 for (l, lp) in face.loops.iter().enumerate() {
307 for (c, coedge) in lp.coedges.iter().enumerate() {
308 uses.entry(coedge.edge_id).or_default().push((f, l, c));
309 }
310 }
311 }
312
313 {
315 let mut seen: HashSet<usize> = HashSet::default();
316 let mut stack = vec![selected_indices[0]];
317 seen.insert(selected_indices[0]);
318 while let Some(f) = stack.pop() {
319 for lp in &shell.faces[f].loops {
320 for coedge in &lp.coedges {
321 for &(g, _, _) in uses.get(&coedge.edge_id).into_iter().flatten() {
322 if selected.contains(&g) && seen.insert(g) {
323 stack.push(g);
324 }
325 }
326 }
327 }
328 }
329 if seen.len() != selected.len() {
330 let stray: Vec<String> = selected_indices
331 .iter()
332 .filter(|f| !seen.contains(f))
333 .map(|&f| face_label(&shell.faces[f]))
334 .collect();
335 return Err(refuse_input(
336 "face_ids",
337 format!(
338 "the selected faces are {NOT_EDGE_CONNECTED}: {} {} no edge with the rest",
339 stray.join(", "),
340 if stray.len() == 1 { "shares" } else { "share" }
341 ),
342 ));
343 }
344 }
345
346 let mut internal_edges: HashSet<u64> = HashSet::default();
348 let mut boundary: Vec<BoundaryUse> = Vec::new();
349 let mut region_vertices: HashSet<u64> = HashSet::default();
350 let mut loop_total = 0usize;
351 for &f in &selected_indices {
352 loop_total += shell.faces[f].loops.len();
353 for lp in &shell.faces[f].loops {
354 for coedge in &lp.coedges {
355 let edge = edge_by_id.get(&coedge.edge_id).ok_or_else(|| {
356 refuse_input("solid", format!("coedge names missing edge {}", coedge.edge_id))
357 })?;
358 region_vertices.insert(edge.start_vertex_id);
359 region_vertices.insert(edge.end_vertex_id);
360 let edge_uses = &uses[&coedge.edge_id];
361 let outside: Vec<usize> = edge_uses
362 .iter()
363 .map(|(g, _, _)| *g)
364 .filter(|g| !selected.contains(g))
365 .collect();
366 if outside.is_empty() {
367 internal_edges.insert(coedge.edge_id);
368 continue;
369 }
370 if edge_uses.len() != 2 || outside.len() != 1 {
371 return Err(refuse_unsupported(
372 KernelStage::Classify,
373 "non_manifold_boundary",
374 format!(
375 "edge {} of the selection is used by {} faces; the region is \
376 {NOT_DISK_OR_ANNULUS}",
377 coedge.edge_id,
378 edge_uses.len()
379 ),
380 ));
381 }
382 let (start, end) = if coedge.forward {
383 (edge.start_vertex_id, edge.end_vertex_id)
384 } else {
385 (edge.end_vertex_id, edge.start_vertex_id)
386 };
387 boundary.push(BoundaryUse {
388 edge_id: coedge.edge_id,
389 start,
390 end,
391 forward: coedge.forward,
392 neighbour: outside[0],
393 });
394 }
395 }
396 }
397 if boundary.is_empty() {
398 let euler = region_vertices.len() as i64 - internal_edges.len() as i64
401 + (2 * selected_indices.len() as i64 - loop_total as i64);
402 return refit_closed_shell(
403 solid,
404 shell_index,
405 &selected_indices,
406 euler,
407 kind,
408 tolerance,
409 scale,
410 &edge_by_id,
411 &vertex_by_id,
412 );
413 }
414
415 let mut outgoing: HashMap<u64, usize> = HashMap::default();
417 for (index, use_) in boundary.iter().enumerate() {
418 if outgoing.insert(use_.start, index).is_some() {
419 return Err(refuse_unsupported(
420 KernelStage::Classify,
421 "pinched_region",
422 format!(
423 "the region's boundary touches itself at a vertex; the region is \
424 {NOT_DISK_OR_ANNULUS}"
425 ),
426 ));
427 }
428 }
429 let mut cycles: Vec<Vec<BoundaryUse>> = Vec::new();
430 let mut visited = vec![false; boundary.len()];
431 for first in 0..boundary.len() {
432 if visited[first] {
433 continue;
434 }
435 let mut cycle = Vec::new();
436 let mut at = first;
437 loop {
438 if visited[at] {
439 break;
440 }
441 visited[at] = true;
442 cycle.push(boundary[at]);
443 match outgoing.get(&boundary[at].end) {
444 Some(&next) => at = next,
445 None => {
446 return Err(refuse_unsupported(
447 KernelStage::Classify,
448 "open_boundary",
449 format!("the region's boundary does not close; it is {NOT_DISK_OR_ANNULUS}"),
450 ))
451 }
452 }
453 }
454 if at != first {
455 return Err(refuse_unsupported(
456 KernelStage::Classify,
457 "open_boundary",
458 format!("the region's boundary does not close; it is {NOT_DISK_OR_ANNULUS}"),
459 ));
460 }
461 cycles.push(cycle);
462 }
463 let boundary_edges: HashSet<u64> = boundary.iter().map(|b| b.edge_id).collect();
465 let euler = region_vertices.len() as i64
466 - (internal_edges.len() + boundary_edges.len()) as i64
467 + (2 * selected_indices.len() as i64 - loop_total as i64);
468 if !(cycles.len() == 1 || cycles.len() == 2) || euler != 2 - cycles.len() as i64 {
469 return Err(refuse_unsupported(
470 KernelStage::Classify,
471 "region_topology",
472 format!(
473 "the selected region has {} boundary loop(s) and Euler characteristic {euler}; \
474 it is {NOT_DISK_OR_ANNULUS}, which one face can take",
475 cycles.len()
476 ),
477 ));
478 }
479
480 let samples = fit_samples(shell, &selected_indices, &edge_by_id, &vertex_by_id)?;
482 let fitted = fit_carrier(&samples, kind, tolerance, scale)?;
483 let carrier = fitted.carrier;
484 let (rms_residual, max_residual) = residuals(&carrier, &samples.points);
485 if max_residual > tolerance {
486 return Err(KernelRefusal::ill_posed(
487 KernelStage::Refine,
488 "refit_residual",
489 format!(
490 "Refit Faces: the {} {RESIDUAL_OVER_TOLERANCE} {max_residual:.6} (rms \
491 {rms_residual:.6}) is over the tolerance {tolerance}; these faces are not \
492 one {} within it",
493 carrier.kind().name(),
494 carrier.kind().name()
495 ),
496 ));
497 }
498
499 coarse_facet_gate(&carrier, shell, &selected_indices, &edge_by_id, &vertex_by_id)?;
500
501 let surface = carrier_surface(&carrier, &samples.points, scale)?;
504
505 let same_sense = region_same_sense(&surface, &samples)?;
507
508 let mut chains_per_cycle: Vec<Vec<Chain>> = Vec::new();
510 for cycle in &cycles {
511 chains_per_cycle.push(split_chains(cycle));
512 }
513
514 let chain_tol = (tolerance * 2.0).max(fit_tol * 10.0);
516 let mut next_edge_id = solid.edges.iter().map(|e| e.id).max().unwrap_or(0) + 1;
517 let mut next_vertex_id = solid.vertices.iter().map(|v| v.id).max().unwrap_or(0) + 1;
518 let mut moved_vertices: HashMap<u64, Vec3> = HashMap::default();
519 let mut new_vertices: Vec<VertexRecord> = Vec::new();
520
521 let mut chain_curves: Vec<Vec<NurbsCurve>> = Vec::new();
523 for chains in &chains_per_cycle {
524 for chain in chains {
525 let neighbour = &shell.faces[chain.neighbour];
526 let points = chain_points(chain, &vertex_by_id);
527 let mut curves = intersect_analytic_pair(&surface, &neighbour.surface, fit_tol)
531 .unwrap_or_default();
532 if curves.is_empty() {
536 curves = iso_curve_on(&surface, &neighbour.surface, &points, chain_tol);
537 }
538 if curves.is_empty() {
539 return Err(refuse_unsupported(
540 KernelStage::Intersect,
541 "no_exact_boundary",
542 format!(
543 "{NO_EXACT_BOUNDARY} with {}: its carrier is not one whose \
544 intersection with the fitted {} is known in closed form",
545 face_label(neighbour),
546 carrier.kind().name()
547 ),
548 ));
549 }
550 let mut best: Option<(f64, NurbsCurve)> = None;
551 for curve in curves {
552 let mut worst = 0.0_f64;
553 for point in &points {
554 let d = project_point_to_curve(&curve, *point)
555 .map(|p| p.distance)
556 .unwrap_or(f64::INFINITY);
557 worst = worst.max(d);
558 }
559 if best.as_ref().map_or(true, |(w, _)| worst < *w) {
560 best = Some((worst, curve));
561 }
562 }
563 match best {
564 Some((worst, curve)) if worst <= chain_tol => chain_curves.push(vec![curve]),
565 Some((worst, _)) => {
566 return Err(refuse_unsupported(
567 KernelStage::Intersect,
568 "no_exact_boundary",
569 format!(
570 "{NO_EXACT_BOUNDARY} with {}: the shared edges sit {worst:.6} off \
571 the curve where the fitted {} meets it (allowed {chain_tol:.6})",
572 face_label(neighbour),
573 carrier.kind().name()
574 ),
575 ))
576 }
577 None => {
578 return Err(refuse_unsupported(
579 KernelStage::Intersect,
580 "no_exact_boundary",
581 format!(
582 "{NO_EXACT_BOUNDARY} with {}: the fitted {} does not meet it",
583 face_label(neighbour),
584 carrier.kind().name()
585 ),
586 ))
587 }
588 }
589 }
590 }
591
592 {
595 let mut flat = 0usize;
596 for chains in &chains_per_cycle {
597 let base = flat;
598 let count = chains.len();
599 for index in 0..count {
600 let chain = &chains[index];
601 flat += 1;
602 if chain.closed {
603 continue;
604 }
605 let next = (index + 1) % count;
606 let corner = chain.uses.last().expect("chain has uses").end;
607 let original = vertex_by_id[&corner];
608 let first = &chain_curves[base + index][0];
609 let second = &chain_curves[base + next][0];
610 let (point, gap) = curve_meeting(first, second, original)?;
611 if gap > chain_tol {
612 return Err(refuse_unsupported(
613 KernelStage::Intersect,
614 "corner_gap",
615 format!(
616 "{NO_EXACT_BOUNDARY}: at a corner the rebuilt edges along {} and \
617 {} miss each other by {gap:.6}",
618 face_label(&shell.faces[chain.neighbour]),
619 face_label(&shell.faces[chains[next].neighbour])
620 ),
621 ));
622 }
623 moved_vertices.insert(corner, point);
624 }
625 }
626 }
627
628 let mut built: Vec<Vec<BuiltEdge>> = Vec::new();
629 {
630 let mut flat = 0usize;
631 for chains in &chains_per_cycle {
632 let mut row = Vec::new();
633 for chain in chains {
634 let curve = chain_curves[flat][0].clone();
635 flat += 1;
636 let points = chain_trace_points(chain, &vertex_by_id, &edge_by_id)?;
637 let edge = if chain.closed {
638 if let Some((curve, pcurve, region_forward)) =
639 iso_ring(&surface, &curve, &points, chain_tol)
640 {
641 let [t0, t1] = curve.domain().map_err(|e| refuse_input("curve", e))?;
642 let point = curve.evaluate(t0).map_err(|e| refuse_input("curve", e))?;
643 let id = next_vertex_id;
644 next_vertex_id += 1;
645 new_vertices.push(VertexRecord { id, point });
646 row.push(BuiltEdge {
647 curve,
648 t0,
649 t1,
650 start: id,
651 end: id,
652 region_forward,
653 region_pcurve: Some(pcurve),
654 });
655 continue;
656 }
657 let start_point = points[0];
658 let (curve, region_forward) = closed_ring_curve(&curve, &points, scale)?;
659 let start_point = curve.evaluate(curve.domain().map_err(|e| {
660 refuse_input("curve", e)
661 })?[0])
662 .unwrap_or(start_point);
663 let id = next_vertex_id;
664 next_vertex_id += 1;
665 new_vertices.push(VertexRecord { id, point: start_point });
666 let [t0, t1] = curve.domain().map_err(|e| refuse_input("curve", e))?;
667 BuiltEdge { curve, t0, t1, start: id, end: id, region_forward, region_pcurve: None }
668 } else {
669 let start = chain.uses.first().unwrap().start;
670 let end = chain.uses.last().unwrap().end;
671 let start_point = moved_vertices.get(&start).copied().unwrap_or(points[0]);
672 let end_point =
673 moved_vertices.get(&end).copied().unwrap_or(*points.last().unwrap());
674 let middle = points[points.len() / 2];
675 let mid_point = if points.len() > 2 {
676 middle
677 } else {
678 points[0].add(points[1]).scale(0.5)
679 };
680 open_arc(&curve, start, end, start_point, end_point, mid_point, scale)?
681 };
682 row.push(edge);
683 }
684 built.push(row);
685 }
686 }
687
688 let mut out = solid.clone();
690 let mut dropped_edges: HashSet<u64> = internal_edges.clone();
692 dropped_edges.extend(boundary_edges.iter().copied());
693 let mut new_edges: Vec<EdgeRecord> = Vec::new();
694 let mut new_edge_ids: Vec<Vec<u64>> = Vec::new();
695 for row in &built {
696 let mut ids = Vec::new();
697 for edge in row {
698 let id = next_edge_id;
699 next_edge_id += 1;
700 new_edges.push(EdgeRecord {
701 id,
702 curve: edge.curve.clone(),
703 t0: edge.t0,
704 t1: edge.t1,
705 start_vertex_id: edge.start,
706 end_vertex_id: edge.end,
707 degenerate: false,
708 name: None,
709 });
710 ids.push(id);
711 }
712 new_edge_ids.push(ids);
713 }
714
715 for vertex in &mut out.vertices {
718 if let Some(point) = moved_vertices.get(&vertex.id) {
719 vertex.point = *point;
720 }
721 }
722 let point_of = |out: &BrepSolid, id: u64| -> Vec3 {
723 out.vertices
724 .iter()
725 .find(|v| v.id == id)
726 .map(|v| v.point)
727 .or_else(|| new_vertices.iter().find(|v| v.id == id).map(|v| v.point))
728 .unwrap_or(Vec3::new(0.0, 0.0, 0.0))
729 };
730 let mut restraightened: HashSet<u64> = HashSet::default();
731 let snapshot = out.clone();
732 for edge in &mut out.edges {
733 if dropped_edges.contains(&edge.id) {
734 continue;
735 }
736 let touches = moved_vertices.contains_key(&edge.start_vertex_id)
737 || moved_vertices.contains_key(&edge.end_vertex_id);
738 if !touches {
739 continue;
740 }
741 if edge.curve.degree == 1 && edge.curve.control_points.len() == 2 {
742 let a = point_of(&snapshot, edge.start_vertex_id);
743 let b = point_of(&snapshot, edge.end_vertex_id);
744 if let Ok(line) = make_line(a, b) {
745 edge.curve = line;
746 edge.t0 = 0.0;
747 edge.t1 = 1.0;
748 restraightened.insert(edge.id);
749 }
750 }
751 }
752
753 for (cycle_index, chains) in chains_per_cycle.iter().enumerate() {
759 for (chain_index, chain) in chains.iter().enumerate() {
760 if !chain.closed {
761 continue;
762 }
763 let neighbour = &out.shells[shell_index].faces[chain.neighbour];
764 let chain_edges: HashSet<u64> = chain.uses.iter().map(|u| u.edge_id).collect();
765 let whole_boundary = neighbour.loops.len() == 1
766 && neighbour.loops[0].coedges.iter().all(|c| chain_edges.contains(&c.edge_id));
767 if !whole_boundary || !neighbour.surface.is_affine().unwrap_or(false) {
768 continue;
769 }
770 let ring = &built[cycle_index][chain_index].curve;
771 if let Some(plane) = reseated_plane(&neighbour.surface, ring, scale) {
772 out.shells[shell_index].faces[chain.neighbour].surface = plane;
773 }
774 }
775 }
776
777 let shell_out = &mut out.shells[shell_index];
778 let mut flat = 0usize;
779 for (cycle_index, chains) in chains_per_cycle.iter().enumerate() {
780 for (chain_index, chain) in chains.iter().enumerate() {
781 let built_edge = &built[cycle_index][chain_index];
782 let edge_id = new_edge_ids[cycle_index][chain_index];
783 flat += 1;
784 let neighbour = &mut shell_out.faces[chain.neighbour];
785 let chain_edges: HashSet<u64> = chain.uses.iter().map(|u| u.edge_id).collect();
786 let coedge_forward = !built_edge.region_forward;
787 let pcurve = build_pcurve_on_surface_range(
788 &neighbour.surface,
789 &built_edge.curve,
790 built_edge.t0,
791 built_edge.t1,
792 coedge_forward,
793 fit_tol,
794 )
795 .map_err(|e| {
796 refuse_unsupported(
797 KernelStage::Refine,
798 "neighbour_pcurve",
799 format!(
800 "{NO_EXACT_BOUNDARY}: no trim curve on {} for the rebuilt edge ({e})",
801 face_label(neighbour)
802 ),
803 )
804 })?;
805 let mut replaced = false;
806 for lp in &mut neighbour.loops {
807 let positions: Vec<usize> = lp
808 .coedges
809 .iter()
810 .enumerate()
811 .filter(|(_, c)| chain_edges.contains(&c.edge_id))
812 .map(|(i, _)| i)
813 .collect();
814 if positions.is_empty() {
815 continue;
816 }
817 if positions.len() != chain_edges.len() {
818 return Err(refuse_unsupported(
819 KernelStage::Sew,
820 "neighbour_loop",
821 format!(
822 "the edges shared with {} are split across its loops",
823 face_label(neighbour)
824 ),
825 ));
826 }
827 let n = lp.coedges.len();
829 let run_start = (0..n)
830 .find(|&i| {
831 chain_edges.contains(&lp.coedges[i].edge_id)
832 && !chain_edges.contains(&lp.coedges[(i + n - 1) % n].edge_id)
833 })
834 .unwrap_or(0);
835 lp.coedges.rotate_left(run_start);
836 if !lp.coedges[..positions.len()]
837 .iter()
838 .all(|c| chain_edges.contains(&c.edge_id))
839 {
840 return Err(refuse_unsupported(
841 KernelStage::Sew,
842 "neighbour_loop",
843 format!(
844 "the edges shared with {} are not one run of its loop",
845 face_label(neighbour)
846 ),
847 ));
848 }
849 let first_id = lp.coedges[0].id;
850 lp.coedges.splice(
851 0..positions.len(),
852 [CoedgeRecord { id: first_id, edge_id, forward: coedge_forward, pcurve }],
853 );
854 replaced = true;
855 break;
856 }
857 if !replaced {
858 return Err(refuse_unsupported(
859 KernelStage::Sew,
860 "neighbour_loop",
861 format!("{} does not use the edges it shares", face_label(neighbour)),
862 ));
863 }
864 }
865 }
866 let _ = flat;
867
868 if !restraightened.is_empty() {
870 let edges: HashMap<u64, EdgeRecord> = out
871 .edges
872 .iter()
873 .filter(|e| restraightened.contains(&e.id))
874 .map(|e| (e.id, e.clone()))
875 .collect();
876 for face in &mut out.shells[shell_index].faces {
877 let surface = face.surface.clone();
878 for lp in &mut face.loops {
879 for coedge in &mut lp.coedges {
880 if let Some(edge) = edges.get(&coedge.edge_id) {
881 coedge.pcurve = build_pcurve_on_surface_range(
882 &surface,
883 &edge.curve,
884 edge.t0,
885 edge.t1,
886 coedge.forward,
887 fit_tol,
888 )
889 .map_err(|e| {
890 refuse_unsupported(
891 KernelStage::Refine,
892 "neighbour_pcurve",
893 format!("a straight edge at a moved corner lost its trim ({e})"),
894 )
895 })?;
896 }
897 }
898 }
899 }
900 }
901
902 let keeper = &shell.faces[selected_indices[0]];
904 let mut next_coedge_id = out
905 .shells
906 .iter()
907 .flat_map(|s| s.faces.iter())
908 .flat_map(|f| f.loops.iter())
909 .flat_map(|l| l.coedges.iter().map(|c| c.id))
910 .max()
911 .unwrap_or(0)
912 + 1;
913 let mut next_loop_id = out
914 .shells
915 .iter()
916 .flat_map(|s| s.faces.iter())
917 .flat_map(|f| f.loops.iter().map(|l| l.id))
918 .max()
919 .unwrap_or(0)
920 + 1;
921 let mut loops = Vec::new();
922 for (cycle_index, row) in built.iter().enumerate() {
923 let mut coedges = Vec::new();
924 for (chain_index, edge) in row.iter().enumerate() {
925 let pcurve = match &edge.region_pcurve {
926 Some(pcurve) => Ok(pcurve.clone()),
927 None => build_pcurve_on_surface_range(
928 &surface,
929 &edge.curve,
930 edge.t0,
931 edge.t1,
932 edge.region_forward,
933 fit_tol,
934 ),
935 }
936 .map_err(|e| {
937 refuse_unsupported(
938 KernelStage::Refine,
939 "region_pcurve",
940 format!("{NO_EXACT_BOUNDARY}: no trim curve on the fitted surface ({e})"),
941 )
942 })?;
943 coedges.push(CoedgeRecord {
944 id: next_coedge_id,
945 edge_id: new_edge_ids[cycle_index][chain_index],
946 forward: edge.region_forward,
947 pcurve,
948 });
949 next_coedge_id += 1;
950 }
951 loops.push(LoopRecord { id: next_loop_id, coedges });
952 next_loop_id += 1;
953 }
954 let new_face = FaceRecord {
955 id: keeper.id,
956 surface: surface.clone(),
957 same_sense,
958 loops,
959 name: keeper.name.clone(),
960 };
961 let new_face_id = new_face.id;
962 let shell_out = &mut out.shells[shell_index];
963 let keep_at = selected_indices[0];
964 let mut faces = Vec::with_capacity(shell_out.faces.len());
965 for (index, face) in shell_out.faces.drain(..).enumerate() {
966 if index == keep_at {
967 faces.push(new_face.clone());
968 } else if !selected.contains(&index) {
969 faces.push(face);
970 }
971 }
972 shell_out.faces = faces;
973 out.edges.retain(|edge| !dropped_edges.contains(&edge.id));
974 out.edges.extend(new_edges);
975 out.vertices.extend(new_vertices);
976 let used: HashSet<u64> = out
978 .edges
979 .iter()
980 .flat_map(|e| [e.start_vertex_id, e.end_vertex_id])
981 .collect();
982 out.vertices.retain(|v| used.contains(&v.id));
983
984 let issues = out.validate();
985 if !issues.is_empty() {
986 let first: Vec<String> = issues.iter().take(3).map(|i| format!("{i:?}")).collect();
987 return Err(KernelRefusal::new(
988 RefusalClass::InvalidResultTopology {
989 issues: issues.len() as u32,
990 },
991 KernelStage::Validate,
992 format!(
993 "Refit Faces: the {RESULT_INVALID} ({} issue(s): {})",
994 issues.len(),
995 first.join("; ")
996 ),
997 ));
998 }
999
1000 let report = RefitReport {
1001 carrier,
1002 rms_residual,
1003 max_residual,
1004 tolerance,
1005 samples: samples.points.len(),
1006 replaced_faces: selected_indices.len(),
1007 boundary_edges_before: boundary_edges.len(),
1008 boundary_edges_after: built.iter().map(Vec::len).sum(),
1009 boundary_loops: built.len(),
1010 new_face_id,
1011 };
1012 Ok((out, report))
1013}
1014
1015struct Samples {
1020 points: Vec<Vec3>,
1023 mesh_vertices: Vec<Vec3>,
1026 mesh_triangles: Vec<[u32; 3]>,
1027 fit_vertices: Vec<usize>,
1031 oriented: Vec<(Vec3, Vec3)>,
1034}
1035
1036fn fit_samples(
1037 shell: &crate::topology::ShellRecord,
1038 selected: &[usize],
1039 edges: &HashMap<u64, &EdgeRecord>,
1040 vertices: &HashMap<u64, Vec3>,
1041) -> Result<Samples, KernelRefusal> {
1042 let mut welded: HashMap<(i64, i64, i64), u32> = HashMap::default();
1043 let mut mesh_vertices: Vec<Vec3> = Vec::new();
1044 let mut mesh_triangles: Vec<[u32; 3]> = Vec::new();
1045 let mut points: Vec<Vec3> = Vec::new();
1046 let mut oriented = Vec::new();
1047 let extent = vertices
1048 .values()
1049 .fold(0.0_f64, |m, p| m.max(p.x.abs()).max(p.y.abs()).max(p.z.abs()))
1050 .max(1.0);
1051 let quantum = extent * 1e-9;
1052 let mut weld = |p: Vec3, mesh_vertices: &mut Vec<Vec3>| -> u32 {
1053 let key = (
1054 (p.x / quantum).round() as i64,
1055 (p.y / quantum).round() as i64,
1056 (p.z / quantum).round() as i64,
1057 );
1058 *welded.entry(key).or_insert_with(|| {
1059 mesh_vertices.push(p);
1060 (mesh_vertices.len() - 1) as u32
1061 })
1062 };
1063 let mut support: HashMap<u64, Vec<Vec3>> = HashMap::default();
1068 for &f in selected {
1069 let face = &shell.faces[f];
1070 if !face.surface.is_affine().unwrap_or(false) {
1071 continue;
1072 }
1073 let normal = face.surface.domain_u().and_then(|u| {
1074 face.surface.domain_v().and_then(|v| face.surface.normal(u[0], v[0]))
1075 });
1076 let Ok(normal) = normal else { continue };
1077 for c in face.loops.iter().flat_map(|l| &l.coedges) {
1078 let Some(edge) = edges.get(&c.edge_id) else { continue };
1079 for id in [edge.start_vertex_id, edge.end_vertex_id] {
1080 support.entry(id).or_default().push(normal);
1081 }
1082 }
1083 }
1084 let ridge_vertices: HashSet<u64> = support
1085 .iter()
1086 .filter_map(|(&id, normals)| {
1087 normals.iter().any(|n| normals[0].cross(*n).length() > 1e-9).then_some(id)
1088 })
1089 .collect();
1090 let mut fit_vertices: HashSet<usize> = HashSet::default();
1091 for &f in selected {
1092 let face = &shell.faces[f];
1093 let affine = face.surface.is_affine().unwrap_or(false);
1094 let mut polygon: Vec<Vec3> = Vec::new();
1096 let mut polygon_ids = Vec::new();
1097 for lp in &face.loops {
1098 for coedge in &lp.coedges {
1099 let Some(edge) = edges.get(&coedge.edge_id) else { continue };
1100 let start = if coedge.forward { edge.start_vertex_id } else { edge.end_vertex_id };
1101 if let Some(p) = vertices.get(&start) {
1102 polygon.push(*p);
1103 polygon_ids.push(start);
1104 points.push(*p);
1105 }
1106 let is_line = edge.curve.degree == 1 && edge.curve.control_points.len() == 2;
1107 if !is_line {
1108 for k in 1..8 {
1109 let t = edge.t0 + (edge.t1 - edge.t0) * k as f64 / 8.0;
1110 if let Ok(p) = edge.curve.evaluate(t) {
1111 points.push(p);
1112 }
1113 }
1114 }
1115 }
1116 }
1117 if affine && face.loops.len() == 1 && polygon.len() >= 3 {
1118 let ids: Vec<u32> = polygon.iter().map(|p| weld(*p, &mut mesh_vertices)).collect();
1121 for (&vertex_id, &mesh_id) in polygon_ids.iter().zip(&ids) {
1122 if ridge_vertices.contains(&vertex_id) {
1123 fit_vertices.insert(mesh_id as usize);
1124 }
1125 }
1126 let mut normal = Vec3::new(0.0, 0.0, 0.0);
1127 for i in 1..polygon.len() - 1 {
1128 let a = polygon[0];
1129 let b = polygon[i];
1130 let c = polygon[i + 1];
1131 normal = normal.add(b.sub(a).cross(c.sub(a)));
1132 if ids[0] != ids[i] && ids[i] != ids[i + 1] && ids[0] != ids[i + 1] {
1133 mesh_triangles.push([ids[0], ids[i], ids[i + 1]]);
1134 }
1135 }
1136 let centroid = polygon
1137 .iter()
1138 .fold(Vec3::new(0.0, 0.0, 0.0), |s, p| s.add(*p))
1139 .scale(1.0 / polygon.len() as f64);
1140 if normal.length() > 0.0 {
1142 oriented.push((centroid, normal));
1143 }
1144 } else {
1145 let mesh = crate::tessellate_face(face, crate::TessellationOptions::default(), 0)
1146 .map_err(|e| refuse_input("face", format!("could not sample {}: {e}", face_label(face))))?;
1147 let base: Vec<u32> = mesh
1148 .positions
1149 .chunks_exact(3)
1150 .map(|p| weld(Vec3::new(p[0], p[1], p[2]), &mut mesh_vertices))
1151 .collect();
1152 if !affine {
1153 fit_vertices.extend(base.iter().map(|&i| i as usize));
1154 points.extend(mesh.positions.chunks_exact(3).map(|p| Vec3::new(p[0], p[1], p[2])));
1155 }
1156 for tri in mesh.indices.chunks_exact(3) {
1157 let [a, b, c] = [base[tri[0] as usize], base[tri[1] as usize], base[tri[2] as usize]];
1158 if a != b && b != c && a != c {
1159 mesh_triangles.push([a, b, c]);
1160 let (pa, pb, pc) =
1161 (mesh_vertices[a as usize], mesh_vertices[b as usize], mesh_vertices[c as usize]);
1162 let normal = pb.sub(pa).cross(pc.sub(pa));
1163 if normal.length() > 0.0 {
1164 oriented.push((pa.add(pb).add(pc).scale(1.0 / 3.0), normal));
1165 }
1166 }
1167 }
1168 }
1169 }
1170 if points.len() < 3 || mesh_triangles.is_empty() {
1171 return Err(refuse_input(
1172 "face_ids",
1173 format!("{NO_FIT}: the selection has too few points to fit ({})", points.len()),
1174 ));
1175 }
1176 let mut fit_vertices: Vec<usize> = fit_vertices.into_iter().collect();
1177 fit_vertices.sort_unstable();
1178 Ok(Samples { points, mesh_vertices, mesh_triangles, fit_vertices, oriented })
1179}
1180
1181struct Fitted {
1182 carrier: RefitCarrier,
1183}
1184
1185fn to_ransac(p: Vec3) -> brep_ransac::Vec3 {
1186 brep_ransac::Vec3::new(p.x, p.y, p.z)
1187}
1188
1189fn from_ransac(p: brep_ransac::Vec3) -> Vec3 {
1190 Vec3::new(p.x, p.y, p.z)
1191}
1192
1193fn fit_carrier(
1194 samples: &Samples,
1195 kind: RefitSurfaceKind,
1196 tolerance: f64,
1197 scale: f64,
1198) -> Result<Fitted, KernelRefusal> {
1199 use brep_ransac::{AnalyticSurface, RecognitionOptions, SamplingMode, SurfaceHint, SurfaceType};
1200 let mesh = brep_ransac::Mesh::new(
1201 samples.mesh_vertices.iter().copied().map(to_ransac).collect(),
1202 samples.mesh_triangles.clone(),
1203 );
1204 let triangles: Vec<usize> = (0..samples.mesh_triangles.len()).collect();
1205 let hint = match kind {
1206 RefitSurfaceKind::Auto => SurfaceHint::Unknown,
1207 RefitSurfaceKind::Plane => SurfaceHint::KnownType { surface_type: SurfaceType::Plane },
1208 RefitSurfaceKind::Cylinder => {
1209 SurfaceHint::KnownType { surface_type: SurfaceType::Cylinder }
1210 }
1211 RefitSurfaceKind::Cone => SurfaceHint::KnownType { surface_type: SurfaceType::Cone },
1212 RefitSurfaceKind::Sphere => SurfaceHint::KnownType { surface_type: SurfaceType::Sphere },
1213 RefitSurfaceKind::Torus => SurfaceHint::KnownType { surface_type: SurfaceType::Torus },
1214 };
1215 let options = |distance: f64, normal: f64| RecognitionOptions {
1216 distance_tolerance: distance,
1217 relative_tolerance: 0.0,
1218 normal_tolerance: normal,
1221 minimum_support: 1,
1222 minimum_support_area: 0.0,
1223 discover_regions: false,
1224 sampling: SamplingMode::Vertices,
1225 ..RecognitionOptions::default()
1226 };
1227 let reconstruct = |options: &RecognitionOptions| {
1232 if !samples.fit_vertices.is_empty() && samples.fit_vertices.len() < mesh.vertices.len() {
1235 if let Ok(fit) = brep_ransac::reconstruct_surface_from_vertices(
1236 &mesh,
1237 &samples.fit_vertices,
1238 &hint,
1239 options,
1240 ) {
1241 if samples.points.iter().all(|p| {
1244 fit.surface.signed_distance(to_ransac(*p)).abs() <= options.distance_tolerance
1245 }) {
1246 return Ok(fit);
1247 }
1248 }
1249 }
1250 brep_ransac::reconstruct_surface(&mesh, &triangles, &hint, options)
1251 };
1252 let fit = reconstruct(&options(tolerance, 60.0_f64.to_radians()))
1253 .or_else(|_| reconstruct(&options(scale * 10.0, PI)))
1254 .map_err(|e| {
1255 refuse_unsupported(
1256 KernelStage::Refine,
1257 "refit_no_fit",
1258 format!("{NO_FIT}: the recognition pass found no {} ({e})", kind.name()),
1259 )
1260 })?;
1261 let carrier = match fit.surface {
1262 AnalyticSurface::Plane(p) => RefitCarrier::Plane {
1263 origin: from_ransac(p.origin),
1264 normal: unit(from_ransac(p.normal))?,
1265 },
1266 AnalyticSurface::Cylinder(c) => RefitCarrier::Cylinder {
1267 axis_origin: from_ransac(c.axis_origin),
1268 axis: unit(from_ransac(c.axis))?,
1269 radius: c.radius,
1270 },
1271 AnalyticSurface::Cone(c) => RefitCarrier::Cone {
1272 apex: from_ransac(c.apex),
1273 axis: unit(from_ransac(c.axis))?,
1274 half_angle: c.half_angle,
1275 },
1276 AnalyticSurface::Sphere(s) => {
1277 RefitCarrier::Sphere { center: from_ransac(s.center), radius: s.radius }
1278 }
1279 AnalyticSurface::Torus(t) => RefitCarrier::Torus {
1280 center: from_ransac(t.center),
1281 axis: unit(from_ransac(t.axis))?,
1282 major_radius: t.major_radius,
1283 minor_radius: t.minor_radius,
1284 },
1285 };
1286 Ok(Fitted { carrier })
1287}
1288
1289fn unit(v: Vec3) -> Result<Vec3, KernelRefusal> {
1290 v.normalized()
1291 .map_err(|e| refuse_unsupported(KernelStage::Refine, "refit_no_fit", format!("{NO_FIT}: {e}")))
1292}
1293
1294fn residuals(carrier: &RefitCarrier, points: &[Vec3]) -> (f64, f64) {
1295 let mut sum = 0.0;
1296 let mut max = 0.0_f64;
1297 for p in points {
1298 let d = carrier.distance(*p);
1299 sum += d * d;
1300 max = max.max(d);
1301 }
1302 ((sum / points.len().max(1) as f64).sqrt(), max)
1303}
1304
1305fn seam_frame(axis: Vec3, origin: Vec3, points: &[Vec3]) -> Result<(Vec3, Vec3), KernelRefusal> {
1314 let x0 = axis.perpendicular().map_err(|e| refuse_input("axis", e))?;
1315 let y0 = axis.cross(x0);
1316 let mut angles: Vec<f64> = points
1317 .iter()
1318 .filter_map(|p| {
1319 let rel = p.sub(origin);
1320 let (x, y) = (rel.dot(x0), rel.dot(y0));
1321 (x.hypot(y) > 1e-12).then(|| y.atan2(x))
1322 })
1323 .collect();
1324 if angles.is_empty() {
1325 return Ok((x0, y0));
1326 }
1327 angles.sort_by(f64::total_cmp);
1328 let mut best_gap = angles[0] + TAU - angles[angles.len() - 1];
1329 let mut best_mid = angles[angles.len() - 1] + best_gap * 0.5;
1330 for pair in angles.windows(2) {
1331 let gap = pair[1] - pair[0];
1332 if gap > best_gap {
1333 best_gap = gap;
1334 best_mid = pair[0] + gap * 0.5;
1335 }
1336 }
1337 let x = x0.scale(best_mid.cos()).add(y0.scale(best_mid.sin()));
1338 Ok((x, axis.cross(x)))
1339}
1340
1341fn carrier_surface(
1342 carrier: &RefitCarrier,
1343 points: &[Vec3],
1344 scale: f64,
1345) -> Result<NurbsSurface, KernelRefusal> {
1346 let fail = |e: String| {
1347 refuse_unsupported(KernelStage::Refine, "refit_surface", format!("{NO_FIT}: {e}"))
1348 };
1349 match *carrier {
1350 RefitCarrier::Plane { origin, normal } => {
1351 let u = normal.perpendicular().map_err(fail)?;
1352 let v = normal.cross(u);
1353 let (mut u0, mut u1, mut v0, mut v1) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
1354 for p in points {
1355 let rel = p.sub(origin);
1356 u0 = u0.min(rel.dot(u));
1357 u1 = u1.max(rel.dot(u));
1358 v0 = v0.min(rel.dot(v));
1359 v1 = v1.max(rel.dot(v));
1360 }
1361 let margin = ((u1 - u0).max(v1 - v0) * 0.1).max(scale * 1e-3);
1362 let corner = origin.add(u.scale(u0 - margin)).add(v.scale(v0 - margin));
1363 make_plane(corner, u, v, u1 - u0 + 2.0 * margin, v1 - v0 + 2.0 * margin).map_err(fail)
1364 }
1365 RefitCarrier::Cylinder { axis_origin, axis, radius } => {
1366 let (h0, h1) = axial_range(axis_origin, axis, points);
1367 let margin = ((h1 - h0) * 0.1).max(scale * 1e-3);
1368 let (x, _) = seam_frame(axis, axis_origin, points)?;
1369 let base = axis_origin.add(axis.scale(h0 - margin));
1370 let start = base.add(x.scale(radius));
1371 let generatrix =
1372 make_line(start, start.add(axis.scale(h1 - h0 + 2.0 * margin))).map_err(fail)?;
1373 make_revolution(base, axis, &generatrix, TAU).map_err(fail)
1374 }
1375 RefitCarrier::Cone { apex, axis, half_angle } => {
1376 let (h0, h1) = axial_range(apex, axis, points);
1377 if h0 <= 0.0 {
1378 return Err(refuse_unsupported(
1379 KernelStage::Refine,
1380 "cone_apex",
1381 format!(
1382 "the fitted cone's apex lies inside the region; a pointed cone face is \
1383 {NOT_DISK_OR_ANNULUS} this edit builds (v1)"
1384 ),
1385 ));
1386 }
1387 let margin = ((h1 - h0) * 0.1).max(scale * 1e-3).min(h0 * 0.5);
1388 let (x, _) = seam_frame(axis, apex, points)?;
1389 let tan = half_angle.tan();
1390 let (a, b) = (h0 - margin, h1 + margin);
1391 let generatrix = make_line(
1392 apex.add(axis.scale(a)).add(x.scale(a * tan)),
1393 apex.add(axis.scale(b)).add(x.scale(b * tan)),
1394 )
1395 .map_err(fail)?;
1396 make_revolution(apex, axis, &generatrix, TAU).map_err(fail)
1397 }
1398 RefitCarrier::Sphere { center, radius } => {
1399 let mean = points
1402 .iter()
1403 .fold(Vec3::new(0.0, 0.0, 0.0), |s, p| s.add(p.sub(center).normalized().unwrap_or(*p)));
1404 let mean = mean.normalized().unwrap_or(Vec3::new(1.0, 0.0, 0.0));
1405 let pole = mean.perpendicular().map_err(fail)?;
1406 let limit = (85.0_f64).to_radians().sin();
1407 let other = mean.cross(pole);
1410 let mut best = (f64::MAX, pole);
1411 for k in 0..36 {
1412 let angle = PI * k as f64 / 36.0;
1413 let candidate = pole.scale(angle.cos()).add(other.scale(angle.sin()));
1414 let worst = points
1415 .iter()
1416 .map(|p| (p.sub(center).dot(candidate) / radius).abs())
1417 .fold(0.0_f64, f64::max);
1418 if worst < best.0 {
1419 best = (worst, candidate);
1420 }
1421 }
1422 if best.0 > limit {
1423 return Err(refuse_unsupported(
1424 KernelStage::Refine,
1425 "sphere_extent",
1426 format!(
1427 "the spherical region reaches within 5 degrees of every pole this edit \
1428 can place; a region that large is {NOT_DISK_OR_ANNULUS} this edit \
1429 builds (v1)"
1430 ),
1431 ));
1432 }
1433 let pole = best.1;
1434 let seam = mean.scale(-1.0);
1435 let x_axis = seam.sub(pole.scale(seam.dot(pole))).normalized().map_err(fail)?;
1436 let meridian =
1437 make_arc(center, x_axis, pole, radius, -PI / 2.0, PI / 2.0).map_err(fail)?;
1438 make_revolution(center, pole, &meridian, TAU).map_err(fail)
1439 }
1440 RefitCarrier::Torus { center, axis, major_radius, minor_radius } => {
1441 if !(minor_radius > 0.0 && major_radius > minor_radius) {
1442 return Err(refuse_unsupported(
1443 KernelStage::Refine,
1444 "torus_radii",
1445 format!(
1446 "the fitted torus (radii {major_radius:.6}/{minor_radius:.6}) \
1447 self-intersects; {NO_FIT} as one face (v1)"
1448 ),
1449 ));
1450 }
1451 let (x, _) = seam_frame(axis, center, points)?;
1452 let mut angles: Vec<f64> = points
1454 .iter()
1455 .map(|p| {
1456 let rel = p.sub(center);
1457 let along = rel.dot(axis);
1458 let radial = rel.sub(axis.scale(along)).length() - major_radius;
1459 along.atan2(radial)
1460 })
1461 .collect();
1462 angles.sort_by(f64::total_cmp);
1463 let mut start = 0.0;
1464 if !angles.is_empty() {
1465 let mut best_gap = angles[0] + TAU - angles[angles.len() - 1];
1466 let mut best_mid = angles[angles.len() - 1] + best_gap * 0.5;
1467 for pair in angles.windows(2) {
1468 if pair[1] - pair[0] > best_gap {
1469 best_gap = pair[1] - pair[0];
1470 best_mid = pair[0] + best_gap * 0.5;
1471 }
1472 }
1473 start = best_mid;
1474 }
1475 let tube_center = center.add(x.scale(major_radius));
1476 let tube = make_arc(tube_center, x, axis, minor_radius, start, start + TAU)
1477 .map_err(fail)?;
1478 make_revolution(center, axis, &tube, TAU).map_err(fail)
1479 }
1480 }
1481}
1482
1483fn axial_range(origin: Vec3, axis: Vec3, points: &[Vec3]) -> (f64, f64) {
1484 points.iter().fold((f64::MAX, f64::MIN), |(lo, hi), p| {
1485 let h = p.sub(origin).dot(axis);
1486 (lo.min(h), hi.max(h))
1487 })
1488}
1489
1490fn region_same_sense(surface: &NurbsSurface, samples: &Samples) -> Result<bool, KernelRefusal> {
1491 let mut vote = 0.0;
1492 for (point, outward) in samples.oriented.iter().step_by((samples.oriented.len() / 64).max(1)) {
1493 let Ok(projection) = crate::project_point_to_surface(surface, *point) else { continue };
1494 let Ok(normal) = surface.normal(projection.u, projection.v) else { continue };
1495 vote += normal.dot(*outward).signum() * outward.length();
1496 }
1497 if vote == 0.0 {
1498 return Err(refuse_unsupported(
1499 KernelStage::Classify,
1500 "orientation",
1501 format!("{NO_FIT}: the selection's outward side could not be read"),
1502 ));
1503 }
1504 Ok(vote > 0.0)
1505}
1506
1507fn split_chains(cycle: &[BoundaryUse]) -> Vec<Chain> {
1512 let n = cycle.len();
1513 let Some(change) = (0..n).find(|&i| cycle[i].neighbour != cycle[(i + n - 1) % n].neighbour)
1514 else {
1515 return vec![Chain { uses: cycle.to_vec(), closed: true, neighbour: cycle[0].neighbour }];
1516 };
1517 let mut chains: Vec<Chain> = Vec::new();
1518 for k in 0..n {
1519 let use_ = cycle[(change + k) % n];
1520 match chains.last_mut() {
1521 Some(chain) if chain.neighbour == use_.neighbour => chain.uses.push(use_),
1522 _ => chains.push(Chain { uses: vec![use_], closed: false, neighbour: use_.neighbour }),
1523 }
1524 }
1525 chains
1526}
1527
1528fn chain_points(chain: &Chain, vertices: &HashMap<u64, Vec3>) -> Vec<Vec3> {
1529 let mut points: Vec<Vec3> = chain.uses.iter().map(|u| vertices[&u.start]).collect();
1530 if !chain.closed {
1531 points.push(vertices[&chain.uses.last().unwrap().end]);
1532 }
1533 points
1534}
1535
1536fn chain_trace_points(
1544 chain: &Chain,
1545 vertices: &HashMap<u64, Vec3>,
1546 edges: &HashMap<u64, &EdgeRecord>,
1547) -> Result<Vec<Vec3>, KernelRefusal> {
1548 let mut points = Vec::new();
1549 for use_ in &chain.uses {
1550 points.push(vertices[&use_.start]);
1551 let edge = edges[&use_.edge_id];
1552 if edge.curve.degree <= 1 {
1553 continue;
1554 }
1555 let mut parameters = Vec::new();
1556 for span in edge.curve.knots.windows(2) {
1557 let lo = span[0].max(edge.t0);
1558 let hi = span[1].min(edge.t1);
1559 if hi <= lo { continue; }
1560 for step in 0..4 {
1561 let t = lo + (hi - lo) * step as f64 / 4.0;
1562 if t > edge.t0 && t < edge.t1 { parameters.push(t); }
1563 }
1564 }
1565 if !use_.forward { parameters.reverse(); }
1566 for t in parameters {
1567 points.push(edge.curve.evaluate(t).map_err(|e| refuse_input("curve", e))?);
1568 }
1569 }
1570 if !chain.closed {
1571 points.push(vertices[&chain.uses.last().unwrap().end]);
1572 }
1573 Ok(points)
1574}
1575
1576fn curve_meeting(
1579 first: &NurbsCurve,
1580 second: &NurbsCurve,
1581 seed: Vec3,
1582) -> Result<(Vec3, f64), KernelRefusal> {
1583 let project = |curve: &NurbsCurve, p: Vec3| {
1584 project_point_to_curve(curve, p).map_err(|e| {
1585 refuse_unsupported(KernelStage::Intersect, "corner", format!("{NO_EXACT_BOUNDARY}: {e}"))
1586 })
1587 };
1588 let mut p = seed;
1589 let mut gap = f64::MAX;
1590 for _ in 0..200 {
1591 let a = project(first, p)?.point;
1592 let b = project(second, a)?.point;
1593 gap = a.sub(b).length();
1594 let next = a.add(b).scale(0.5);
1595 let step = next.sub(p).length();
1596 p = next;
1597 if step < 1e-14 * (1.0 + p.length()) {
1598 break;
1599 }
1600 }
1601 let a = project(first, p)?.point;
1602 let b = project(second, p)?.point;
1603 gap = gap.min(a.sub(b).length());
1604 Ok((a.add(b).scale(0.5), gap))
1605}
1606
1607fn as_circle(curve: &NurbsCurve, scale: f64) -> Option<(Vec3, Vec3, f64)> {
1609 let [t0, t1] = curve.domain().ok()?;
1610 let samples: Vec<Vec3> = (0..24)
1611 .filter_map(|k| curve.evaluate(t0 + (t1 - t0) * k as f64 / 24.0).ok())
1612 .collect();
1613 if samples.len() < 24 {
1614 return None;
1615 }
1616 let (a, b, c) = (samples[0], samples[8], samples[16]);
1617 let ab = b.sub(a);
1618 let ac = c.sub(a);
1619 let normal = ab.cross(ac);
1620 let denom = 2.0 * normal.length_squared();
1621 if denom <= 1e-30 {
1622 return None;
1623 }
1624 let center = a.add(
1625 normal
1626 .cross(ab)
1627 .scale(ac.length_squared())
1628 .add(ac.cross(normal).scale(ab.length_squared()))
1629 .scale(1.0 / denom),
1630 );
1631 let radius = a.sub(center).length();
1632 let unit_normal = normal.normalized().ok()?;
1633 let tol = scale * 1e-9 + radius * 1e-9;
1634 for p in &samples {
1635 if (p.sub(center).length() - radius).abs() > tol || p.sub(center).dot(unit_normal).abs() > tol {
1636 return None;
1637 }
1638 }
1639 Some((center, unit_normal, radius))
1640}
1641
1642fn angle_in(center: Vec3, x: Vec3, y: Vec3, p: Vec3) -> f64 {
1643 let rel = p.sub(center);
1644 let angle = rel.dot(y).atan2(rel.dot(x));
1645 if angle < 0.0 {
1646 angle + TAU
1647 } else {
1648 angle
1649 }
1650}
1651
1652fn closed_ring_curve(
1655 curve: &NurbsCurve,
1656 points: &[Vec3],
1657 scale: f64,
1658) -> Result<(NurbsCurve, bool), KernelRefusal> {
1659 let fail = |e: String| {
1660 refuse_unsupported(KernelStage::Intersect, "ring", format!("{NO_EXACT_BOUNDARY}: {e}"))
1661 };
1662 if let Some((center, normal, radius)) = as_circle(curve, scale) {
1663 let start = points[0];
1664 let x = start.sub(center);
1665 let x = x.sub(normal.scale(x.dot(normal))).normalized().map_err(fail)?;
1666 let mut n = normal;
1667 let y = n.cross(x);
1668 if angle_in(center, x, y, points[1]) > PI {
1670 n = n.scale(-1.0);
1671 }
1672 let y = n.cross(x);
1673 let arc = make_arc(center, x, y, radius, 0.0, TAU).map_err(fail)?;
1674 return Ok((arc, true));
1675 }
1676 let p0 = project_point_to_curve(curve, points[0]).map_err(fail)?;
1678 let p1 = project_point_to_curve(curve, points[1]).map_err(fail)?;
1679 let [t0, t1] = curve.domain().map_err(fail)?;
1680 let mut delta = p1.u - p0.u;
1681 let period = t1 - t0;
1682 if delta > period * 0.5 {
1683 delta -= period;
1684 } else if delta < -period * 0.5 {
1685 delta += period;
1686 }
1687 Ok((curve.clone(), delta > 0.0))
1688}
1689
1690fn open_arc(
1693 curve: &NurbsCurve,
1694 start: u64,
1695 end: u64,
1696 start_point: Vec3,
1697 end_point: Vec3,
1698 mid_point: Vec3,
1699 scale: f64,
1700) -> Result<BuiltEdge, KernelRefusal> {
1701 let fail = |e: String| {
1702 refuse_unsupported(KernelStage::Intersect, "arc", format!("{NO_EXACT_BOUNDARY}: {e}"))
1703 };
1704 if let Some((center, normal, radius)) = as_circle(curve, scale) {
1705 let x = start_point.sub(center);
1706 let x = x.sub(normal.scale(x.dot(normal))).normalized().map_err(fail)?;
1707 let mut n = normal;
1708 let mut y = n.cross(x);
1709 let mut sweep = angle_in(center, x, y, end_point);
1710 if angle_in(center, x, y, mid_point) > sweep {
1711 n = n.scale(-1.0);
1712 y = n.cross(x);
1713 sweep = angle_in(center, x, y, end_point);
1714 }
1715 let arc = make_arc(center, x, y, radius, 0.0, sweep).map_err(fail)?;
1716 let [t0, t1] = arc.domain().map_err(fail)?;
1717 return Ok(BuiltEdge { curve: arc, t0, t1, start, end, region_forward: true, region_pcurve: None });
1718 }
1719 let a = project_point_to_curve(curve, start_point).map_err(fail)?.u;
1720 let b = project_point_to_curve(curve, end_point).map_err(fail)?.u;
1721 let m = project_point_to_curve(curve, mid_point).map_err(fail)?.u;
1722 if !((a < m && m < b) || (b < m && m < a)) {
1723 return Err(refuse_unsupported(
1724 KernelStage::Intersect,
1725 "arc_seam",
1726 format!(
1727 "{NO_EXACT_BOUNDARY}: a boundary run crosses its intersection curve's seam (v1)"
1728 ),
1729 ));
1730 }
1731 let (lo, hi) = if a < b { (a, b) } else { (b, a) };
1732 let trimmed = crate::feature_pipeline::features::common::trimmed_curve(curve, lo, hi)
1733 .map_err(fail)?;
1734 let [t0, t1] = trimmed.domain().map_err(fail)?;
1735 if a < b {
1736 Ok(BuiltEdge { curve: trimmed, t0, t1, start, end, region_forward: true, region_pcurve: None })
1737 } else {
1738 Ok(BuiltEdge {
1739 curve: trimmed,
1740 t0,
1741 t1,
1742 start: end,
1743 end: start,
1744 region_forward: false,
1745 region_pcurve: None,
1746 })
1747 }
1748}
1749
1750fn iso_ring(
1757 surface: &NurbsSurface,
1758 intersection: &NurbsCurve,
1759 points: &[Vec3],
1760 tolerance: f64,
1761) -> Option<(NurbsCurve, NurbsCurve, bool)> {
1762 let uv: Vec<(f64, f64)> = points
1763 .iter()
1764 .map(|p| crate::project_point_to_surface(surface, *p).map(|q| (q.u, q.v)))
1765 .collect::<Result<_, _>>()
1766 .ok()?;
1767 if uv.len() < 3 {
1768 return None;
1769 }
1770 let [u0, u1] = [surface.knots_u[0], *surface.knots_u.last()?];
1771 let [v0, v1] = [surface.knots_v[0], *surface.knots_v.last()?];
1772 let mean_u = uv.iter().map(|q| q.0).sum::<f64>() / uv.len() as f64;
1773 let mean_v = uv.iter().map(|q| q.1).sum::<f64>() / uv.len() as f64;
1774 let wrap = |delta: f64, period: f64| {
1775 let mut d = delta % period;
1776 if d > period * 0.5 {
1777 d -= period;
1778 } else if d < -period * 0.5 {
1779 d += period;
1780 }
1781 d
1782 };
1783 let candidates = [
1784 (surface.iso_curve_v(mean_v).ok(), true),
1786 (surface.iso_curve_u(mean_u).ok(), false),
1788 ];
1789 for (curve, along_u) in candidates {
1790 let Some(curve) = curve else { continue };
1791 let [t0, t1] = curve.domain().ok()?;
1792 let (Ok(start), Ok(end)) = (curve.evaluate(t0), curve.evaluate(t1)) else { continue };
1793 if start.sub(end).length() > tolerance * 1e-3 {
1794 continue; }
1796 let on_intersection = (0..16).all(|k| {
1797 curve
1798 .evaluate(t0 + (t1 - t0) * (k as f64 + 0.5) / 16.0)
1799 .ok()
1800 .and_then(|p| project_point_to_curve(intersection, p).ok())
1801 .is_some_and(|q| q.distance <= tolerance)
1802 });
1803 let through_points =
1804 points.iter().all(|p| project_point_to_curve(&curve, *p).is_ok_and(|q| q.distance <= tolerance));
1805 if !(on_intersection && through_points) {
1806 continue;
1807 }
1808 let (period, a, b) = if along_u {
1809 (u1 - u0, uv[0].0, uv[1].0)
1810 } else {
1811 (v1 - v0, uv[0].1, uv[1].1)
1812 };
1813 let forward = wrap(b - a, period) > 0.0;
1814 let (from, to) = if along_u {
1815 ([u0, mean_v], [u1, mean_v])
1816 } else {
1817 ([mean_u, v0], [mean_u, v1])
1818 };
1819 let (from, to) = if forward { (from, to) } else { (to, from) };
1820 let pcurve =
1821 make_line(Vec3::new(from[0], from[1], 0.0), Vec3::new(to[0], to[1], 0.0)).ok()?;
1822 return Some((curve, pcurve, forward));
1823 }
1824 None
1825}
1826
1827fn iso_curve_on(
1832 surface: &NurbsSurface,
1833 other: &NurbsSurface,
1834 points: &[Vec3],
1835 tolerance: f64,
1836) -> Vec<NurbsCurve> {
1837 let Ok(uv) = points
1838 .iter()
1839 .map(|p| crate::project_point_to_surface(surface, *p).map(|q| (q.u, q.v)))
1840 .collect::<Result<Vec<_>, _>>()
1841 else {
1842 return Vec::new();
1843 };
1844 if uv.is_empty() {
1845 return Vec::new();
1846 }
1847 let mean_u = uv.iter().map(|q| q.0).sum::<f64>() / uv.len() as f64;
1848 let mean_v = uv.iter().map(|q| q.1).sum::<f64>() / uv.len() as f64;
1849 let mut out = Vec::new();
1850 for curve in [surface.iso_curve_v(mean_v), surface.iso_curve_u(mean_u)].into_iter().flatten() {
1851 let Ok([t0, t1]) = curve.domain() else { continue };
1852 let through = points
1853 .iter()
1854 .all(|p| project_point_to_curve(&curve, *p).is_ok_and(|q| q.distance <= tolerance));
1855 let on_other = (0..=32).all(|k| {
1856 curve
1857 .evaluate(t0 + (t1 - t0) * k as f64 / 32.0)
1858 .ok()
1859 .is_some_and(|p| {
1860 if let Some(crate::AnalyticSurface::Plane { origin, u_dir, v_dir, .. }) =
1864 other.analytic()
1865 {
1866 u_dir.cross(*v_dir).normalized()
1867 .is_ok_and(|n| p.sub(*origin).dot(n).abs() <= tolerance)
1868 } else {
1869 crate::project_point_to_surface(other, p).is_ok_and(|q| q.distance <= tolerance)
1870 }
1871 })
1872 });
1873 if through && on_other {
1874 out.push(curve);
1875 }
1876 }
1877 out
1878}
1879
1880fn reseated_plane(old: &NurbsSurface, ring: &NurbsCurve, scale: f64) -> Option<NurbsSurface> {
1885 let [t0, t1] = ring.domain().ok()?;
1886 let points: Vec<Vec3> =
1887 (0..48).filter_map(|k| ring.evaluate(t0 + (t1 - t0) * k as f64 / 48.0).ok()).collect();
1888 if points.len() < 48 {
1889 return None;
1890 }
1891 let centroid = points
1892 .iter()
1893 .fold(Vec3::new(0.0, 0.0, 0.0), |s, p| s.add(*p))
1894 .scale(1.0 / points.len() as f64);
1895 let mut normal = Vec3::new(0.0, 0.0, 0.0);
1897 for (i, a) in points.iter().enumerate() {
1898 let b = points[(i + 1) % points.len()];
1899 normal = normal.add(a.sub(centroid).cross(b.sub(centroid)));
1900 }
1901 let normal = normal.normalized().ok()?;
1902 let flat = points.iter().all(|p| p.sub(centroid).dot(normal).abs() <= scale * 1e-10);
1903 if !flat {
1904 return None;
1905 }
1906 let [u0, u1] = [old.knots_u[0], *old.knots_u.last()?];
1907 let [v0, v1] = [old.knots_v[0], *old.knots_v.last()?];
1908 let origin = old.evaluate(u0, v0).ok()?;
1909 let su = old.evaluate(u1, v0).ok()?.sub(origin);
1910 let sv = old.evaluate(u0, v1).ok()?.sub(origin);
1911 let old_normal = su.cross(sv).normalized().ok()?;
1912 let off = points.iter().map(|p| p.sub(origin).dot(old_normal).abs()).fold(0.0, f64::max);
1913 if off <= scale * 1e-12 {
1914 return None;
1915 }
1916 let n = if normal.dot(old_normal) >= 0.0 { normal } else { normal.scale(-1.0) };
1917 let u_dir = su.sub(n.scale(su.dot(n))).normalized().ok()?;
1918 let v_dir = n.cross(u_dir);
1919 let mut lo = [f64::MAX; 2];
1921 let mut hi = [f64::MIN; 2];
1922 let corners = [origin, origin.add(su), origin.add(sv), origin.add(su).add(sv)];
1923 for p in points.iter().chain(corners.iter()) {
1924 let rel = p.sub(centroid);
1925 let (a, b) = (rel.dot(u_dir), rel.dot(v_dir));
1926 lo = [lo[0].min(a), lo[1].min(b)];
1927 hi = [hi[0].max(a), hi[1].max(b)];
1928 }
1929 let margin = (hi[0] - lo[0]).max(hi[1] - lo[1]) * 0.05;
1930 let corner = centroid.add(u_dir.scale(lo[0] - margin)).add(v_dir.scale(lo[1] - margin));
1931 make_plane(corner, u_dir, v_dir, hi[0] - lo[0] + 2.0 * margin, hi[1] - lo[1] + 2.0 * margin).ok()
1932}
1933
1934#[allow(clippy::too_many_arguments)]
1939fn refit_closed_shell(
1940 solid: &BrepSolid,
1941 shell_index: usize,
1942 selected_indices: &[usize],
1943 euler: i64,
1944 kind: RefitSurfaceKind,
1945 tolerance: f64,
1946 scale: f64,
1947 edge_by_id: &HashMap<u64, &EdgeRecord>,
1948 vertex_by_id: &HashMap<u64, Vec3>,
1949) -> Result<(BrepSolid, RefitReport), KernelRefusal> {
1950 let shell = &solid.shells[shell_index];
1951 if selected_indices.len() != shell.faces.len() {
1952 return Err(refuse_unsupported(
1953 KernelStage::Classify,
1954 "closed_region",
1955 format!(
1956 "the selection has no boundary but is not every face of its shell; the region \
1957 is {NOT_DISK_OR_ANNULUS}"
1958 ),
1959 ));
1960 }
1961 let kind = match kind {
1964 RefitSurfaceKind::Auto => match euler {
1965 2 => RefitSurfaceKind::Sphere,
1966 0 => RefitSurfaceKind::Torus,
1967 _ => {
1968 return Err(refuse_unsupported(
1969 KernelStage::Classify,
1970 "closed_shell_genus",
1971 format!(
1972 "the selected closed shell has Euler characteristic {euler}; \
1973 {CLOSED_SHELL_KIND} (2 or 0)"
1974 ),
1975 ))
1976 }
1977 },
1978 RefitSurfaceKind::Plane | RefitSurfaceKind::Cylinder | RefitSurfaceKind::Cone => {
1979 return Err(refuse_unsupported(
1980 KernelStage::Classify,
1981 "closed_shell_kind",
1982 format!(
1983 "the selection is a whole closed shell and no {} can be one: \
1984 {CLOSED_SHELL_KIND}",
1985 kind.name()
1986 ),
1987 ))
1988 }
1989 other => other,
1990 };
1991 let wanted = if kind == RefitSurfaceKind::Sphere { 2 } else { 0 };
1992 if euler != wanted {
1993 return Err(refuse_unsupported(
1994 KernelStage::Classify,
1995 "closed_shell_genus",
1996 format!(
1997 "a {} {CLOSED_SHELL_GENUS}: the selected closed shell has Euler characteristic \
1998 {euler}, a {} has {wanted}",
1999 kind.name(),
2000 kind.name()
2001 ),
2002 ));
2003 }
2004
2005 let samples = fit_samples(shell, selected_indices, edge_by_id, vertex_by_id)?;
2006 let carrier = fit_carrier(&samples, kind, tolerance, scale)?.carrier;
2007 let (rms_residual, max_residual) = residuals(&carrier, &samples.points);
2008 if max_residual > tolerance {
2009 return Err(KernelRefusal::ill_posed(
2010 KernelStage::Refine,
2011 "refit_residual",
2012 format!(
2013 "Refit Faces: the {} {RESIDUAL_OVER_TOLERANCE} {max_residual:.6} (rms \
2014 {rms_residual:.6}) is over the tolerance {tolerance}; these faces are not \
2015 one {} within it",
2016 carrier.kind().name(),
2017 carrier.kind().name()
2018 ),
2019 ));
2020 }
2021 coarse_facet_gate(&carrier, shell, selected_indices, edge_by_id, vertex_by_id)?;
2022 let fail = |e: String| {
2023 refuse_unsupported(KernelStage::Refine, "refit_surface", format!("{NO_FIT}: {e}"))
2024 };
2025 let primitive = match carrier {
2026 RefitCarrier::Sphere { center, radius } => {
2027 crate::make_sphere_brep(center, radius, Vec3::new(0.0, 0.0, 1.0)).map_err(fail)?
2030 }
2031 RefitCarrier::Torus { center, axis, major_radius, minor_radius } => {
2032 if !(minor_radius > 0.0 && major_radius > minor_radius) {
2033 return Err(refuse_unsupported(
2034 KernelStage::Refine,
2035 "torus_radii",
2036 format!(
2037 "the fitted torus (radii {major_radius:.6}/{minor_radius:.6}) \
2038 self-intersects; {NO_FIT} as one face (v1)"
2039 ),
2040 ));
2041 }
2042 crate::make_torus_brep(center, axis, major_radius, minor_radius).map_err(fail)?
2043 }
2044 other => {
2045 return Err(refuse_unsupported(
2046 KernelStage::Classify,
2047 "closed_shell_kind",
2048 format!(
2049 "the fit returned a {}; {CLOSED_SHELL_KIND}",
2050 other.kind().name()
2051 ),
2052 ))
2053 }
2054 };
2055
2056 let prim_face = &primitive.shells[0].faces[0];
2059 let vote = region_same_sense(&prim_face.surface, &samples)?;
2060 let cavity = vote != prim_face.same_sense;
2061
2062 let base = solid
2064 .vertices
2065 .iter()
2066 .map(|v| v.id)
2067 .chain(solid.edges.iter().map(|e| e.id))
2068 .chain(solid.shells.iter().map(|s| s.id))
2069 .chain(solid.shells.iter().flat_map(|s| s.faces.iter()).flat_map(|f| {
2070 std::iter::once(f.id).chain(
2071 f.loops
2072 .iter()
2073 .flat_map(|l| std::iter::once(l.id).chain(l.coedges.iter().map(|c| c.id))),
2074 )
2075 }))
2076 .max()
2077 .unwrap_or(0)
2078 + 1;
2079 let keeper = &shell.faces[selected_indices[0]];
2080 let mut new_shell = primitive.shells[0].clone();
2081 new_shell.id = shell.id;
2082 for face in &mut new_shell.faces {
2083 face.id = keeper.id;
2084 face.name = keeper.name.clone();
2085 for lp in &mut face.loops {
2086 lp.id += base;
2087 for coedge in &mut lp.coedges {
2088 coedge.id += base;
2089 coedge.edge_id += base;
2090 }
2091 }
2092 if cavity {
2093 flip_face(face).map_err(fail)?;
2094 }
2095 }
2096 let new_edges: Vec<EdgeRecord> = primitive
2097 .edges
2098 .iter()
2099 .map(|e| {
2100 let mut e = e.clone();
2101 e.id += base;
2102 e.start_vertex_id += base;
2103 e.end_vertex_id += base;
2104 e
2105 })
2106 .collect();
2107 let new_vertices: Vec<VertexRecord> = primitive
2108 .vertices
2109 .iter()
2110 .map(|v| VertexRecord { id: v.id + base, point: v.point })
2111 .collect();
2112
2113 let old_edges: HashSet<u64> = shell
2115 .faces
2116 .iter()
2117 .flat_map(|f| f.loops.iter())
2118 .flat_map(|l| l.coedges.iter().map(|c| c.edge_id))
2119 .collect();
2120 let mut out = solid.clone();
2121 out.edges.retain(|e| !old_edges.contains(&e.id));
2122 out.edges.extend(new_edges);
2123 out.shells[shell_index] = new_shell;
2124 let used: HashSet<u64> =
2125 out.edges.iter().flat_map(|e| [e.start_vertex_id, e.end_vertex_id]).collect();
2126 out.vertices.retain(|v| used.contains(&v.id));
2127 out.vertices.extend(new_vertices);
2128 let issues = out.validate();
2131 if !issues.is_empty() {
2132 let first: Vec<String> = issues.iter().take(3).map(|i| format!("{i:?}")).collect();
2133 return Err(KernelRefusal::new(
2134 RefusalClass::InvalidResultTopology { issues: issues.len() as u32 },
2135 KernelStage::Validate,
2136 format!(
2137 "Refit Faces: the {RESULT_INVALID} ({} issue(s): {})",
2138 issues.len(),
2139 first.join("; ")
2140 ),
2141 ));
2142 }
2143 let report = RefitReport {
2144 carrier,
2145 rms_residual,
2146 max_residual,
2147 tolerance,
2148 samples: samples.points.len(),
2149 replaced_faces: selected_indices.len(),
2150 boundary_edges_before: 0,
2151 boundary_edges_after: 0,
2152 boundary_loops: 0,
2153 new_face_id: keeper.id,
2154 };
2155 Ok((out, report))
2156}
2157
2158pub(super) fn flip_face(face: &mut FaceRecord) -> Result<(), String> {
2161 face.same_sense = !face.same_sense;
2162 for lp in &mut face.loops {
2163 lp.coedges.reverse();
2164 for coedge in &mut lp.coedges {
2165 coedge.forward = !coedge.forward;
2166 coedge.pcurve = coedge.pcurve.reversed()?;
2167 }
2168 }
2169 Ok(())
2170}
2171
2172pub const MAX_FACET_SPAN_DEGREES: f64 = 60.0;
2184
2185pub const FACET_SPAN_SLACK_DEGREES: f64 = 0.1;
2189
2190fn coarse_facet_gate(
2195 carrier: &RefitCarrier,
2196 shell: &crate::topology::ShellRecord,
2197 selected: &[usize],
2198 edges: &HashMap<u64, &EdgeRecord>,
2199 vertices: &HashMap<u64, Vec3>,
2200) -> Result<(), KernelRefusal> {
2201 if matches!(carrier, RefitCarrier::Plane { .. }) {
2202 return Ok(());
2203 }
2204 let mut worst: Option<(f64, &FaceRecord)> = None;
2205 for &f in selected {
2206 let face = &shell.faces[f];
2207 if !face.surface.is_affine().unwrap_or(false) {
2208 continue;
2209 }
2210 let mut sum = Vec3::new(0.0, 0.0, 0.0);
2211 let mut count = 0usize;
2212 for coedge in face.loops.iter().flat_map(|l| l.coedges.iter()) {
2213 let Some(edge) = edges.get(&coedge.edge_id) else { continue };
2214 let id = if coedge.forward { edge.start_vertex_id } else { edge.end_vertex_id };
2215 if let Some(p) = vertices.get(&id) {
2216 sum = sum.add(*p);
2217 count += 1;
2218 }
2219 }
2220 if count < 3 {
2221 continue;
2222 }
2223 let centroid = sum.scale(1.0 / count as f64);
2224 let radius = local_radius(carrier, centroid);
2225 if !(radius > 0.0) {
2226 continue;
2227 }
2228 let sag = carrier.distance(centroid);
2229 let span = 2.0 * (1.0 - (sag / radius).min(2.0)).clamp(-1.0, 1.0).acos().to_degrees();
2230 if worst.map_or(true, |(w, _)| span > w) {
2231 worst = Some((span, face));
2232 }
2233 }
2234 match worst {
2235 Some((span, face)) if span > MAX_FACET_SPAN_DEGREES + FACET_SPAN_SLACK_DEGREES => {
2236 Err(KernelRefusal::ill_posed(
2237 KernelStage::Refine,
2238 "refit_facets_coarse",
2239 format!(
2240 "Refit Faces: the selection is {FACETS_TOO_COARSE} to be one {}: {} spans \
2241 {span:.1} degrees of the fitted surface (at most {MAX_FACET_SPAN_DEGREES}); \
2242 a facet may be no wider than the radius it approximates",
2243 carrier.kind().name(),
2244 face_label(face)
2245 ),
2246 ))
2247 }
2248 _ => Ok(()),
2249 }
2250}
2251
2252fn local_radius(carrier: &RefitCarrier, point: Vec3) -> f64 {
2254 match *carrier {
2255 RefitCarrier::Plane { .. } => f64::INFINITY,
2256 RefitCarrier::Cylinder { radius, .. } => radius,
2257 RefitCarrier::Sphere { radius, .. } => radius,
2258 RefitCarrier::Torus { minor_radius, .. } => minor_radius,
2259 RefitCarrier::Cone { apex, axis, half_angle } => {
2260 let rel = point.sub(apex);
2261 let radial = rel.sub(axis.scale(rel.dot(axis))).length();
2262 radial / half_angle.cos()
2263 }
2264 }
2265}
2266