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