1use crate::{OgeomResult, Tolerances, ogeom_bail};
57use ogeom_algo::{Built, History, make_edge_between, make_solid, make_vertex, sew};
58use ogeom_core::ogeom_err;
59use ogeom_geom::Curve3d as _;
60use ogeom_geom::Transformable as _;
61use ogeom_geom::{Curve, SurfaceGeometry};
62use ogeom_intersect::{
63 CurveSurfaceOptions, IntersectOptions, SurfaceIntersection, intersect_curve_surface,
64 intersect_surfaces,
65};
66use ogeom_math::Point;
67use ogeom_topo::{Filter, Model, NodeData, Shape, ShapeType, TShapeId, explore};
68use std::collections::{HashMap, HashSet};
69
70pub fn remove_faces(
81 model: &mut Model,
82 solid: &Shape,
83 faces: &[Shape],
84 tol: Tolerances,
85) -> OgeomResult<Built> {
86 if faces.is_empty() {
87 ogeom_bail!(Construction, "no faces named; there is nothing to remove");
88 }
89 let groups = feature_groups(model, faces)?;
97 if groups.len() > 1 {
98 let mut current = Built::from_nothing(solid.clone());
99 for group in &groups {
100 let step = remove_faces(model, ¤t.shape, group, tol)?;
103 current = Built {
104 shape: step.shape,
105 history: current.history.then(&step.history),
106 };
107 }
108 return Ok(current);
109 }
110 let all_faces = explore(model, solid, Filter::OfType(ShapeType::Face))?;
111 let removed: HashSet<TShapeId> = faces.iter().map(Shape::node).collect();
112 for face in faces {
113 if !all_faces.iter().any(|f| f.node() == face.node()) {
114 ogeom_bail!(
115 Construction,
116 "a face named for removal is not a face of this solid"
117 );
118 }
119 }
120 let survivors: Vec<Shape> = all_faces
121 .iter()
122 .filter(|f| !removed.contains(&f.node()))
123 .cloned()
124 .collect();
125 if survivors.is_empty() {
126 ogeom_bail!(
127 Construction,
128 "every face was named for removal; nothing remains to close"
129 );
130 }
131
132 let mut users: HashMap<TShapeId, Vec<Shape>> = HashMap::new();
135 for face in &all_faces {
136 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
137 users.entry(edge.node()).or_default().push(face.clone());
138 }
139 }
140 let is_ring = |edge: &Shape| -> bool {
141 users.get(&edge.node()).is_some_and(|fs| {
142 fs.iter().any(|f| removed.contains(&f.node()))
143 && fs.iter().any(|f| !removed.contains(&f.node()))
144 })
145 };
146
147 struct Touched {
150 face: Shape,
151 kept: Vec<Shape>,
153 whole: bool,
155 partial: bool,
157 outer: bool,
159 }
160 let mut untouched: Vec<Shape> = Vec::new();
161 let mut touched: Vec<Touched> = Vec::new();
162 for face in &survivors {
163 let wires = model.ordered_children_of(face)?;
164 let mut kept = Vec::new();
165 let (mut whole, mut partial, mut outer) = (false, false, false);
166 for (index, wire) in wires.iter().enumerate() {
167 let edges = model.ordered_children_of(wire)?;
168 let ring_count = edges.iter().filter(|e| is_ring(e)).count();
169 if ring_count == 0 {
170 kept.push(wire.clone());
171 } else if ring_count == edges.len() {
172 whole = true;
173 outer |= index == 0;
174 } else {
175 partial = true;
176 }
177 }
178 if whole || partial {
179 touched.push(Touched {
180 face: face.clone(),
181 kept,
182 whole,
183 partial,
184 outer,
185 });
186 } else {
187 untouched.push(face.clone());
188 }
189 }
190
191 let sides: Vec<Shape> = touched.iter().map(|t| t.face.clone()).collect();
197 let recovers = touched.iter().any(|t| t.whole) && wound_recovers(model, faces, &sides, tol)?;
198 let mut rim_surgery: Vec<(Shape, Vec<Shape>)> = Vec::new(); let mut interrupted: Vec<Shape> = Vec::new();
200 for entry in touched {
201 if entry.partial || recovers {
202 interrupted.push(entry.face);
203 } else {
204 if entry.outer {
208 ogeom_bail!(
209 Construction,
210 "removing these faces erases a neighbour's whole outer \
211 boundary; that face has nothing left to stand on"
212 );
213 }
214 rim_surgery.push((entry.face, entry.kept));
215 }
216 }
217
218 let mut history = History::new();
219 for face in faces {
220 history.delete(face);
221 }
222
223 let mut rebuilt: Vec<Shape> = untouched;
224 for (face, kept_wires) in rim_surgery {
225 let new_face = {
226 let Some(data) = model.node(&face).and_then(|n| match n.data() {
227 NodeData::Face(d) => Some(d.clone()),
228 _ => None,
229 }) else {
230 ogeom_bail!(Construction, "a surviving face holds no face data");
231 };
232 let built = ogeom_algo::make_face_on(model, data.surface, &kept_wires, tol)?.shape;
235 orient_like(&face, built)
236 };
237 history.modify(&face, new_face.clone());
238 rebuilt.push(new_face);
239 }
240
241 if !interrupted.is_empty() {
242 let band = close_wound(model, faces, &interrupted, &removed, &users, &is_ring, tol)?;
243 for (old, new) in band {
244 history.modify(&old, new.clone());
245 rebuilt.push(new);
246 }
247 }
248
249 let sewn = sew(model, &rebuilt, tol)?;
250 let [shell] = sewn.shells.as_slice() else {
251 ogeom_bail!(
252 Construction,
253 "closing the wound left {} shells where one solid's worth was \
254 expected; the removal disconnected the boundary",
255 sewn.shells.len()
256 );
257 };
258 if !ogeom_algo::is_shell_closed(model, shell)? {
259 ogeom_bail!(
260 Construction,
261 "the boundary does not close after removal; the wound needs a \
262 closure this operation does not construct yet"
263 );
264 }
265 let built = make_solid(model, std::slice::from_ref(shell))?;
266 let mut solid_history = history;
267 solid_history.modify(solid, built.shape.clone());
268 Ok(Built::new(built.shape, solid_history))
269}
270
271fn placed_surface(model: &Model, face: &Shape, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
273 let placement = face.transform(model.datums())?;
274 let Some(data) = model.node(face).and_then(|n| n.data().as_face().cloned()) else {
275 ogeom_bail!(Construction, "a band face holds no face data");
276 };
277 let Some(surface) = model.geometry().surface(data.surface) else {
278 ogeom_bail!(Construction, "a band face's surface is not in this model");
279 };
280 surface.clone().transformed(&placement, tol)
281}
282
283fn wound_recovers(
301 model: &Model,
302 removed_faces: &[Shape],
303 candidates: &[Shape],
304 tol: Tolerances,
305) -> OgeomResult<bool> {
306 let mut room = ogeom_math::Aabb::default();
307 for face in removed_faces {
308 room = room.union(&ogeom_algo::shape_bounds(model, face, tol)?);
309 }
310 let room = room.expanded(tol.confusion() * 1e3);
311 let Some(centre) = room.centre() else {
312 return Ok(false);
313 };
314 for (i, first) in candidates.iter().enumerate() {
315 let sa = placed_surface(model, first, tol)?;
316 for second in &candidates[i + 1..] {
317 let sb = placed_surface(model, second, tol)?;
318 let Ok(SurfaceIntersection::Along(sections)) =
319 intersect_surfaces(&sa, &sb, IntersectOptions::default(), tol)
320 else {
321 continue;
322 };
323 for section in sections {
324 let foot = ogeom_algo::project_on_curve(§ion.curve, centre, 64, tol)?;
325 if room.contains(foot.point) {
326 return Ok(true);
327 }
328 }
329 }
330 }
331 Ok(false)
332}
333
334struct Crease {
337 sides: [Shape; 2],
339 curve: Curve,
341 extent: (f64, f64),
344 anchor: f64,
348}
349
350fn unwrapped(curve: &Curve, t: f64, about: f64) -> f64 {
353 if !curve.is_periodic() {
354 return t;
355 }
356 let (lo, hi) = curve.domain();
357 let period = hi - lo;
358 if period <= 0.0 {
359 return t;
360 }
361 about + (t - about + period / 2.0).rem_euclid(period) - period / 2.0
362}
363
364#[allow(clippy::too_many_lines, reason = "one wound, one narrative")]
379fn close_wound(
380 model: &mut Model,
381 removed_faces: &[Shape],
382 interrupted: &[Shape],
383 removed: &HashSet<TShapeId>,
384 users: &HashMap<TShapeId, Vec<Shape>>,
385 is_ring: &dyn Fn(&Shape) -> bool,
386 tol: Tolerances,
387) -> OgeomResult<Vec<(Shape, Shape)>> {
388 let surface_of = |model: &Model, face: &Shape| placed_surface(model, face, tol);
389 let vertices_of = |model: &Model, face: &Shape| -> OgeomResult<Vec<Point>> {
390 let mut out = Vec::new();
391 for vertex in explore(model, face, Filter::OfType(ShapeType::Vertex))? {
392 let placement = vertex.transform(model.datums())?;
393 if let Some(d) = model.node(&vertex).and_then(|nd| nd.data().as_vertex()) {
394 out.push(placement.apply(d.point));
395 }
396 }
397 Ok(out)
398 };
399 let interrupted_by_node: HashMap<TShapeId, Shape> =
400 interrupted.iter().map(|f| (f.node(), f.clone())).collect();
401
402 let mut creases: Vec<(TShapeId, TShapeId, [Shape; 2], Vec<Point>)> = Vec::new();
409 let mut crease_of: HashMap<TShapeId, usize> = HashMap::new();
410 let mut leftovers: Vec<Shape> = Vec::new();
411 for face in removed_faces {
412 let mut shared: HashMap<TShapeId, f64> = HashMap::new();
413 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
414 if !is_ring(&edge) {
415 continue;
416 }
417 let length = edge_length(model, &edge, tol)?;
418 for user in users.get(&edge.node()).into_iter().flatten() {
419 if !removed.contains(&user.node()) && interrupted_by_node.contains_key(&user.node())
420 {
421 *shared.entry(user.node()).or_default() += length;
422 }
423 }
424 }
425 let mut ranked: Vec<(TShapeId, f64)> = shared.into_iter().collect();
426 ranked.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.0.index().cmp(&b.0.index())));
427 let [(a, _), (b, _), ..] = ranked.as_slice() else {
428 leftovers.push(face.clone());
429 continue;
430 };
431 let (lo, hi) = if a.index() <= b.index() {
432 (*a, *b)
433 } else {
434 (*b, *a)
435 };
436 let points = vertices_of(model, face)?;
437 let index = match creases.iter().position(|c| c.0 == lo && c.1 == hi) {
438 Some(i) => {
439 creases[i].3.extend(points);
440 i
441 }
442 None => {
443 creases.push((
444 lo,
445 hi,
446 [
447 interrupted_by_node[&lo].clone(),
448 interrupted_by_node[&hi].clone(),
449 ],
450 points,
451 ));
452 creases.len() - 1
453 }
454 };
455 crease_of.insert(face.node(), index);
456 }
457 for face in leftovers {
458 let mut joined = None;
459 for edge in explore(model, &face, Filter::OfType(ShapeType::Edge))? {
460 for user in users.get(&edge.node()).into_iter().flatten() {
461 if let Some(&index) = crease_of.get(&user.node()) {
462 joined = Some(index);
463 }
464 }
465 }
466 let Some(index) = joined else {
467 ogeom_bail!(
468 Construction,
469 "a removed face shares ring edges with fewer than two \
470 interrupted neighbours and borders no removed face with a \
471 crease; closing it needs a neighbour to meet itself, which \
472 is not constructed yet"
473 );
474 };
475 let points = vertices_of(model, &face)?;
476 creases[index].3.extend(points);
477 }
478
479 let mut recovered: Vec<Crease> = Vec::new();
481 for (_, _, sides, points) in creases {
482 let sa = surface_of(model, &sides[0])?;
483 let sb = surface_of(model, &sides[1])?;
484 let meeting = intersect_surfaces(&sa, &sb, IntersectOptions::default(), tol)?;
485 let SurfaceIntersection::Along(sections) = meeting else {
486 ogeom_bail!(
487 Construction,
488 "the band's side surfaces do not meet along a curve; the edge \
489 the feature replaced cannot be recovered from them"
490 );
491 };
492 let anchor = {
493 let mut sum = ogeom_math::Vector::ZERO;
494 for p in &points {
495 sum += p.to_vector();
496 }
497 #[allow(clippy::cast_precision_loss)]
498 let n = points.len().max(1) as f64;
499 Point::ORIGIN + sum * (1.0 / n)
500 };
501 let section = sections
502 .into_iter()
503 .min_by(|p, q| {
504 nearest_distance(&p.curve, anchor, tol)
505 .total_cmp(&nearest_distance(&q.curve, anchor, tol))
506 })
507 .ok_or_else(|| ogeom_err!(Construction, "the side surfaces meet along no branch"))?;
508 let curve = section.curve;
509 let anchor = parameter_near(&curve, anchor, tol)?;
510 let mut extent = (f64::INFINITY, f64::NEG_INFINITY);
511 for p in &points {
512 let t = unwrapped(&curve, parameter_near(&curve, *p, tol)?, anchor);
513 extent = (extent.0.min(t), extent.1.max(t));
514 }
515 recovered.push(Crease {
516 sides,
517 curve,
518 extent,
519 anchor,
520 });
521 }
522
523 let mut corner_vertices: Vec<(Point, Shape)> = Vec::new();
527 let mut vertex_at = |model: &mut Model, p: Point| -> Shape {
528 if let Some((_, v)) = corner_vertices
529 .iter()
530 .find(|(q, _)| q.distance(p) <= tol.confusion() * 1e3)
531 {
532 return v.clone();
533 }
534 let v = make_vertex(model, p).shape;
535 corner_vertices.push((p, v.clone()));
536 v
537 };
538 let mut new_edges: Vec<(Shape, [TShapeId; 2])> = Vec::new(); let mut corners: Vec<Shape> = Vec::new();
540 for (index, crease) in recovered.iter().enumerate() {
541 let mut piercings: Vec<(f64, Point)> = Vec::new();
542 for face in interrupted {
543 if crease.sides.iter().any(|s| s.node() == face.node()) {
544 continue;
545 }
546 let se = surface_of(model, face)?;
547 let hit =
548 intersect_curve_surface(&crease.curve, &se, CurveSurfaceOptions::default(), tol)?;
549 for c in &hit.crossings {
550 piercings.push((unwrapped(&crease.curve, c.on_curve, crease.anchor), c.point));
551 }
552 }
553 for face in removed_faces {
566 if crease_of.get(&face.node()) != Some(&index) {
567 continue;
568 }
569 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
570 let Some(other) = users
571 .get(&edge.node())
572 .into_iter()
573 .flatten()
574 .filter(|user| user.node() != face.node())
575 .filter_map(|user| crease_of.get(&user.node()).copied())
576 .find(|&other| other != index)
577 else {
578 continue;
579 };
580 let samples = sample_edge(model, &edge, tol)?;
581 let Some(&middle) = samples.get(samples.len() / 2) else {
582 continue;
583 };
584 let here = ogeom_algo::project_on_curve(&crease.curve, middle, 64, tol)?;
585 let there = ogeom_algo::project_on_curve(&recovered[other].curve, middle, 64, tol)?;
586 if here.point.distance(there.point) > tol.confusion() * 1e3 {
587 continue;
588 }
589 let ta = crease.curve.d1_at(here.parameter, tol)?;
590 let tb = recovered[other].curve.d1_at(there.parameter, tol)?;
591 if ta.cross(tb).magnitude() > ta.magnitude() * tb.magnitude() * 1e-3 {
592 continue;
593 }
594 piercings.push((
595 unwrapped(&crease.curve, here.parameter, crease.anchor),
596 here.point,
597 ));
598 }
599 }
600 let slack = tol.parametric().max(1e-6);
601 let below = piercings
602 .iter()
603 .filter(|(t, _)| *t <= crease.extent.0 + slack)
604 .max_by(|a, b| a.0.total_cmp(&b.0));
605 let above = piercings
606 .iter()
607 .filter(|(t, _)| *t >= crease.extent.1 - slack)
608 .min_by(|a, b| a.0.total_cmp(&b.0));
609 match (below, above) {
610 (None, None) if piercings.is_empty() => {
611 let (lo, hi) = crease.curve.domain();
621 if !crease.curve.is_periodic() {
622 ogeom_bail!(
623 Construction,
624 "a wrapping band recovered an open curve; the closure is \
625 not constructible from it"
626 );
627 }
628 let period = hi - lo;
629 let mut stops: Vec<f64> = Vec::new();
630 for side in &crease.sides {
631 let mut rim_vertices: HashSet<TShapeId> = HashSet::new();
632 for edge in explore(model, side, Filter::OfType(ShapeType::Edge))? {
633 if is_ring(&edge) {
634 for v in model.ordered_children_of(&edge)? {
635 rim_vertices.insert(v.node());
636 }
637 }
638 }
639 for edge in explore(model, side, Filter::OfType(ShapeType::Edge))? {
640 if is_ring(&edge) {
641 continue;
642 }
643 let free: Vec<Point> = model
644 .ordered_children_of(&edge)?
645 .iter()
646 .filter(|v| rim_vertices.contains(&v.node()))
647 .filter_map(|v| {
648 model
649 .node(v)
650 .and_then(|n| n.data().as_vertex())
651 .map(|d| d.point)
652 })
653 .collect();
654 if free.is_empty() {
655 continue;
656 }
657 let Some(geometry) = edge_geometry(model, &edge, tol)? else {
658 continue;
659 };
660 for start in free {
661 let mut point = start;
666 for _ in 0..8 {
667 let on_curve =
668 ogeom_algo::project_on_curve(&crease.curve, point, 64, tol)?;
669 let t = parameter_near(&geometry, on_curve.point, tol)?;
670 let Ok(on_edge) = geometry.point_at(t, tol) else {
671 break;
672 };
673 if on_edge.distance(on_curve.point) <= tol.confusion() * 10.0 {
674 stops.push(lo + (on_curve.parameter - lo).rem_euclid(period));
675 break;
676 }
677 point = on_edge;
678 }
679 }
680 }
681 }
682 let (curve, stops) = match (&crease.curve, stops.first().copied()) {
689 (Curve::Circle(circle), Some(first)) => {
690 let at = crease.curve.point_at(first, tol)?;
691 let held = circle.circle();
692 let anchored = ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(
693 ogeom_math::Frame::new(
694 held.centre(),
695 held.frame().z(),
696 ogeom_math::Direction::new(at - held.centre(), tol)?,
697 tol,
698 )?,
699 held.radius(),
700 tol,
701 )?);
702 let mut anchored = Curve::Circle(anchored);
703 if anchored
707 .d1_at(0.0, tol)?
708 .dot(crease.curve.d1_at(first, tol)?)
709 < 0.0
710 {
711 anchored = ogeom_geom::Reversible::reversed(&anchored);
712 }
713 let shifted = stops
714 .iter()
715 .map(|t| (t - first).rem_euclid(period))
716 .collect::<Vec<f64>>();
717 (anchored, shifted)
718 }
719 _ => {
723 let mut kept = stops;
724 kept.push(lo);
725 (crease.curve.clone(), kept)
726 }
727 };
728 let mut cuts = stops;
729 cuts.sort_by(f64::total_cmp);
730 cuts.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric().max(1e-9));
731 if cuts.is_empty() {
732 cuts.push(lo);
733 }
734 let mut cut: Vec<Shape> = Vec::with_capacity(cuts.len());
735 for t in &cuts {
736 let at = curve.point_at(*t, tol)?;
737 cut.push(vertex_at(model, at));
738 }
739 for (index, t) in cuts.iter().enumerate() {
740 let next = if index + 1 == cuts.len() {
741 cuts[0] + period
742 } else {
743 cuts[index + 1]
744 };
745 let edge = make_edge_between(
746 model,
747 curve.clone(),
748 (*t, next),
749 &cut[index],
750 &cut[(index + 1) % cuts.len()],
751 tol,
752 )?
753 .shape;
754 new_edges.push((edge, [crease.sides[0].node(), crease.sides[1].node()]));
755 }
756 corners.extend(cut);
757 }
758 (Some(&(t0, p0)), Some(&(t1, p1))) => {
759 let v0 = vertex_at(model, p0);
760 let v1 = vertex_at(model, p1);
761 let window = if crease.curve.is_periodic() {
766 let (lo, hi) = crease.curve.domain();
767 let turns = ((t0 - lo) / (hi - lo)).floor() * (hi - lo);
768 (t0 - turns, t1 - turns)
769 } else {
770 (t0, t1)
771 };
772 let edge =
773 make_edge_between(model, crease.curve.clone(), window, &v0, &v1, tol)?.shape;
774 corners.push(v0);
775 corners.push(v1);
776 new_edges.push((edge, [crease.sides[0].node(), crease.sides[1].node()]));
777 }
778 _ => ogeom_bail!(
779 Construction,
780 "an end face's surface never meets the recovered edge; the \
781 corner cannot be placed"
782 ),
783 }
784 }
785
786 let mut out = Vec::new();
787 let mut extended: HashMap<TShapeId, Shape> = HashMap::new();
788 for face in interrupted {
789 let rims: Vec<Shape> = explore(model, face, Filter::OfType(ShapeType::Edge))?
790 .into_iter()
791 .filter(|e| is_ring(e))
792 .collect();
793 let borders: Vec<Shape> = new_edges
794 .iter()
795 .filter(|(_, sides)| sides.contains(&face.node()))
796 .map(|(e, _)| e.clone())
797 .collect();
798 let new_face =
799 rebuild_interrupted(model, face, &rims, &borders, &corners, &mut extended, tol)?;
800 out.push((face.clone(), new_face));
801 }
802 Ok(out)
803}
804
805fn feature_groups(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Vec<Shape>>> {
808 let mut edge_sets: Vec<HashSet<TShapeId>> = Vec::with_capacity(faces.len());
809 for face in faces {
810 edge_sets.push(
811 explore(model, face, Filter::OfType(ShapeType::Edge))?
812 .iter()
813 .map(Shape::node)
814 .collect(),
815 );
816 }
817 let mut group_of: Vec<usize> = (0..faces.len()).collect();
818 fn root(group_of: &mut [usize], mut i: usize) -> usize {
820 while group_of[i] != i {
821 group_of[i] = group_of[group_of[i]];
822 i = group_of[i];
823 }
824 i
825 }
826 for i in 0..faces.len() {
827 for j in i + 1..faces.len() {
828 if edge_sets[i].intersection(&edge_sets[j]).next().is_some() {
829 let (a, b) = (root(&mut group_of, i), root(&mut group_of, j));
830 group_of[a.max(b)] = a.min(b);
831 }
832 }
833 }
834 let mut groups: HashMap<usize, Vec<Shape>> = HashMap::new();
835 for (i, face) in faces.iter().enumerate() {
836 groups
837 .entry(root(&mut group_of, i))
838 .or_default()
839 .push(face.clone());
840 }
841 let mut out: Vec<Vec<Shape>> = groups.into_values().collect();
842 out.sort_by_key(|g| g.iter().map(|f| f.node().index()).min());
844 Ok(out)
845}
846
847fn rebuild_interrupted(
851 model: &mut Model,
852 face: &Shape,
853 rims: &[Shape],
854 borders: &[Shape],
855 corners: &[Shape],
856 extended: &mut HashMap<TShapeId, Shape>,
857 tol: Tolerances,
858) -> OgeomResult<Shape> {
859 let placement = face.transform(model.datums())?;
860 let Some(data) = model.node(face).and_then(|n| n.data().as_face().cloned()) else {
861 ogeom_bail!(Construction, "an interrupted face holds no face data");
862 };
863 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
864 ogeom_bail!(
865 Construction,
866 "an interrupted face's surface is not in this model"
867 );
868 };
869 let surface = surface.transformed(&placement, tol)?;
870 let rim_nodes: HashSet<TShapeId> = rims.iter().map(Shape::node).collect();
871
872 let mut wires: Vec<Vec<Shape>> = Vec::new();
873 for wire in model.ordered_children_of(face)? {
874 let edges = model.ordered_children_of(&wire)?;
875 let touched = edges.iter().any(|e| rim_nodes.contains(&e.node()));
876 if !touched {
877 wires.push(edges);
878 continue;
879 }
880 let mut rim_vertices: HashSet<TShapeId> = HashSet::new();
883 for edge in &edges {
884 if rim_nodes.contains(&edge.node()) {
885 for v in model.ordered_children_of(edge)? {
886 rim_vertices.insert(v.node());
887 }
888 }
889 }
890 let mut ring: Vec<(Shape, Option<Shape>)> = Vec::with_capacity(edges.len());
897 for edge in &edges {
898 if rim_nodes.contains(&edge.node()) {
899 ring.push((edge.clone(), None));
900 continue;
901 }
902 let replaced = match extended.get(&edge.node()) {
905 Some(found) => Some(found.clone()),
906 None => {
907 let dangles = model
908 .ordered_children_of(edge)?
909 .iter()
910 .any(|v| rim_vertices.contains(&v.node()));
911 match (dangles, corner_on_edge(model, edge, corners, tol)?) {
912 (true, Some(corner)) => {
913 Some(extend_to_corner(model, edge, &corner, extended, tol)?)
914 }
915 _ => None,
916 }
917 }
918 };
919 ring.push((
922 edge.clone(),
923 Some(match replaced {
924 Some(fresh) if edge.orientation() == ogeom_topo::Orientation::Reversed => {
925 fresh.reversed()
926 }
927 Some(fresh) => fresh,
928 None => edge.clone(),
929 }),
930 ));
931 }
932 if std::env::var_os("OGEOM_DEBUG_DEFEATURE").is_some() {
933 let point = |v: &Shape| {
934 model
935 .node(v)
936 .and_then(|n| n.data().as_vertex())
937 .map(|d| d.point)
938 };
939 for (edge, spliced) in &ring {
940 let (a, b) = edge_ends(model, spliced.as_ref().unwrap_or(edge))?;
941 eprintln!(
942 "DEFEATURE wire edge {} {} {:?} .. {:?}",
943 edge.node().index(),
944 if spliced.is_none() { "RIM" } else { "kept" },
945 point(&a),
946 point(&b)
947 );
948 }
949 }
950 let mut pool: Vec<Shape> = borders.to_vec();
951 let mut chained: Vec<Shape> = Vec::new();
952 if ring.iter().all(|(_, spliced)| spliced.is_none()) {
953 let Some(start) = pool.first().cloned() else {
956 ogeom_bail!(
957 Construction,
958 "a neighbour lost a whole ring and no recovered edge \
959 borders it; the wound needs a closure this operation \
960 does not construct yet"
961 );
962 };
963 let (from, _) = edge_ends(model, &start)?;
964 chained = bridge_gap(model, &mut pool, &from, &from)?;
965 let mut was = Vec::new();
966 for (edge, _) in &ring {
967 was.extend(sample_edge(model, edge, tol)?);
968 }
969 wind_like(model, &mut chained, &was, tol)?;
970 } else {
971 let first = ring
974 .iter()
975 .position(|(_, spliced)| spliced.is_some())
976 .unwrap_or(0);
977 ring.rotate_left(first);
978 let mut index = 0;
979 while index < ring.len() {
980 if let Some(edge) = ring[index].1.clone() {
981 chained.push(edge);
982 index += 1;
983 continue;
984 }
985 let run_end = ring[index..]
986 .iter()
987 .position(|(_, spliced)| spliced.is_some())
988 .map_or(ring.len(), |k| index + k);
989 let Some(previous) = chained.last() else {
990 ogeom_bail!(Construction, "a wound's rim opens a wire that has no start");
991 };
992 let (_, from) = edge_ends(model, previous)?;
993 let to = match ring.get(run_end).and_then(|(_, spliced)| spliced.as_ref()) {
994 Some(next) => edge_ends(model, next)?.0,
995 None => edge_ends(model, &chained[0])?.0,
997 };
998 let mut bridge = bridge_gap(model, &mut pool, &from, &to)?;
999 if from.node() == to.node() {
1002 let mut was = Vec::new();
1003 for (edge, _) in &ring[index..run_end] {
1004 was.extend(sample_edge(model, edge, tol)?);
1005 }
1006 wind_like(model, &mut bridge, &was, tol)?;
1007 }
1008 chained.extend(bridge);
1009 index = run_end;
1010 }
1011 }
1012 if !pool.is_empty() {
1013 ogeom_bail!(
1014 Construction,
1015 "{} recovered edges border this face and its wound's rim has \
1016 nowhere to put them",
1017 pool.len()
1018 );
1019 }
1020 wires.push(chained);
1021 }
1022 let built = ogeom_algo::make_face_with_pcurves(model, surface, &wires, tol)?.shape;
1023 Ok(orient_like(face, built))
1028}
1029
1030fn orient_like(was: &Shape, built: Shape) -> Shape {
1032 if was.orientation() == ogeom_topo::Orientation::Reversed {
1033 built.reversed()
1034 } else {
1035 built
1036 }
1037}
1038
1039fn sample_edge(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<Vec<Point>> {
1041 const STATIONS: usize = 12;
1042 let Some(geometry) = edge_geometry(model, edge, tol)? else {
1043 return Ok(Vec::new());
1044 };
1045 let Some(range) = model
1046 .node(edge)
1047 .and_then(|n| n.data().as_edge())
1048 .and_then(|d| match d.curve3d()? {
1049 ogeom_topo::EdgeRepr::Curve3d { range, .. } => Some(*range),
1050 _ => None,
1051 })
1052 else {
1053 return Ok(Vec::new());
1054 };
1055 let backwards = edge.orientation() == ogeom_topo::Orientation::Reversed;
1056 let mut out = Vec::with_capacity(STATIONS + 1);
1057 for i in 0..=STATIONS {
1058 #[allow(clippy::cast_precision_loss)]
1059 let f = i as f64 / STATIONS as f64;
1060 let f = if backwards { 1.0 - f } else { f };
1061 out.push(geometry.point_at(range.0 + (range.1 - range.0) * f, tol)?);
1062 }
1063 Ok(out)
1064}
1065
1066fn swept_area(points: &[Point]) -> ogeom_math::Vector {
1070 if points.len() < 3 {
1071 return ogeom_math::Vector::ZERO;
1072 }
1073 let mut sum = ogeom_math::Vector::ZERO;
1074 for p in points {
1075 sum += p.to_vector();
1076 }
1077 #[allow(clippy::cast_precision_loss)]
1078 let centre = Point::ORIGIN + sum / points.len() as f64;
1079 let mut area = ogeom_math::Vector::ZERO;
1080 for pair in points.windows(2) {
1081 area += (pair[0] - centre).cross(pair[1] - centre);
1082 }
1083 area
1084}
1085
1086fn edge_geometry(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<Option<Curve>> {
1092 let placement = edge.transform(model.datums())?;
1093 let Some(curve) = model
1094 .node(edge)
1095 .and_then(|n| n.data().as_edge())
1096 .and_then(|d| match d.curve3d()? {
1097 ogeom_topo::EdgeRepr::Curve3d { curve, .. } => Some(*curve),
1098 _ => None,
1099 })
1100 .and_then(|id| model.geometry().curve(id).cloned())
1101 else {
1102 return Ok(None);
1103 };
1104 Ok(Some(curve.transformed(&placement, tol)?))
1105}
1106
1107fn edge_ends(model: &Model, edge: &Shape) -> OgeomResult<(Shape, Shape)> {
1109 ogeom_algo::edge_vertices(model, edge)?
1110 .ok_or_else(|| ogeom_err!(Construction, "an edge of a rebuilt wire has no vertices"))
1111}
1112
1113fn wind_like(
1122 model: &Model,
1123 chain: &mut [Shape],
1124 was: &[Point],
1125 tol: Tolerances,
1126) -> OgeomResult<()> {
1127 let mut now = Vec::new();
1128 for edge in chain.iter() {
1129 now.extend(sample_edge(model, edge, tol)?);
1130 }
1131 if swept_area(&now).dot(swept_area(was)) >= 0.0 {
1132 return Ok(());
1133 }
1134 chain.reverse();
1135 for edge in chain.iter_mut() {
1136 *edge = edge.clone().reversed();
1137 }
1138 Ok(())
1139}
1140
1141fn bridge_gap(
1152 model: &Model,
1153 pool: &mut Vec<Shape>,
1154 from: &Shape,
1155 to: &Shape,
1156) -> OgeomResult<Vec<Shape>> {
1157 let mut chain = Vec::new();
1158 let mut here = from.node();
1159 loop {
1160 let mut found = None;
1161 for (index, edge) in pool.iter().enumerate() {
1162 let (a, b) = edge_ends(model, edge)?;
1163 if a.node() == here {
1164 found = Some((index, false, b));
1165 break;
1166 }
1167 if b.node() == here {
1168 found = Some((index, true, a));
1169 break;
1170 }
1171 }
1172 let Some((index, backwards, next)) = found else {
1173 if here == to.node() {
1174 return Ok(chain);
1177 }
1178 if std::env::var_os("OGEOM_DEBUG_DEFEATURE").is_some() {
1179 let point = |v: &Shape| {
1180 model
1181 .node(v)
1182 .and_then(|n| n.data().as_vertex())
1183 .map(|d| d.point)
1184 };
1185 eprintln!(
1186 "DEFEATURE bridge stuck at {:?} heading for {:?}",
1187 point(from),
1188 point(to)
1189 );
1190 for edge in pool.iter() {
1191 let (a, b) = edge_ends(model, edge)?;
1192 eprintln!(" border {:?} .. {:?}", point(&a), point(&b));
1193 }
1194 }
1195 ogeom_bail!(
1196 Construction,
1197 "no recovered edge bridges the wound's rim; the closure is \
1198 not constructible from what the neighbours meet along"
1199 );
1200 };
1201 let edge = pool.remove(index);
1202 chain.push(if backwards { edge.reversed() } else { edge });
1203 here = next.node();
1204 if here == to.node() {
1205 return Ok(chain);
1206 }
1207 if pool.is_empty() {
1208 ogeom_bail!(
1209 Construction,
1210 "the recovered edges do not reach across the wound's rim; \
1211 the closure is not constructible from them"
1212 );
1213 }
1214 }
1215}
1216
1217fn corner_on_edge(
1220 model: &Model,
1221 edge: &Shape,
1222 corners: &[Shape],
1223 tol: Tolerances,
1224) -> OgeomResult<Option<Shape>> {
1225 let placement = edge.transform(model.datums())?;
1226 let Some((curve, range)) = model
1227 .node(edge)
1228 .and_then(|n| n.data().as_edge())
1229 .and_then(|d| match d.curve3d()? {
1230 ogeom_topo::EdgeRepr::Curve3d { curve, range, .. } => Some((*curve, *range)),
1231 _ => None,
1232 })
1233 else {
1234 return Ok(None);
1235 };
1236 let Some(geometry) = model.geometry().curve(curve).cloned() else {
1237 return Ok(None);
1238 };
1239 let geometry = geometry.transformed(&placement, tol)?;
1240 let head = geometry.point_at(range.0, tol)?;
1241 let tail = geometry.point_at(range.1, tol)?;
1242 let mut best: Option<(f64, Shape)> = None;
1243 for corner in corners {
1244 let Some(p) = model
1245 .node(corner)
1246 .and_then(|n| n.data().as_vertex())
1247 .map(|d| d.point)
1248 else {
1249 continue;
1250 };
1251 let t = parameter_near(&geometry, p, tol)?;
1252 if geometry.point_at(t, tol)?.distance(p) > tol.confusion() * 1e3 {
1253 continue;
1254 }
1255 let gap = head.distance(p).min(tail.distance(p));
1256 if best.as_ref().is_none_or(|(g, _)| gap < *g) {
1257 best = Some((gap, corner.clone()));
1258 }
1259 }
1260 Ok(best.map(|(_, c)| c))
1261}
1262
1263fn extend_to_corner(
1267 model: &mut Model,
1268 edge: &Shape,
1269 corner: &Shape,
1270 extended: &mut HashMap<TShapeId, Shape>,
1271 tol: Tolerances,
1272) -> OgeomResult<Shape> {
1273 if let Some(found) = extended.get(&edge.node()) {
1274 return Ok(found.clone());
1275 }
1276 let placement = edge.transform(model.datums())?;
1277 let Some((curve, range)) = model
1278 .node(edge)
1279 .and_then(|n| n.data().as_edge())
1280 .and_then(|d| match d.curve3d()? {
1281 ogeom_topo::EdgeRepr::Curve3d { curve, range, .. } => Some((*curve, *range)),
1282 _ => None,
1283 })
1284 else {
1285 ogeom_bail!(Construction, "a dangling edge has no curve to extend");
1286 };
1287 let Some(geometry) = model.geometry().curve(curve).cloned() else {
1288 ogeom_bail!(Construction, "a dangling edge's curve is not in this model");
1289 };
1290 let geometry = geometry.transformed(&placement, tol)?;
1291 let corner_point = model
1292 .node(corner)
1293 .and_then(|n| n.data().as_vertex())
1294 .map(|d| d.point)
1295 .ok_or_else(|| ogeom_err!(Construction, "a corner vertex holds no point"))?;
1296
1297 let head = geometry.point_at(range.0, tol)?;
1300 let tail = geometry.point_at(range.1, tol)?;
1301 let t_corner = parameter_near(&geometry, corner_point, tol)?;
1302 let (vertices, new_range, dangle_head) = {
1303 let bounds = model.children_of(edge)?;
1306 let (Some(va), Some(vb)) = (bounds.first().cloned(), bounds.last().cloned()) else {
1307 ogeom_bail!(Construction, "a dangling edge has no vertices");
1308 };
1309 if head.distance(corner_point) <= tail.distance(corner_point) {
1310 ((corner.clone(), vb), (t_corner, range.1), true)
1311 } else {
1312 ((va, corner.clone()), (range.0, t_corner), false)
1313 }
1314 };
1315 let _ = dangle_head;
1316 if new_range.1 <= new_range.0 {
1317 ogeom_bail!(
1318 Construction,
1319 "extending an edge to its corner inverted its range; the corner \
1320 sits on the wrong side of the edge"
1321 );
1322 }
1323 let geometry = match geometry {
1326 Curve::Line(line) => {
1327 let (lo, hi) = ogeom_geom::Curve3d::domain(&line);
1328 Curve::Line(ogeom_geom::LineCurve::over(
1329 line.axis(),
1330 lo.min(new_range.0),
1331 hi.max(new_range.1),
1332 )?)
1333 }
1334 other => other,
1335 };
1336 let built = make_edge_between(model, geometry, new_range, &vertices.0, &vertices.1, tol)?;
1337 extended.insert(edge.node(), built.shape.clone());
1338 Ok(built.shape)
1339}
1340
1341fn parameter_near(curve: &Curve, p: Point, tol: Tolerances) -> OgeomResult<f64> {
1344 match curve {
1345 Curve::Line(line) => {
1346 let axis = line.axis();
1347 Ok((p - axis.location).dot(axis.direction.vector()))
1348 }
1349 Curve::Circle(c) => {
1350 let local = c.circle().frame().to_local(p);
1351 Ok(local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU))
1352 }
1353 _ => Ok(ogeom_algo::project_on_curve(curve, p, 64, tol)?.parameter),
1354 }
1355}
1356
1357fn nearest_distance(curve: &Curve, p: Point, tol: Tolerances) -> f64 {
1358 ogeom_algo::project_on_curve(curve, p, 32, tol).map_or(f64::INFINITY, |pr| pr.distance)
1359}
1360
1361fn edge_length(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<f64> {
1362 let Some((curve, range)) = model
1363 .node(edge)
1364 .and_then(|n| n.data().as_edge())
1365 .and_then(|d| match d.curve3d()? {
1366 ogeom_topo::EdgeRepr::Curve3d { curve, range, .. } => Some((*curve, *range)),
1367 _ => None,
1368 })
1369 else {
1370 return Ok(0.0);
1371 };
1372 let Some(geometry) = model.geometry().curve(curve) else {
1373 return Ok(0.0);
1374 };
1375 ogeom_algo::curve_length(geometry, range, tol)
1376}