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| {
1216 let mut options = RecognitionOptions::default();
1217 options.distance_tolerance = distance;
1218 options.relative_tolerance = 0.0;
1219 options.normal_tolerance = normal;
1222 options.minimum_support = 1;
1223 options.minimum_support_area = 0.0;
1224 options.discover_regions = false;
1225 options.sampling = SamplingMode::Vertices;
1226 options
1227 };
1228 let reconstruct = |options: &RecognitionOptions| {
1233 if !samples.fit_vertices.is_empty() && samples.fit_vertices.len() < mesh.vertices.len() {
1236 if let Ok(fit) = brep_ransac::reconstruct_surface_from_vertices(
1237 &mesh,
1238 &samples.fit_vertices,
1239 &hint,
1240 options,
1241 ) {
1242 if samples.points.iter().all(|p| {
1245 fit.surface.signed_distance(to_ransac(*p)).abs() <= options.distance_tolerance
1246 }) {
1247 return Ok(fit);
1248 }
1249 }
1250 }
1251 brep_ransac::reconstruct_surface(&mesh, &triangles, &hint, options)
1252 };
1253 let fit = reconstruct(&options(tolerance, 60.0_f64.to_radians()))
1254 .or_else(|_| reconstruct(&options(scale * 10.0, PI)))
1255 .map_err(|e| {
1256 refuse_unsupported(
1257 KernelStage::Refine,
1258 "refit_no_fit",
1259 format!("{NO_FIT}: the recognition pass found no {} ({e})", kind.name()),
1260 )
1261 })?;
1262 let carrier = match fit.surface {
1263 AnalyticSurface::Plane(p) => RefitCarrier::Plane {
1264 origin: from_ransac(p.origin),
1265 normal: unit(from_ransac(p.normal))?,
1266 },
1267 AnalyticSurface::Cylinder(c) => RefitCarrier::Cylinder {
1268 axis_origin: from_ransac(c.axis_origin),
1269 axis: unit(from_ransac(c.axis))?,
1270 radius: c.radius,
1271 },
1272 AnalyticSurface::Cone(c) => RefitCarrier::Cone {
1273 apex: from_ransac(c.apex),
1274 axis: unit(from_ransac(c.axis))?,
1275 half_angle: c.half_angle,
1276 },
1277 AnalyticSurface::Sphere(s) => {
1278 RefitCarrier::Sphere { center: from_ransac(s.center), radius: s.radius }
1279 }
1280 AnalyticSurface::Torus(t) => RefitCarrier::Torus {
1281 center: from_ransac(t.center),
1282 axis: unit(from_ransac(t.axis))?,
1283 major_radius: t.major_radius,
1284 minor_radius: t.minor_radius,
1285 },
1286 };
1287 Ok(Fitted { carrier })
1288}
1289
1290fn unit(v: Vec3) -> Result<Vec3, KernelRefusal> {
1291 v.normalized()
1292 .map_err(|e| refuse_unsupported(KernelStage::Refine, "refit_no_fit", format!("{NO_FIT}: {e}")))
1293}
1294
1295fn residuals(carrier: &RefitCarrier, points: &[Vec3]) -> (f64, f64) {
1296 let mut sum = 0.0;
1297 let mut max = 0.0_f64;
1298 for p in points {
1299 let d = carrier.distance(*p);
1300 sum += d * d;
1301 max = max.max(d);
1302 }
1303 ((sum / points.len().max(1) as f64).sqrt(), max)
1304}
1305
1306fn seam_frame(axis: Vec3, origin: Vec3, points: &[Vec3]) -> Result<(Vec3, Vec3), KernelRefusal> {
1315 let x0 = axis.perpendicular().map_err(|e| refuse_input("axis", e))?;
1316 let y0 = axis.cross(x0);
1317 let mut angles: Vec<f64> = points
1318 .iter()
1319 .filter_map(|p| {
1320 let rel = p.sub(origin);
1321 let (x, y) = (rel.dot(x0), rel.dot(y0));
1322 (x.hypot(y) > 1e-12).then(|| y.atan2(x))
1323 })
1324 .collect();
1325 if angles.is_empty() {
1326 return Ok((x0, y0));
1327 }
1328 angles.sort_by(f64::total_cmp);
1329 let mut best_gap = angles[0] + TAU - angles[angles.len() - 1];
1330 let mut best_mid = angles[angles.len() - 1] + best_gap * 0.5;
1331 for pair in angles.windows(2) {
1332 let gap = pair[1] - pair[0];
1333 if gap > best_gap {
1334 best_gap = gap;
1335 best_mid = pair[0] + gap * 0.5;
1336 }
1337 }
1338 let x = x0.scale(best_mid.cos()).add(y0.scale(best_mid.sin()));
1339 Ok((x, axis.cross(x)))
1340}
1341
1342fn carrier_surface(
1343 carrier: &RefitCarrier,
1344 points: &[Vec3],
1345 scale: f64,
1346) -> Result<NurbsSurface, KernelRefusal> {
1347 let fail = |e: String| {
1348 refuse_unsupported(KernelStage::Refine, "refit_surface", format!("{NO_FIT}: {e}"))
1349 };
1350 match *carrier {
1351 RefitCarrier::Plane { origin, normal } => {
1352 let u = normal.perpendicular().map_err(fail)?;
1353 let v = normal.cross(u);
1354 let (mut u0, mut u1, mut v0, mut v1) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
1355 for p in points {
1356 let rel = p.sub(origin);
1357 u0 = u0.min(rel.dot(u));
1358 u1 = u1.max(rel.dot(u));
1359 v0 = v0.min(rel.dot(v));
1360 v1 = v1.max(rel.dot(v));
1361 }
1362 let margin = ((u1 - u0).max(v1 - v0) * 0.1).max(scale * 1e-3);
1363 let corner = origin.add(u.scale(u0 - margin)).add(v.scale(v0 - margin));
1364 make_plane(corner, u, v, u1 - u0 + 2.0 * margin, v1 - v0 + 2.0 * margin).map_err(fail)
1365 }
1366 RefitCarrier::Cylinder { axis_origin, axis, radius } => {
1367 let (h0, h1) = axial_range(axis_origin, axis, points);
1368 let margin = ((h1 - h0) * 0.1).max(scale * 1e-3);
1369 let (x, _) = seam_frame(axis, axis_origin, points)?;
1370 let base = axis_origin.add(axis.scale(h0 - margin));
1371 let start = base.add(x.scale(radius));
1372 let generatrix =
1373 make_line(start, start.add(axis.scale(h1 - h0 + 2.0 * margin))).map_err(fail)?;
1374 make_revolution(base, axis, &generatrix, TAU).map_err(fail)
1375 }
1376 RefitCarrier::Cone { apex, axis, half_angle } => {
1377 let (h0, h1) = axial_range(apex, axis, points);
1378 if h0 <= 0.0 {
1379 return Err(refuse_unsupported(
1380 KernelStage::Refine,
1381 "cone_apex",
1382 format!(
1383 "the fitted cone's apex lies inside the region; a pointed cone face is \
1384 {NOT_DISK_OR_ANNULUS} this edit builds (v1)"
1385 ),
1386 ));
1387 }
1388 let margin = ((h1 - h0) * 0.1).max(scale * 1e-3).min(h0 * 0.5);
1389 let (x, _) = seam_frame(axis, apex, points)?;
1390 let tan = half_angle.tan();
1391 let (a, b) = (h0 - margin, h1 + margin);
1392 let generatrix = make_line(
1393 apex.add(axis.scale(a)).add(x.scale(a * tan)),
1394 apex.add(axis.scale(b)).add(x.scale(b * tan)),
1395 )
1396 .map_err(fail)?;
1397 make_revolution(apex, axis, &generatrix, TAU).map_err(fail)
1398 }
1399 RefitCarrier::Sphere { center, radius } => {
1400 let mean = points
1403 .iter()
1404 .fold(Vec3::new(0.0, 0.0, 0.0), |s, p| s.add(p.sub(center).normalized().unwrap_or(*p)));
1405 let mean = mean.normalized().unwrap_or(Vec3::new(1.0, 0.0, 0.0));
1406 let pole = mean.perpendicular().map_err(fail)?;
1407 let limit = (85.0_f64).to_radians().sin();
1408 let other = mean.cross(pole);
1411 let mut best = (f64::MAX, pole);
1412 for k in 0..36 {
1413 let angle = PI * k as f64 / 36.0;
1414 let candidate = pole.scale(angle.cos()).add(other.scale(angle.sin()));
1415 let worst = points
1416 .iter()
1417 .map(|p| (p.sub(center).dot(candidate) / radius).abs())
1418 .fold(0.0_f64, f64::max);
1419 if worst < best.0 {
1420 best = (worst, candidate);
1421 }
1422 }
1423 if best.0 > limit {
1424 return Err(refuse_unsupported(
1425 KernelStage::Refine,
1426 "sphere_extent",
1427 format!(
1428 "the spherical region reaches within 5 degrees of every pole this edit \
1429 can place; a region that large is {NOT_DISK_OR_ANNULUS} this edit \
1430 builds (v1)"
1431 ),
1432 ));
1433 }
1434 let pole = best.1;
1435 let seam = mean.scale(-1.0);
1436 let x_axis = seam.sub(pole.scale(seam.dot(pole))).normalized().map_err(fail)?;
1437 let meridian =
1438 make_arc(center, x_axis, pole, radius, -PI / 2.0, PI / 2.0).map_err(fail)?;
1439 make_revolution(center, pole, &meridian, TAU).map_err(fail)
1440 }
1441 RefitCarrier::Torus { center, axis, major_radius, minor_radius } => {
1442 if !(minor_radius > 0.0 && major_radius > minor_radius) {
1443 return Err(refuse_unsupported(
1444 KernelStage::Refine,
1445 "torus_radii",
1446 format!(
1447 "the fitted torus (radii {major_radius:.6}/{minor_radius:.6}) \
1448 self-intersects; {NO_FIT} as one face (v1)"
1449 ),
1450 ));
1451 }
1452 let (x, _) = seam_frame(axis, center, points)?;
1453 let mut angles: Vec<f64> = points
1455 .iter()
1456 .map(|p| {
1457 let rel = p.sub(center);
1458 let along = rel.dot(axis);
1459 let radial = rel.sub(axis.scale(along)).length() - major_radius;
1460 along.atan2(radial)
1461 })
1462 .collect();
1463 angles.sort_by(f64::total_cmp);
1464 let mut start = 0.0;
1465 if !angles.is_empty() {
1466 let mut best_gap = angles[0] + TAU - angles[angles.len() - 1];
1467 let mut best_mid = angles[angles.len() - 1] + best_gap * 0.5;
1468 for pair in angles.windows(2) {
1469 if pair[1] - pair[0] > best_gap {
1470 best_gap = pair[1] - pair[0];
1471 best_mid = pair[0] + best_gap * 0.5;
1472 }
1473 }
1474 start = best_mid;
1475 }
1476 let tube_center = center.add(x.scale(major_radius));
1477 let tube = make_arc(tube_center, x, axis, minor_radius, start, start + TAU)
1478 .map_err(fail)?;
1479 make_revolution(center, axis, &tube, TAU).map_err(fail)
1480 }
1481 }
1482}
1483
1484fn axial_range(origin: Vec3, axis: Vec3, points: &[Vec3]) -> (f64, f64) {
1485 points.iter().fold((f64::MAX, f64::MIN), |(lo, hi), p| {
1486 let h = p.sub(origin).dot(axis);
1487 (lo.min(h), hi.max(h))
1488 })
1489}
1490
1491fn region_same_sense(surface: &NurbsSurface, samples: &Samples) -> Result<bool, KernelRefusal> {
1492 let mut vote = 0.0;
1493 for (point, outward) in samples.oriented.iter().step_by((samples.oriented.len() / 64).max(1)) {
1494 let Ok(projection) = crate::project_point_to_surface(surface, *point) else { continue };
1495 let Ok(normal) = surface.normal(projection.u, projection.v) else { continue };
1496 vote += normal.dot(*outward).signum() * outward.length();
1497 }
1498 if vote == 0.0 {
1499 return Err(refuse_unsupported(
1500 KernelStage::Classify,
1501 "orientation",
1502 format!("{NO_FIT}: the selection's outward side could not be read"),
1503 ));
1504 }
1505 Ok(vote > 0.0)
1506}
1507
1508fn split_chains(cycle: &[BoundaryUse]) -> Vec<Chain> {
1513 let n = cycle.len();
1514 let Some(change) = (0..n).find(|&i| cycle[i].neighbour != cycle[(i + n - 1) % n].neighbour)
1515 else {
1516 return vec![Chain { uses: cycle.to_vec(), closed: true, neighbour: cycle[0].neighbour }];
1517 };
1518 let mut chains: Vec<Chain> = Vec::new();
1519 for k in 0..n {
1520 let use_ = cycle[(change + k) % n];
1521 match chains.last_mut() {
1522 Some(chain) if chain.neighbour == use_.neighbour => chain.uses.push(use_),
1523 _ => chains.push(Chain { uses: vec![use_], closed: false, neighbour: use_.neighbour }),
1524 }
1525 }
1526 chains
1527}
1528
1529fn chain_points(chain: &Chain, vertices: &HashMap<u64, Vec3>) -> Vec<Vec3> {
1530 let mut points: Vec<Vec3> = chain.uses.iter().map(|u| vertices[&u.start]).collect();
1531 if !chain.closed {
1532 points.push(vertices[&chain.uses.last().unwrap().end]);
1533 }
1534 points
1535}
1536
1537fn chain_trace_points(
1545 chain: &Chain,
1546 vertices: &HashMap<u64, Vec3>,
1547 edges: &HashMap<u64, &EdgeRecord>,
1548) -> Result<Vec<Vec3>, KernelRefusal> {
1549 let mut points = Vec::new();
1550 for use_ in &chain.uses {
1551 points.push(vertices[&use_.start]);
1552 let edge = edges[&use_.edge_id];
1553 if edge.curve.degree <= 1 {
1554 continue;
1555 }
1556 let mut parameters = Vec::new();
1557 for span in edge.curve.knots.windows(2) {
1558 let lo = span[0].max(edge.t0);
1559 let hi = span[1].min(edge.t1);
1560 if hi <= lo { continue; }
1561 for step in 0..4 {
1562 let t = lo + (hi - lo) * step as f64 / 4.0;
1563 if t > edge.t0 && t < edge.t1 { parameters.push(t); }
1564 }
1565 }
1566 if !use_.forward { parameters.reverse(); }
1567 for t in parameters {
1568 points.push(edge.curve.evaluate(t).map_err(|e| refuse_input("curve", e))?);
1569 }
1570 }
1571 if !chain.closed {
1572 points.push(vertices[&chain.uses.last().unwrap().end]);
1573 }
1574 Ok(points)
1575}
1576
1577fn curve_meeting(
1580 first: &NurbsCurve,
1581 second: &NurbsCurve,
1582 seed: Vec3,
1583) -> Result<(Vec3, f64), KernelRefusal> {
1584 let project = |curve: &NurbsCurve, p: Vec3| {
1585 project_point_to_curve(curve, p).map_err(|e| {
1586 refuse_unsupported(KernelStage::Intersect, "corner", format!("{NO_EXACT_BOUNDARY}: {e}"))
1587 })
1588 };
1589 let mut p = seed;
1590 let mut gap = f64::MAX;
1591 for _ in 0..200 {
1592 let a = project(first, p)?.point;
1593 let b = project(second, a)?.point;
1594 gap = a.sub(b).length();
1595 let next = a.add(b).scale(0.5);
1596 let step = next.sub(p).length();
1597 p = next;
1598 if step < 1e-14 * (1.0 + p.length()) {
1599 break;
1600 }
1601 }
1602 let a = project(first, p)?.point;
1603 let b = project(second, p)?.point;
1604 gap = gap.min(a.sub(b).length());
1605 Ok((a.add(b).scale(0.5), gap))
1606}
1607
1608fn as_circle(curve: &NurbsCurve, scale: f64) -> Option<(Vec3, Vec3, f64)> {
1610 let [t0, t1] = curve.domain().ok()?;
1611 let samples: Vec<Vec3> = (0..24)
1612 .filter_map(|k| curve.evaluate(t0 + (t1 - t0) * k as f64 / 24.0).ok())
1613 .collect();
1614 if samples.len() < 24 {
1615 return None;
1616 }
1617 let (a, b, c) = (samples[0], samples[8], samples[16]);
1618 let ab = b.sub(a);
1619 let ac = c.sub(a);
1620 let normal = ab.cross(ac);
1621 let center = crate::circumcenter(a, b, c)?;
1622 let radius = a.sub(center).length();
1623 let unit_normal = normal.normalized().ok()?;
1624 let tol = scale * 1e-9 + radius * 1e-9;
1625 for p in &samples {
1626 if (p.sub(center).length() - radius).abs() > tol || p.sub(center).dot(unit_normal).abs() > tol {
1627 return None;
1628 }
1629 }
1630 Some((center, unit_normal, radius))
1631}
1632
1633fn angle_in(center: Vec3, x: Vec3, y: Vec3, p: Vec3) -> f64 {
1634 let rel = p.sub(center);
1635 let angle = rel.dot(y).atan2(rel.dot(x));
1636 if angle < 0.0 {
1637 angle + TAU
1638 } else {
1639 angle
1640 }
1641}
1642
1643fn closed_ring_curve(
1646 curve: &NurbsCurve,
1647 points: &[Vec3],
1648 scale: f64,
1649) -> Result<(NurbsCurve, bool), KernelRefusal> {
1650 let fail = |e: String| {
1651 refuse_unsupported(KernelStage::Intersect, "ring", format!("{NO_EXACT_BOUNDARY}: {e}"))
1652 };
1653 if let Some((center, normal, radius)) = as_circle(curve, scale) {
1654 let start = points[0];
1655 let x = start.sub(center);
1656 let x = x.sub(normal.scale(x.dot(normal))).normalized().map_err(fail)?;
1657 let mut n = normal;
1658 let y = n.cross(x);
1659 if angle_in(center, x, y, points[1]) > PI {
1661 n = n.scale(-1.0);
1662 }
1663 let y = n.cross(x);
1664 let arc = make_arc(center, x, y, radius, 0.0, TAU).map_err(fail)?;
1665 return Ok((arc, true));
1666 }
1667 let p0 = project_point_to_curve(curve, points[0]).map_err(fail)?;
1669 let p1 = project_point_to_curve(curve, points[1]).map_err(fail)?;
1670 let [t0, t1] = curve.domain().map_err(fail)?;
1671 let mut delta = p1.u - p0.u;
1672 let period = t1 - t0;
1673 if delta > period * 0.5 {
1674 delta -= period;
1675 } else if delta < -period * 0.5 {
1676 delta += period;
1677 }
1678 Ok((curve.clone(), delta > 0.0))
1679}
1680
1681fn open_arc(
1684 curve: &NurbsCurve,
1685 start: u64,
1686 end: u64,
1687 start_point: Vec3,
1688 end_point: Vec3,
1689 mid_point: Vec3,
1690 scale: f64,
1691) -> Result<BuiltEdge, KernelRefusal> {
1692 let fail = |e: String| {
1693 refuse_unsupported(KernelStage::Intersect, "arc", format!("{NO_EXACT_BOUNDARY}: {e}"))
1694 };
1695 if let Some((center, normal, radius)) = as_circle(curve, scale) {
1696 let x = start_point.sub(center);
1697 let x = x.sub(normal.scale(x.dot(normal))).normalized().map_err(fail)?;
1698 let mut n = normal;
1699 let mut y = n.cross(x);
1700 let mut sweep = angle_in(center, x, y, end_point);
1701 if angle_in(center, x, y, mid_point) > sweep {
1702 n = n.scale(-1.0);
1703 y = n.cross(x);
1704 sweep = angle_in(center, x, y, end_point);
1705 }
1706 let arc = make_arc(center, x, y, radius, 0.0, sweep).map_err(fail)?;
1707 let [t0, t1] = arc.domain().map_err(fail)?;
1708 return Ok(BuiltEdge { curve: arc, t0, t1, start, end, region_forward: true, region_pcurve: None });
1709 }
1710 let a = project_point_to_curve(curve, start_point).map_err(fail)?.u;
1711 let b = project_point_to_curve(curve, end_point).map_err(fail)?.u;
1712 let m = project_point_to_curve(curve, mid_point).map_err(fail)?.u;
1713 if !((a < m && m < b) || (b < m && m < a)) {
1714 return Err(refuse_unsupported(
1715 KernelStage::Intersect,
1716 "arc_seam",
1717 format!(
1718 "{NO_EXACT_BOUNDARY}: a boundary run crosses its intersection curve's seam (v1)"
1719 ),
1720 ));
1721 }
1722 let (lo, hi) = if a < b { (a, b) } else { (b, a) };
1723 let trimmed = crate::feature_pipeline::features::common::trimmed_curve(curve, lo, hi)
1724 .map_err(fail)?;
1725 let [t0, t1] = trimmed.domain().map_err(fail)?;
1726 if a < b {
1727 Ok(BuiltEdge { curve: trimmed, t0, t1, start, end, region_forward: true, region_pcurve: None })
1728 } else {
1729 Ok(BuiltEdge {
1730 curve: trimmed,
1731 t0,
1732 t1,
1733 start: end,
1734 end: start,
1735 region_forward: false,
1736 region_pcurve: None,
1737 })
1738 }
1739}
1740
1741fn iso_ring(
1748 surface: &NurbsSurface,
1749 intersection: &NurbsCurve,
1750 points: &[Vec3],
1751 tolerance: f64,
1752) -> Option<(NurbsCurve, NurbsCurve, bool)> {
1753 let uv: Vec<(f64, f64)> = points
1754 .iter()
1755 .map(|p| crate::project_point_to_surface(surface, *p).map(|q| (q.u, q.v)))
1756 .collect::<Result<_, _>>()
1757 .ok()?;
1758 if uv.len() < 3 {
1759 return None;
1760 }
1761 let [u0, u1] = [surface.knots_u[0], *surface.knots_u.last()?];
1762 let [v0, v1] = [surface.knots_v[0], *surface.knots_v.last()?];
1763 let mean_u = uv.iter().map(|q| q.0).sum::<f64>() / uv.len() as f64;
1764 let mean_v = uv.iter().map(|q| q.1).sum::<f64>() / uv.len() as f64;
1765 let wrap = |delta: f64, period: f64| {
1766 let mut d = delta % period;
1767 if d > period * 0.5 {
1768 d -= period;
1769 } else if d < -period * 0.5 {
1770 d += period;
1771 }
1772 d
1773 };
1774 let candidates = [
1775 (surface.iso_curve_v(mean_v).ok(), true),
1777 (surface.iso_curve_u(mean_u).ok(), false),
1779 ];
1780 for (curve, along_u) in candidates {
1781 let Some(curve) = curve else { continue };
1782 let [t0, t1] = curve.domain().ok()?;
1783 let (Ok(start), Ok(end)) = (curve.evaluate(t0), curve.evaluate(t1)) else { continue };
1784 if start.sub(end).length() > tolerance * 1e-3 {
1785 continue; }
1787 let on_intersection = (0..16).all(|k| {
1788 curve
1789 .evaluate(t0 + (t1 - t0) * (k as f64 + 0.5) / 16.0)
1790 .ok()
1791 .and_then(|p| project_point_to_curve(intersection, p).ok())
1792 .is_some_and(|q| q.distance <= tolerance)
1793 });
1794 let through_points =
1795 points.iter().all(|p| project_point_to_curve(&curve, *p).is_ok_and(|q| q.distance <= tolerance));
1796 if !(on_intersection && through_points) {
1797 continue;
1798 }
1799 let (period, a, b) = if along_u {
1800 (u1 - u0, uv[0].0, uv[1].0)
1801 } else {
1802 (v1 - v0, uv[0].1, uv[1].1)
1803 };
1804 let forward = wrap(b - a, period) > 0.0;
1805 let (from, to) = if along_u {
1806 ([u0, mean_v], [u1, mean_v])
1807 } else {
1808 ([mean_u, v0], [mean_u, v1])
1809 };
1810 let (from, to) = if forward { (from, to) } else { (to, from) };
1811 let pcurve =
1812 make_line(Vec3::new(from[0], from[1], 0.0), Vec3::new(to[0], to[1], 0.0)).ok()?;
1813 return Some((curve, pcurve, forward));
1814 }
1815 None
1816}
1817
1818fn iso_curve_on(
1823 surface: &NurbsSurface,
1824 other: &NurbsSurface,
1825 points: &[Vec3],
1826 tolerance: f64,
1827) -> Vec<NurbsCurve> {
1828 let Ok(uv) = points
1829 .iter()
1830 .map(|p| crate::project_point_to_surface(surface, *p).map(|q| (q.u, q.v)))
1831 .collect::<Result<Vec<_>, _>>()
1832 else {
1833 return Vec::new();
1834 };
1835 if uv.is_empty() {
1836 return Vec::new();
1837 }
1838 let mean_u = uv.iter().map(|q| q.0).sum::<f64>() / uv.len() as f64;
1839 let mean_v = uv.iter().map(|q| q.1).sum::<f64>() / uv.len() as f64;
1840 let mut out = Vec::new();
1841 for curve in [surface.iso_curve_v(mean_v), surface.iso_curve_u(mean_u)].into_iter().flatten() {
1842 let Ok([t0, t1]) = curve.domain() else { continue };
1843 let through = points
1844 .iter()
1845 .all(|p| project_point_to_curve(&curve, *p).is_ok_and(|q| q.distance <= tolerance));
1846 let on_other = (0..=32).all(|k| {
1847 curve
1848 .evaluate(t0 + (t1 - t0) * k as f64 / 32.0)
1849 .ok()
1850 .is_some_and(|p| {
1851 if let Some(crate::AnalyticSurface::Plane { origin, u_dir, v_dir, .. }) =
1855 other.analytic()
1856 {
1857 u_dir.cross(*v_dir).normalized()
1858 .is_ok_and(|n| p.sub(*origin).dot(n).abs() <= tolerance)
1859 } else {
1860 crate::project_point_to_surface(other, p).is_ok_and(|q| q.distance <= tolerance)
1861 }
1862 })
1863 });
1864 if through && on_other {
1865 out.push(curve);
1866 }
1867 }
1868 out
1869}
1870
1871fn reseated_plane(old: &NurbsSurface, ring: &NurbsCurve, scale: f64) -> Option<NurbsSurface> {
1876 let [t0, t1] = ring.domain().ok()?;
1877 let points: Vec<Vec3> =
1878 (0..48).filter_map(|k| ring.evaluate(t0 + (t1 - t0) * k as f64 / 48.0).ok()).collect();
1879 if points.len() < 48 {
1880 return None;
1881 }
1882 let centroid = points
1883 .iter()
1884 .fold(Vec3::new(0.0, 0.0, 0.0), |s, p| s.add(*p))
1885 .scale(1.0 / points.len() as f64);
1886 let mut normal = Vec3::new(0.0, 0.0, 0.0);
1888 for (i, a) in points.iter().enumerate() {
1889 let b = points[(i + 1) % points.len()];
1890 normal = normal.add(a.sub(centroid).cross(b.sub(centroid)));
1891 }
1892 let normal = normal.normalized().ok()?;
1893 let flat = points.iter().all(|p| p.sub(centroid).dot(normal).abs() <= scale * 1e-10);
1894 if !flat {
1895 return None;
1896 }
1897 let [u0, u1] = [old.knots_u[0], *old.knots_u.last()?];
1898 let [v0, v1] = [old.knots_v[0], *old.knots_v.last()?];
1899 let origin = old.evaluate(u0, v0).ok()?;
1900 let su = old.evaluate(u1, v0).ok()?.sub(origin);
1901 let sv = old.evaluate(u0, v1).ok()?.sub(origin);
1902 let old_normal = su.cross(sv).normalized().ok()?;
1903 let off = points.iter().map(|p| p.sub(origin).dot(old_normal).abs()).fold(0.0, f64::max);
1904 if off <= scale * 1e-12 {
1905 return None;
1906 }
1907 let n = if normal.dot(old_normal) >= 0.0 { normal } else { normal.scale(-1.0) };
1908 let u_dir = su.sub(n.scale(su.dot(n))).normalized().ok()?;
1909 let v_dir = n.cross(u_dir);
1910 let mut lo = [f64::MAX; 2];
1912 let mut hi = [f64::MIN; 2];
1913 let corners = [origin, origin.add(su), origin.add(sv), origin.add(su).add(sv)];
1914 for p in points.iter().chain(corners.iter()) {
1915 let rel = p.sub(centroid);
1916 let (a, b) = (rel.dot(u_dir), rel.dot(v_dir));
1917 lo = [lo[0].min(a), lo[1].min(b)];
1918 hi = [hi[0].max(a), hi[1].max(b)];
1919 }
1920 let margin = (hi[0] - lo[0]).max(hi[1] - lo[1]) * 0.05;
1921 let corner = centroid.add(u_dir.scale(lo[0] - margin)).add(v_dir.scale(lo[1] - margin));
1922 make_plane(corner, u_dir, v_dir, hi[0] - lo[0] + 2.0 * margin, hi[1] - lo[1] + 2.0 * margin).ok()
1923}
1924
1925#[allow(clippy::too_many_arguments)]
1930fn refit_closed_shell(
1931 solid: &BrepSolid,
1932 shell_index: usize,
1933 selected_indices: &[usize],
1934 euler: i64,
1935 kind: RefitSurfaceKind,
1936 tolerance: f64,
1937 scale: f64,
1938 edge_by_id: &HashMap<u64, &EdgeRecord>,
1939 vertex_by_id: &HashMap<u64, Vec3>,
1940) -> Result<(BrepSolid, RefitReport), KernelRefusal> {
1941 let shell = &solid.shells[shell_index];
1942 if selected_indices.len() != shell.faces.len() {
1943 return Err(refuse_unsupported(
1944 KernelStage::Classify,
1945 "closed_region",
1946 format!(
1947 "the selection has no boundary but is not every face of its shell; the region \
1948 is {NOT_DISK_OR_ANNULUS}"
1949 ),
1950 ));
1951 }
1952 let kind = match kind {
1955 RefitSurfaceKind::Auto => match euler {
1956 2 => RefitSurfaceKind::Sphere,
1957 0 => RefitSurfaceKind::Torus,
1958 _ => {
1959 return Err(refuse_unsupported(
1960 KernelStage::Classify,
1961 "closed_shell_genus",
1962 format!(
1963 "the selected closed shell has Euler characteristic {euler}; \
1964 {CLOSED_SHELL_KIND} (2 or 0)"
1965 ),
1966 ))
1967 }
1968 },
1969 RefitSurfaceKind::Plane | RefitSurfaceKind::Cylinder | RefitSurfaceKind::Cone => {
1970 return Err(refuse_unsupported(
1971 KernelStage::Classify,
1972 "closed_shell_kind",
1973 format!(
1974 "the selection is a whole closed shell and no {} can be one: \
1975 {CLOSED_SHELL_KIND}",
1976 kind.name()
1977 ),
1978 ))
1979 }
1980 other => other,
1981 };
1982 let wanted = if kind == RefitSurfaceKind::Sphere { 2 } else { 0 };
1983 if euler != wanted {
1984 return Err(refuse_unsupported(
1985 KernelStage::Classify,
1986 "closed_shell_genus",
1987 format!(
1988 "a {} {CLOSED_SHELL_GENUS}: the selected closed shell has Euler characteristic \
1989 {euler}, a {} has {wanted}",
1990 kind.name(),
1991 kind.name()
1992 ),
1993 ));
1994 }
1995
1996 let samples = fit_samples(shell, selected_indices, edge_by_id, vertex_by_id)?;
1997 let carrier = fit_carrier(&samples, kind, tolerance, scale)?.carrier;
1998 let (rms_residual, max_residual) = residuals(&carrier, &samples.points);
1999 if max_residual > tolerance {
2000 return Err(KernelRefusal::ill_posed(
2001 KernelStage::Refine,
2002 "refit_residual",
2003 format!(
2004 "Refit Faces: the {} {RESIDUAL_OVER_TOLERANCE} {max_residual:.6} (rms \
2005 {rms_residual:.6}) is over the tolerance {tolerance}; these faces are not \
2006 one {} within it",
2007 carrier.kind().name(),
2008 carrier.kind().name()
2009 ),
2010 ));
2011 }
2012 coarse_facet_gate(&carrier, shell, selected_indices, edge_by_id, vertex_by_id)?;
2013 let fail = |e: String| {
2014 refuse_unsupported(KernelStage::Refine, "refit_surface", format!("{NO_FIT}: {e}"))
2015 };
2016 let primitive = match carrier {
2017 RefitCarrier::Sphere { center, radius } => {
2018 crate::make_sphere_brep(center, radius, Vec3::new(0.0, 0.0, 1.0)).map_err(fail)?
2021 }
2022 RefitCarrier::Torus { center, axis, major_radius, minor_radius } => {
2023 if !(minor_radius > 0.0 && major_radius > minor_radius) {
2024 return Err(refuse_unsupported(
2025 KernelStage::Refine,
2026 "torus_radii",
2027 format!(
2028 "the fitted torus (radii {major_radius:.6}/{minor_radius:.6}) \
2029 self-intersects; {NO_FIT} as one face (v1)"
2030 ),
2031 ));
2032 }
2033 crate::make_torus_brep(center, axis, major_radius, minor_radius).map_err(fail)?
2034 }
2035 other => {
2036 return Err(refuse_unsupported(
2037 KernelStage::Classify,
2038 "closed_shell_kind",
2039 format!(
2040 "the fit returned a {}; {CLOSED_SHELL_KIND}",
2041 other.kind().name()
2042 ),
2043 ))
2044 }
2045 };
2046
2047 let prim_face = &primitive.shells[0].faces[0];
2050 let vote = region_same_sense(&prim_face.surface, &samples)?;
2051 let cavity = vote != prim_face.same_sense;
2052
2053 let base = solid
2055 .vertices
2056 .iter()
2057 .map(|v| v.id)
2058 .chain(solid.edges.iter().map(|e| e.id))
2059 .chain(solid.shells.iter().map(|s| s.id))
2060 .chain(solid.shells.iter().flat_map(|s| s.faces.iter()).flat_map(|f| {
2061 std::iter::once(f.id).chain(
2062 f.loops
2063 .iter()
2064 .flat_map(|l| std::iter::once(l.id).chain(l.coedges.iter().map(|c| c.id))),
2065 )
2066 }))
2067 .max()
2068 .unwrap_or(0)
2069 + 1;
2070 let keeper = &shell.faces[selected_indices[0]];
2071 let mut new_shell = primitive.shells[0].clone();
2072 new_shell.id = shell.id;
2073 for face in &mut new_shell.faces {
2074 face.id = keeper.id;
2075 face.name = keeper.name.clone();
2076 for lp in &mut face.loops {
2077 lp.id += base;
2078 for coedge in &mut lp.coedges {
2079 coedge.id += base;
2080 coedge.edge_id += base;
2081 }
2082 }
2083 if cavity {
2084 flip_face(face).map_err(fail)?;
2085 }
2086 }
2087 let new_edges: Vec<EdgeRecord> = primitive
2088 .edges
2089 .iter()
2090 .map(|e| {
2091 let mut e = e.clone();
2092 e.id += base;
2093 e.start_vertex_id += base;
2094 e.end_vertex_id += base;
2095 e
2096 })
2097 .collect();
2098 let new_vertices: Vec<VertexRecord> = primitive
2099 .vertices
2100 .iter()
2101 .map(|v| VertexRecord { id: v.id + base, point: v.point })
2102 .collect();
2103
2104 let old_edges: HashSet<u64> = shell
2106 .faces
2107 .iter()
2108 .flat_map(|f| f.loops.iter())
2109 .flat_map(|l| l.coedges.iter().map(|c| c.edge_id))
2110 .collect();
2111 let mut out = solid.clone();
2112 out.edges.retain(|e| !old_edges.contains(&e.id));
2113 out.edges.extend(new_edges);
2114 out.shells[shell_index] = new_shell;
2115 let used: HashSet<u64> =
2116 out.edges.iter().flat_map(|e| [e.start_vertex_id, e.end_vertex_id]).collect();
2117 out.vertices.retain(|v| used.contains(&v.id));
2118 out.vertices.extend(new_vertices);
2119 let issues = out.validate();
2122 if !issues.is_empty() {
2123 let first: Vec<String> = issues.iter().take(3).map(|i| format!("{i:?}")).collect();
2124 return Err(KernelRefusal::new(
2125 RefusalClass::InvalidResultTopology { issues: issues.len() as u32 },
2126 KernelStage::Validate,
2127 format!(
2128 "Refit Faces: the {RESULT_INVALID} ({} issue(s): {})",
2129 issues.len(),
2130 first.join("; ")
2131 ),
2132 ));
2133 }
2134 let report = RefitReport {
2135 carrier,
2136 rms_residual,
2137 max_residual,
2138 tolerance,
2139 samples: samples.points.len(),
2140 replaced_faces: selected_indices.len(),
2141 boundary_edges_before: 0,
2142 boundary_edges_after: 0,
2143 boundary_loops: 0,
2144 new_face_id: keeper.id,
2145 };
2146 Ok((out, report))
2147}
2148
2149pub(super) fn flip_face(face: &mut FaceRecord) -> Result<(), String> {
2152 face.same_sense = !face.same_sense;
2153 for lp in &mut face.loops {
2154 lp.coedges.reverse();
2155 for coedge in &mut lp.coedges {
2156 coedge.forward = !coedge.forward;
2157 coedge.pcurve = coedge.pcurve.reversed()?;
2158 }
2159 }
2160 Ok(())
2161}
2162
2163pub const MAX_FACET_SPAN_DEGREES: f64 = 60.0;
2175
2176pub const FACET_SPAN_SLACK_DEGREES: f64 = 0.1;
2180
2181fn coarse_facet_gate(
2186 carrier: &RefitCarrier,
2187 shell: &crate::topology::ShellRecord,
2188 selected: &[usize],
2189 edges: &HashMap<u64, &EdgeRecord>,
2190 vertices: &HashMap<u64, Vec3>,
2191) -> Result<(), KernelRefusal> {
2192 if matches!(carrier, RefitCarrier::Plane { .. }) {
2193 return Ok(());
2194 }
2195 let mut worst: Option<(f64, &FaceRecord)> = None;
2196 for &f in selected {
2197 let face = &shell.faces[f];
2198 if !face.surface.is_affine().unwrap_or(false) {
2199 continue;
2200 }
2201 let mut sum = Vec3::new(0.0, 0.0, 0.0);
2202 let mut count = 0usize;
2203 for coedge in face.loops.iter().flat_map(|l| l.coedges.iter()) {
2204 let Some(edge) = edges.get(&coedge.edge_id) else { continue };
2205 let id = if coedge.forward { edge.start_vertex_id } else { edge.end_vertex_id };
2206 if let Some(p) = vertices.get(&id) {
2207 sum = sum.add(*p);
2208 count += 1;
2209 }
2210 }
2211 if count < 3 {
2212 continue;
2213 }
2214 let centroid = sum.scale(1.0 / count as f64);
2215 let radius = local_radius(carrier, centroid);
2216 if !(radius > 0.0) {
2217 continue;
2218 }
2219 let sag = carrier.distance(centroid);
2220 let span = 2.0 * (1.0 - (sag / radius).min(2.0)).clamp(-1.0, 1.0).acos().to_degrees();
2221 if worst.map_or(true, |(w, _)| span > w) {
2222 worst = Some((span, face));
2223 }
2224 }
2225 match worst {
2226 Some((span, face)) if span > MAX_FACET_SPAN_DEGREES + FACET_SPAN_SLACK_DEGREES => {
2227 Err(KernelRefusal::ill_posed(
2228 KernelStage::Refine,
2229 "refit_facets_coarse",
2230 format!(
2231 "Refit Faces: the selection is {FACETS_TOO_COARSE} to be one {}: {} spans \
2232 {span:.1} degrees of the fitted surface (at most {MAX_FACET_SPAN_DEGREES}); \
2233 a facet may be no wider than the radius it approximates",
2234 carrier.kind().name(),
2235 face_label(face)
2236 ),
2237 ))
2238 }
2239 _ => Ok(()),
2240 }
2241}
2242
2243fn local_radius(carrier: &RefitCarrier, point: Vec3) -> f64 {
2245 match *carrier {
2246 RefitCarrier::Plane { .. } => f64::INFINITY,
2247 RefitCarrier::Cylinder { radius, .. } => radius,
2248 RefitCarrier::Sphere { radius, .. } => radius,
2249 RefitCarrier::Torus { minor_radius, .. } => minor_radius,
2250 RefitCarrier::Cone { apex, axis, half_angle } => {
2251 let rel = point.sub(apex);
2252 let radial = rel.sub(axis.scale(rel.dot(axis))).length();
2253 radial / half_angle.cos()
2254 }
2255 }
2256}
2257