1use crate::{OgeomResult, Tolerances, ogeom_bail};
76use ogeom_algo::{Built, History, make_edge_between, make_solid, make_vertex, sew};
77use ogeom_core::ogeom_err;
78use ogeom_core::{FastMap, FastSet};
79use ogeom_geom::Curve3d as _;
80use ogeom_geom::Surface as _;
81use ogeom_geom::Transformable as _;
82use ogeom_geom::{Curve, SurfaceGeometry};
83use ogeom_intersect::{
84 CurveSurfaceOptions, IntersectOptions, SurfaceIntersection, intersect_curve_surface,
85 intersect_surfaces,
86};
87use ogeom_math::Point;
88use ogeom_topo::{Filter, Model, NodeData, Shape, ShapeType, TShapeId, explore};
89
90pub fn remove_faces(
104 model: &mut Model,
105 solid: &Shape,
106 faces: &[Shape],
107 tol: Tolerances,
108) -> OgeomResult<Built> {
109 ogeom_algo::on_own_nodes(model, solid, faces, |model, solid, parts| {
110 remove_faces_own(model, solid, parts, tol)
111 })
112}
113
114fn remove_faces_own(
116 model: &mut Model,
117 solid: &Shape,
118 faces: &[Shape],
119 tol: Tolerances,
120) -> OgeomResult<Built> {
121 if faces.is_empty() {
122 ogeom_bail!(Construction, "no faces named; there is nothing to remove");
123 }
124 let groups = feature_groups(model, faces)?;
132 if groups.len() > 1 {
133 let mut current = Built::from_nothing(solid.clone());
134 for group in &groups {
135 let step = remove_faces_own(model, ¤t.shape, group, tol)?;
138 current = Built {
139 shape: step.shape,
140 history: current.history.then(&step.history),
141 };
142 }
143 return Ok(current);
144 }
145 let all_faces = explore(model, solid, Filter::OfType(ShapeType::Face))?;
146 let removed: FastSet<TShapeId> = faces.iter().map(Shape::node).collect();
147 for face in faces {
148 if !all_faces.iter().any(|f| f.node() == face.node()) {
149 ogeom_bail!(
150 Construction,
151 "a face named for removal is not a face of this solid"
152 );
153 }
154 }
155 let survivors: Vec<Shape> = all_faces
156 .iter()
157 .filter(|f| !removed.contains(&f.node()))
158 .cloned()
159 .collect();
160 if survivors.is_empty() {
161 ogeom_bail!(
162 Construction,
163 "every face was named for removal; nothing remains to close"
164 );
165 }
166
167 let mut users: FastMap<TShapeId, Vec<Shape>> = FastMap::default();
170 for face in &all_faces {
171 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
172 users.entry(edge.node()).or_default().push(face.clone());
173 }
174 }
175 let is_ring = |edge: &Shape| -> bool {
176 users.get(&edge.node()).is_some_and(|fs| {
177 fs.iter().any(|f| removed.contains(&f.node()))
178 && fs.iter().any(|f| !removed.contains(&f.node()))
179 })
180 };
181
182 struct Touched {
185 face: Shape,
186 kept: Vec<Shape>,
188 whole: bool,
190 partial: bool,
192 outer: bool,
194 }
195 let mut untouched: Vec<Shape> = Vec::new();
196 let mut touched: Vec<Touched> = Vec::new();
197 for face in &survivors {
198 let wires = model.ordered_children_of(&face.oriented(ogeom_topo::Orientation::Forward))?;
201 let mut kept = Vec::new();
202 let (mut whole, mut partial, mut outer) = (false, false, false);
203 for (index, wire) in wires.iter().enumerate() {
204 let edges = model.ordered_children_of(wire)?;
205 let ring_count = edges.iter().filter(|e| is_ring(e)).count();
206 if ring_count == 0 {
207 kept.push(wire.clone());
208 } else if ring_count == edges.len() {
209 whole = true;
210 outer |= index == 0;
211 } else {
212 partial = true;
213 }
214 }
215 if whole || partial {
216 touched.push(Touched {
217 face: face.clone(),
218 kept,
219 whole,
220 partial,
221 outer,
222 });
223 } else {
224 untouched.push(face.clone());
225 }
226 }
227
228 let sides: Vec<Shape> = touched.iter().map(|t| t.face.clone()).collect();
234 let recovers = touched.iter().any(|t| t.whole) && wound_recovers(model, faces, &sides, tol)?;
235 let mut rim_surgery: Vec<(Shape, Vec<Shape>)> = Vec::new(); let mut interrupted: Vec<Shape> = Vec::new();
237 for entry in touched {
238 if entry.partial || recovers {
239 interrupted.push(entry.face);
240 } else {
241 if entry.outer {
245 ogeom_bail!(
246 Construction,
247 "removing these faces erases a neighbour's whole outer \
248 boundary; that face has nothing left to stand on"
249 );
250 }
251 rim_surgery.push((entry.face, entry.kept));
252 }
253 }
254
255 let mut history = History::new();
256 for face in faces {
257 history.delete(face);
258 }
259
260 let mut rebuilt: Vec<Shape> = untouched;
261 for (face, kept_wires) in rim_surgery {
262 let new_face = {
263 let Some(data) = model.node(&face).and_then(|n| match n.data() {
264 NodeData::Face(d) => Some(d.clone()),
265 _ => None,
266 }) else {
267 ogeom_bail!(Construction, "a surviving face holds no face data");
268 };
269 let built = ogeom_algo::make_face_on(model, data.surface, &kept_wires, tol)?.shape;
272 orient_like(&face, built)
273 };
274 history.modify(&face, new_face.clone());
275 rebuilt.push(new_face);
276 }
277
278 if !interrupted.is_empty() {
279 let band = match close_wound(model, faces, &interrupted, &removed, &users, &is_ring, tol) {
284 Ok(band) => band,
285 Err(refused) => {
289 close_by_extension(model, faces, &interrupted, &users, &is_ring, false, tol)
290 .or_else(|_| {
291 close_by_extension(model, faces, &interrupted, &users, &is_ring, true, tol)
292 })
293 .map_err(|_| refused)?
294 }
295 };
296 for (old, new) in band {
297 history.modify(&old, new.clone());
298 if !rebuilt.iter().any(|f| f.is_same(&new)) {
300 rebuilt.push(new);
301 }
302 }
303 }
304
305 let sewn = sew(model, &rebuilt, tol)?;
306 let [shell] = sewn.shells.as_slice() else {
307 ogeom_bail!(
308 Construction,
309 "closing the wound left {} shells where one solid's worth was \
310 expected; the removal disconnected the boundary",
311 sewn.shells.len()
312 );
313 };
314 if !ogeom_algo::is_shell_closed(model, shell)? {
315 ogeom_bail!(
316 Construction,
317 "the boundary does not close after removal; the wound needs a \
318 closure this operation does not construct yet"
319 );
320 }
321 let built = make_solid(model, std::slice::from_ref(shell))?;
322 let mut solid_history = history;
323 solid_history.modify(solid, built.shape.clone());
324 Ok(Built::new(built.shape, solid_history))
325}
326
327fn placed_surface(model: &Model, face: &Shape, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
329 let placement = face.transform(model.datums())?;
330 let Some(data) = model.node(face).and_then(|n| n.data().as_face().cloned()) else {
331 ogeom_bail!(Construction, "a band face holds no face data");
332 };
333 let Some(surface) = model.geometry().surface(data.surface) else {
334 ogeom_bail!(Construction, "a band face's surface is not in this model");
335 };
336 surface.clone().transformed(&placement, tol)
337}
338
339fn wound_recovers(
357 model: &Model,
358 removed_faces: &[Shape],
359 candidates: &[Shape],
360 tol: Tolerances,
361) -> OgeomResult<bool> {
362 let mut room = ogeom_math::Aabb::default();
363 for face in removed_faces {
364 room = room.union(&ogeom_algo::shape_bounds(model, face, tol)?);
365 }
366 let room = room.expanded(tol.confusion() * 1e3);
367 let Some(centre) = room.centre() else {
368 return Ok(false);
369 };
370 for (i, first) in candidates.iter().enumerate() {
371 let sa = placed_surface(model, first, tol)?;
372 for second in &candidates[i + 1..] {
373 let sb = placed_surface(model, second, tol)?;
374 let Ok(SurfaceIntersection::Along(sections)) =
375 intersect_surfaces(&sa, &sb, IntersectOptions::default(), tol)
376 else {
377 continue;
378 };
379 for section in sections {
380 let foot = ogeom_algo::project_on_curve(§ion.curve, centre, 64, tol)?;
381 if room.contains(foot.point) {
382 return Ok(true);
383 }
384 }
385 }
386 }
387 Ok(false)
388}
389
390struct Crease {
393 sides: [Shape; 2],
395 curve: Curve,
397 extent: (f64, f64),
400 anchor: f64,
404}
405
406fn unwrapped(curve: &Curve, t: f64, about: f64) -> f64 {
409 if !curve.is_periodic() {
410 return t;
411 }
412 let (lo, hi) = curve.domain();
413 let period = hi - lo;
414 if period <= 0.0 {
415 return t;
416 }
417 about + (t - about + period / 2.0).rem_euclid(period) - period / 2.0
418}
419
420#[allow(clippy::too_many_lines, reason = "one wound, one narrative")]
435fn close_wound(
436 model: &mut Model,
437 removed_faces: &[Shape],
438 interrupted: &[Shape],
439 removed: &FastSet<TShapeId>,
440 users: &FastMap<TShapeId, Vec<Shape>>,
441 is_ring: &dyn Fn(&Shape) -> bool,
442 tol: Tolerances,
443) -> OgeomResult<Vec<(Shape, Shape)>> {
444 let surface_of = |model: &Model, face: &Shape| placed_surface(model, face, tol);
445 let vertices_of = |model: &Model, face: &Shape| -> OgeomResult<Vec<Point>> {
446 let mut out = Vec::new();
447 for vertex in explore(model, face, Filter::OfType(ShapeType::Vertex))? {
448 let placement = vertex.transform(model.datums())?;
449 if let Some(d) = model.node(&vertex).and_then(|nd| nd.data().as_vertex()) {
450 out.push(placement.apply(d.point));
451 }
452 }
453 Ok(out)
454 };
455 let interrupted_by_node: FastMap<TShapeId, Shape> =
456 interrupted.iter().map(|f| (f.node(), f.clone())).collect();
457
458 let mut creases: Vec<(TShapeId, TShapeId, [Shape; 2], Vec<Point>)> = Vec::new();
465 let mut crease_of: FastMap<TShapeId, usize> = FastMap::default();
466 let mut leftovers: Vec<Shape> = Vec::new();
467 for face in removed_faces {
468 let mut shared: FastMap<TShapeId, f64> = FastMap::default();
469 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
470 if !is_ring(&edge) {
471 continue;
472 }
473 let length = edge_length(model, &edge, tol)?;
474 for user in users.get(&edge.node()).into_iter().flatten() {
475 if !removed.contains(&user.node()) && interrupted_by_node.contains_key(&user.node())
476 {
477 *shared.entry(user.node()).or_default() += length;
478 }
479 }
480 }
481 let mut ranked: Vec<(TShapeId, f64)> = shared.into_iter().collect();
482 ranked.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.0.index().cmp(&b.0.index())));
483 let [(a, _), (b, _), ..] = ranked.as_slice() else {
484 leftovers.push(face.clone());
485 continue;
486 };
487 let (lo, hi) = if a.index() <= b.index() {
488 (*a, *b)
489 } else {
490 (*b, *a)
491 };
492 let points = vertices_of(model, face)?;
493 let index = match creases.iter().position(|c| c.0 == lo && c.1 == hi) {
494 Some(i) => {
495 creases[i].3.extend(points);
496 i
497 }
498 None => {
499 creases.push((
500 lo,
501 hi,
502 [
503 interrupted_by_node[&lo].clone(),
504 interrupted_by_node[&hi].clone(),
505 ],
506 points,
507 ));
508 creases.len() - 1
509 }
510 };
511 crease_of.insert(face.node(), index);
512 }
513 for face in leftovers {
514 let mut joined = None;
515 for edge in explore(model, &face, Filter::OfType(ShapeType::Edge))? {
516 for user in users.get(&edge.node()).into_iter().flatten() {
517 if let Some(&index) = crease_of.get(&user.node()) {
518 joined = Some(index);
519 }
520 }
521 }
522 let Some(index) = joined else {
523 ogeom_bail!(
524 Construction,
525 "a removed face shares ring edges with fewer than two \
526 interrupted neighbours and borders no removed face with a \
527 crease; closing it needs a neighbour to meet itself, which \
528 is not constructed yet"
529 );
530 };
531 let points = vertices_of(model, &face)?;
532 creases[index].3.extend(points);
533 }
534
535 let mut recovered: Vec<Crease> = Vec::new();
537 for (_, _, sides, points) in creases {
538 let sa = surface_of(model, &sides[0])?;
539 let sb = surface_of(model, &sides[1])?;
540 let meeting = intersect_surfaces(&sa, &sb, IntersectOptions::default(), tol)?;
541 let SurfaceIntersection::Along(sections) = meeting else {
542 ogeom_bail!(
543 Construction,
544 "the band's side surfaces do not meet along a curve; the edge \
545 the feature replaced cannot be recovered from them"
546 );
547 };
548 let anchor = {
549 let mut sum = ogeom_math::Vector::ZERO;
550 for p in &points {
551 sum += p.to_vector();
552 }
553 #[allow(clippy::cast_precision_loss)]
554 let n = points.len().max(1) as f64;
555 Point::ORIGIN + sum * (1.0 / n)
556 };
557 let section = sections
558 .into_iter()
559 .min_by(|p, q| {
560 nearest_distance(&p.curve, anchor, tol)
561 .total_cmp(&nearest_distance(&q.curve, anchor, tol))
562 })
563 .ok_or_else(|| ogeom_err!(Construction, "the side surfaces meet along no branch"))?;
564 let curve = section.curve;
565 let anchor = parameter_near(&curve, anchor, tol)?;
566 let mut extent = (f64::INFINITY, f64::NEG_INFINITY);
567 for p in &points {
568 let t = unwrapped(&curve, parameter_near(&curve, *p, tol)?, anchor);
569 extent = (extent.0.min(t), extent.1.max(t));
570 }
571 recovered.push(Crease {
572 sides,
573 curve,
574 extent,
575 anchor,
576 });
577 }
578
579 let mut corner_vertices: Vec<(Point, Shape)> = Vec::new();
583 let mut vertex_at = |model: &mut Model, p: Point| -> Shape {
584 if let Some((_, v)) = corner_vertices
585 .iter()
586 .find(|(q, _)| q.distance(p) <= tol.confusion() * 1e3)
587 {
588 return v.clone();
589 }
590 let v = make_vertex(model, p).shape;
591 corner_vertices.push((p, v.clone()));
592 v
593 };
594 let mut new_edges: Vec<(Shape, [TShapeId; 2])> = Vec::new(); let mut corners: Vec<Shape> = Vec::new();
596 for (index, crease) in recovered.iter().enumerate() {
597 let mut piercings: Vec<(f64, Point)> = Vec::new();
598 for face in interrupted {
599 if crease.sides.iter().any(|s| s.node() == face.node()) {
600 continue;
601 }
602 let se = surface_of(model, face)?;
603 let hit =
604 intersect_curve_surface(&crease.curve, &se, CurveSurfaceOptions::default(), tol)?;
605 for c in &hit.crossings {
606 piercings.push((unwrapped(&crease.curve, c.on_curve, crease.anchor), c.point));
607 }
608 }
609 for face in removed_faces {
622 if crease_of.get(&face.node()) != Some(&index) {
623 continue;
624 }
625 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
626 let Some(other) = users
627 .get(&edge.node())
628 .into_iter()
629 .flatten()
630 .filter(|user| user.node() != face.node())
631 .filter_map(|user| crease_of.get(&user.node()).copied())
632 .find(|&other| other != index)
633 else {
634 continue;
635 };
636 let samples = sample_edge(model, &edge, tol)?;
637 let Some(&middle) = samples.get(samples.len() / 2) else {
638 continue;
639 };
640 let here = ogeom_algo::project_on_curve(&crease.curve, middle, 64, tol)?;
641 let there = ogeom_algo::project_on_curve(&recovered[other].curve, middle, 64, tol)?;
642 if here.point.distance(there.point) > tol.confusion() * 1e3 {
643 continue;
644 }
645 let ta = crease.curve.d1_at(here.parameter, tol)?;
646 let tb = recovered[other].curve.d1_at(there.parameter, tol)?;
647 if ta.cross(tb).magnitude() > ta.magnitude() * tb.magnitude() * 1e-3 {
648 continue;
649 }
650 piercings.push((
651 unwrapped(&crease.curve, here.parameter, crease.anchor),
652 here.point,
653 ));
654 }
655 }
656 let slack = tol.parametric().max(1e-6);
657 let below = piercings
658 .iter()
659 .filter(|(t, _)| *t <= crease.extent.0 + slack)
660 .max_by(|a, b| a.0.total_cmp(&b.0));
661 let above = piercings
662 .iter()
663 .filter(|(t, _)| *t >= crease.extent.1 - slack)
664 .min_by(|a, b| a.0.total_cmp(&b.0));
665 match (below, above) {
666 (None, None) if piercings.is_empty() => {
667 let (edges, cut) = wrap_round(
670 model,
671 &crease.curve,
672 &crease.sides,
673 is_ring,
674 &mut vertex_at,
675 tol,
676 )?;
677 for edge in edges {
678 new_edges.push((edge, [crease.sides[0].node(), crease.sides[1].node()]));
679 }
680 corners.extend(cut);
681 }
682 (Some(&(t0, p0)), Some(&(t1, p1))) => {
683 let v0 = vertex_at(model, p0);
684 let v1 = vertex_at(model, p1);
685 let window = if crease.curve.is_periodic() {
690 let (lo, hi) = crease.curve.domain();
691 let turns = ((t0 - lo) / (hi - lo)).floor() * (hi - lo);
692 (t0 - turns, t1 - turns)
693 } else {
694 (t0, t1)
695 };
696 let edge =
697 make_edge_between(model, crease.curve.clone(), window, &v0, &v1, tol)?.shape;
698 corners.push(v0);
699 corners.push(v1);
700 new_edges.push((edge, [crease.sides[0].node(), crease.sides[1].node()]));
701 }
702 _ => ogeom_bail!(
703 Construction,
704 "an end face's surface never meets the recovered edge; the \
705 corner cannot be placed"
706 ),
707 }
708 }
709
710 let mut out = Vec::new();
711 let mut extended: FastMap<TShapeId, Shape> = FastMap::default();
712 for face in interrupted {
713 let rims: Vec<Shape> = explore(model, face, Filter::OfType(ShapeType::Edge))?
714 .into_iter()
715 .filter(|e| is_ring(e))
716 .collect();
717 let borders: Vec<Shape> = new_edges
718 .iter()
719 .filter(|(_, sides)| sides.contains(&face.node()))
720 .map(|(e, _)| e.clone())
721 .collect();
722 let new_face = rebuild_interrupted(
723 model,
724 face,
725 &[],
726 &rims,
727 &borders,
728 &corners,
729 &mut extended,
730 tol,
731 )?;
732 out.push((face.clone(), new_face));
733 }
734 Ok(out)
735}
736
737fn wrap_round(
748 model: &mut Model,
749 curve: &Curve,
750 sides: &[Shape; 2],
751 is_ring: &dyn Fn(&Shape) -> bool,
752 vertex_at: &mut dyn FnMut(&mut Model, Point) -> Shape,
753 tol: Tolerances,
754) -> OgeomResult<(Vec<Shape>, Vec<Shape>)> {
755 let (lo, hi) = curve.domain();
756 if !curve.is_periodic() {
757 ogeom_bail!(
758 Construction,
759 "a wrapping band recovered an open curve; the closure is \
760 not constructible from it"
761 );
762 }
763 let period = hi - lo;
764 let mut stops: Vec<f64> = Vec::new();
765 for side in sides {
766 let mut rim_vertices: FastSet<TShapeId> = FastSet::default();
767 for edge in explore(model, side, Filter::OfType(ShapeType::Edge))? {
768 if is_ring(&edge) {
769 for v in model.ordered_children_of(&edge)? {
770 rim_vertices.insert(v.node());
771 }
772 }
773 }
774 for edge in explore(model, side, Filter::OfType(ShapeType::Edge))? {
775 if is_ring(&edge) {
776 continue;
777 }
778 let free: Vec<Point> = model
779 .ordered_children_of(&edge)?
780 .iter()
781 .filter(|v| rim_vertices.contains(&v.node()))
782 .filter_map(|v| {
783 model
784 .node(v)
785 .and_then(|n| n.data().as_vertex())
786 .map(|d| d.point)
787 })
788 .collect();
789 if free.is_empty() {
790 continue;
791 }
792 let Some(geometry) = edge_geometry(model, &edge, tol)? else {
793 continue;
794 };
795 for start in free {
796 let mut point = start;
801 for _ in 0..8 {
802 let on_curve = ogeom_algo::project_on_curve(curve, point, 64, tol)?;
803 let t = parameter_near(&geometry, on_curve.point, tol)?;
804 let Ok(on_edge) = geometry.point_at(t, tol) else {
805 break;
806 };
807 if on_edge.distance(on_curve.point) <= tol.confusion() * 10.0 {
808 stops.push(lo + (on_curve.parameter - lo).rem_euclid(period));
809 break;
810 }
811 point = on_edge;
812 }
813 }
814 }
815 }
816 let (curve, stops) = match (&curve, stops.first().copied()) {
823 (Curve::Circle(circle), Some(first)) => {
824 let at = curve.point_at(first, tol)?;
825 let held = circle.circle();
826 let anchored = ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(
827 ogeom_math::Frame::new(
828 held.centre(),
829 held.frame().z(),
830 ogeom_math::Direction::new(at - held.centre(), tol)?,
831 tol,
832 )?,
833 held.radius(),
834 tol,
835 )?);
836 let mut anchored = Curve::Circle(anchored);
837 if anchored.d1_at(0.0, tol)?.dot(curve.d1_at(first, tol)?) < 0.0 {
841 anchored = ogeom_geom::Reversible::reversed(&anchored);
842 }
843 let shifted = stops
844 .iter()
845 .map(|t| (t - first).rem_euclid(period))
846 .collect::<Vec<f64>>();
847 (anchored, shifted)
848 }
849 _ => {
853 let mut kept = stops;
854 kept.push(lo);
855 (curve.clone(), kept)
856 }
857 };
858 let mut cuts = stops;
859 cuts.sort_by(f64::total_cmp);
860 cuts.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric().max(1e-9));
861 if cuts.is_empty() {
862 cuts.push(lo);
863 }
864 let mut cut: Vec<Shape> = Vec::with_capacity(cuts.len());
865 let mut edges: Vec<Shape> = Vec::with_capacity(cuts.len());
866 for t in &cuts {
867 let at = curve.point_at(*t, tol)?;
868 cut.push(vertex_at(model, at));
869 }
870 for (index, t) in cuts.iter().enumerate() {
871 let next = if index + 1 == cuts.len() {
872 cuts[0] + period
873 } else {
874 cuts[index + 1]
875 };
876 let edge = make_edge_between(
877 model,
878 curve.clone(),
879 (*t, next),
880 &cut[index],
881 &cut[(index + 1) % cuts.len()],
882 tol,
883 )?
884 .shape;
885 edges.push(edge);
886 }
887 Ok((edges, cut))
888}
889
890#[allow(clippy::too_many_lines, reason = "one wound, one narrative")]
909fn close_by_extension(
910 model: &mut Model,
911 removed_faces: &[Shape],
912 interrupted: &[Shape],
913 users: &FastMap<TShapeId, Vec<Shape>>,
914 is_ring: &dyn Fn(&Shape) -> bool,
915 merge: bool,
916 tol: Tolerances,
917) -> OgeomResult<Vec<(Shape, Shape)>> {
918 let mut room = ogeom_math::Aabb::default();
919 for face in removed_faces {
920 room = room.union(&ogeom_algo::shape_bounds(model, face, tol)?);
921 }
922 let room = room.expanded(tol.confusion() * 1e3);
923 let Some(centre) = room.centre() else {
924 ogeom_bail!(Construction, "the removed faces have no extent");
925 };
926 let near = tol.confusion() * 1e3;
927 let point_of = |model: &Model, v: &Shape| -> OgeomResult<Point> {
928 let placement = v.transform(model.datums())?;
929 model
930 .node(v)
931 .and_then(|n| n.data().as_vertex())
932 .map(|d| placement.apply(d.point))
933 .ok_or_else(|| ogeom_err!(Construction, "a vertex holds no point"))
934 };
935
936 let mut rep: FastMap<TShapeId, Shape> = FastMap::default();
939 for (i, face) in interrupted.iter().enumerate() {
940 let mut found = face.clone();
941 if merge {
942 let surface = placed_surface(model, face, tol)?;
943 let sense = if face.orientation() == ogeom_topo::Orientation::Reversed {
944 -1.0
945 } else {
946 1.0
947 };
948 for earlier in &interrupted[..i] {
949 let other = placed_surface(model, earlier, tol)?;
950 let same = matches!(
951 intersect_surfaces(&surface, &other, IntersectOptions::default(), tol),
952 Ok(SurfaceIntersection::Same)
953 );
954 if !same {
955 continue;
956 }
957 let (at, theirs) = ogeom_algo::face_normal(model, earlier, tol)?;
961 let foot = ogeom_algo::project_on_surface(&surface, at, 16, tol)?;
962 let (u, v) = foot.parameters;
963 let ours = surface.normal_at(u, v, tol)?.vector() * sense;
964 if theirs.dot(ours) > 0.0 {
965 found = rep[&earlier.node()].clone();
966 break;
967 }
968 }
969 }
970 rep.insert(face.node(), found);
971 }
972 let rep_of = |id: TShapeId| rep.get(&id).map_or(id, Shape::node);
973 let leaders: Vec<Shape> = interrupted
974 .iter()
975 .filter(|f| rep_of(f.node()) == f.node())
976 .cloned()
977 .collect();
978
979 let mut dangling: Vec<(Point, [TShapeId; 2])> = Vec::new();
982 for face in interrupted {
983 let mut rim_vertices: FastSet<TShapeId> = FastSet::default();
984 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
985 if is_ring(&edge) {
986 for v in model.ordered_children_of(&edge)? {
987 rim_vertices.insert(v.node());
988 }
989 }
990 }
991 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
992 if is_ring(&edge) {
993 continue;
994 }
995 let faces_of: Vec<TShapeId> = users
996 .get(&edge.node())
997 .into_iter()
998 .flatten()
999 .map(Shape::node)
1000 .collect();
1001 let [a, b] = faces_of.as_slice() else {
1002 continue;
1003 };
1004 for v in model.ordered_children_of(&edge)? {
1005 if rim_vertices.contains(&v.node()) {
1006 let p = point_of(model, &v)?;
1007 if !dangling.iter().any(|(q, _)| q.distance(p) <= near) {
1008 dangling.push((p, [rep_of(*a), rep_of(*b)]));
1009 }
1010 }
1011 }
1012 }
1013 }
1014
1015 let mut corner_vertices: Vec<(Point, Shape)> = Vec::new();
1016 let mut vertex_at = |model: &mut Model, p: Point| -> Shape {
1017 if let Some((_, v)) = corner_vertices.iter().find(|(q, _)| q.distance(p) <= near) {
1018 return v.clone();
1019 }
1020 let v = make_vertex(model, p).shape;
1021 corner_vertices.push((p, v.clone()));
1022 v
1023 };
1024 let mut new_edges: Vec<(Shape, [TShapeId; 2])> = Vec::new();
1025 let mut corners: Vec<Shape> = Vec::new();
1026 for (i, first) in leaders.iter().enumerate() {
1027 let sa = placed_surface(model, first, tol)?;
1028 for second in &leaders[i + 1..] {
1029 let sb = placed_surface(model, second, tol)?;
1030 let Ok(SurfaceIntersection::Along(sections)) =
1031 intersect_surfaces(&sa, &sb, IntersectOptions::default(), tol)
1032 else {
1033 continue;
1034 };
1035 let pair = [first.node(), second.node()];
1036 for section in sections {
1037 let curve = section.curve;
1038 let foot = ogeom_algo::project_on_curve(&curve, centre, 64, tol)?;
1039 if !room.contains(foot.point) {
1040 continue;
1041 }
1042 let anchor = foot.parameter;
1043 let mut stops: Vec<(f64, Point, bool)> = Vec::new();
1046 for face in &leaders {
1047 if pair.contains(&face.node()) {
1048 continue;
1049 }
1050 let se = placed_surface(model, face, tol)?;
1051 let hit =
1052 intersect_curve_surface(&curve, &se, CurveSurfaceOptions::default(), tol)?;
1053 for c in &hit.crossings {
1054 stops.push((unwrapped(&curve, c.on_curve, anchor), c.point, false));
1055 }
1056 }
1057 for (p, faces_of) in &dangling {
1058 let shares = faces_of.contains(&pair[0]) && faces_of.contains(&pair[1]);
1059 if !shares || nearest_distance(&curve, *p, tol) > near {
1060 continue;
1061 }
1062 let t = unwrapped(&curve, parameter_near(&curve, *p, tol)?, anchor);
1063 stops.push((t, *p, true));
1064 }
1065 let below = stops
1066 .iter()
1067 .filter(|s| s.0 < anchor)
1068 .max_by(|a, b| a.0.total_cmp(&b.0))
1069 .copied();
1070 let above = stops
1071 .iter()
1072 .filter(|s| s.0 > anchor)
1073 .min_by(|a, b| a.0.total_cmp(&b.0))
1074 .copied();
1075 if stops.is_empty() {
1076 let sides = [first.clone(), second.clone()];
1077 let (edges, cut) =
1078 wrap_round(model, &curve, &sides, is_ring, &mut vertex_at, tol)?;
1079 for edge in edges {
1080 new_edges.push((edge, pair));
1081 }
1082 corners.extend(cut);
1083 continue;
1084 }
1085 let (Some(lo), Some(hi)) = (below, above) else {
1086 ogeom_bail!(
1087 Construction,
1088 "a curve two neighbours meet along leaves the wound \
1089 with no stop on one side; the corner cannot be placed"
1090 );
1091 };
1092 match (lo.2, hi.2) {
1093 (false, false) => {
1095 let (v0, v1) = (vertex_at(model, lo.1), vertex_at(model, hi.1));
1096 let window = if curve.is_periodic() {
1097 let (dlo, dhi) = curve.domain();
1098 let turns = ((lo.0 - dlo) / (dhi - dlo)).floor() * (dhi - dlo);
1099 (lo.0 - turns, hi.0 - turns)
1100 } else {
1101 (lo.0, hi.0)
1102 };
1103 let edge =
1104 make_edge_between(model, curve.clone(), window, &v0, &v1, tol)?.shape;
1105 corners.push(v0);
1106 corners.push(v1);
1107 new_edges.push((edge, pair));
1108 }
1109 (true, false) | (true, true) => corners.push(vertex_at(model, hi.1)),
1112 (false, true) => corners.push(vertex_at(model, lo.1)),
1113 }
1114 }
1115 }
1116 }
1117
1118 let mut out = Vec::new();
1119 let mut extended: FastMap<TShapeId, Shape> = FastMap::default();
1120 let rims_of = |model: &Model, face: &Shape| -> OgeomResult<Vec<Shape>> {
1121 Ok(explore(model, face, Filter::OfType(ShapeType::Edge))?
1122 .into_iter()
1123 .filter(|e| is_ring(e))
1124 .collect())
1125 };
1126 for face in &leaders {
1127 let mut rims = rims_of(model, face)?;
1128 let borders: Vec<Shape> = new_edges
1129 .iter()
1130 .filter(|(_, sides)| sides.contains(&face.node()))
1131 .map(|(e, _)| e.clone())
1132 .collect();
1133 let absorbed: Vec<Shape> = interrupted
1134 .iter()
1135 .filter(|f| f.node() != face.node() && rep_of(f.node()) == face.node())
1136 .cloned()
1137 .collect();
1138 for other in &absorbed {
1139 rims.extend(rims_of(model, other)?);
1140 }
1141 let new_face = rebuild_interrupted(
1142 model,
1143 face,
1144 &absorbed,
1145 &rims,
1146 &borders,
1147 &corners,
1148 &mut extended,
1149 tol,
1150 )?;
1151 for other in &absorbed {
1152 out.push((other.clone(), new_face.clone()));
1153 }
1154 out.push((face.clone(), new_face));
1155 }
1156 Ok(out)
1157}
1158
1159fn feature_groups(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Vec<Shape>>> {
1162 let mut edge_sets: Vec<FastSet<TShapeId>> = Vec::with_capacity(faces.len());
1163 for face in faces {
1164 edge_sets.push(
1165 explore(model, face, Filter::OfType(ShapeType::Edge))?
1166 .iter()
1167 .map(Shape::node)
1168 .collect(),
1169 );
1170 }
1171 let mut group_of: Vec<usize> = (0..faces.len()).collect();
1172 fn root(group_of: &mut [usize], mut i: usize) -> usize {
1174 while group_of[i] != i {
1175 group_of[i] = group_of[group_of[i]];
1176 i = group_of[i];
1177 }
1178 i
1179 }
1180 for i in 0..faces.len() {
1181 for j in i + 1..faces.len() {
1182 if edge_sets[i].intersection(&edge_sets[j]).next().is_some() {
1183 let (a, b) = (root(&mut group_of, i), root(&mut group_of, j));
1184 group_of[a.max(b)] = a.min(b);
1185 }
1186 }
1187 }
1188 let mut groups: FastMap<usize, Vec<Shape>> = FastMap::default();
1189 for (i, face) in faces.iter().enumerate() {
1190 groups
1191 .entry(root(&mut group_of, i))
1192 .or_default()
1193 .push(face.clone());
1194 }
1195 let mut out: Vec<Vec<Shape>> = groups.into_values().collect();
1196 out.sort_by_key(|g| g.iter().map(|f| f.node().index()).min());
1198 Ok(out)
1199}
1200
1201#[allow(
1211 clippy::too_many_arguments,
1212 reason = "one face, everything it is rebuilt from"
1213)]
1214fn rebuild_interrupted(
1215 model: &mut Model,
1216 face: &Shape,
1217 absorbed: &[Shape],
1218 rims: &[Shape],
1219 borders: &[Shape],
1220 corners: &[Shape],
1221 extended: &mut FastMap<TShapeId, Shape>,
1222 tol: Tolerances,
1223) -> OgeomResult<Shape> {
1224 let placement = face.transform(model.datums())?;
1225 let Some(data) = model.node(face).and_then(|n| n.data().as_face().cloned()) else {
1226 ogeom_bail!(Construction, "an interrupted face holds no face data");
1227 };
1228 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
1229 ogeom_bail!(
1230 Construction,
1231 "an interrupted face's surface is not in this model"
1232 );
1233 };
1234 let surface = surface.transformed(&placement, tol)?;
1235 let rim_nodes: FastSet<TShapeId> = rims.iter().map(Shape::node).collect();
1236
1237 let mut wires: Vec<Vec<Shape>> = Vec::new();
1240 let mut carried: Vec<Shape> = Vec::new();
1243 for other in absorbed {
1244 let turned = other.orientation() != face.orientation();
1247 for wire in model.ordered_children_of(&other.oriented(ogeom_topo::Orientation::Forward))? {
1248 let mut edges = model.ordered_children_of(&wire)?;
1249 if !edges.iter().any(|e| rim_nodes.contains(&e.node())) {
1250 if turned {
1251 edges.reverse();
1252 edges = edges.into_iter().map(|e| e.reversed()).collect();
1253 }
1254 wires.push(edges);
1255 continue;
1256 }
1257 let mut rim_vertices: FastSet<TShapeId> = FastSet::default();
1258 for edge in &edges {
1259 if rim_nodes.contains(&edge.node()) {
1260 for v in model.ordered_children_of(edge)? {
1261 rim_vertices.insert(v.node());
1262 }
1263 }
1264 }
1265 for edge in &edges {
1266 if rim_nodes.contains(&edge.node()) {
1267 continue;
1268 }
1269 carried.push(survivor(
1270 model,
1271 edge,
1272 &rim_vertices,
1273 corners,
1274 extended,
1275 tol,
1276 )?);
1277 }
1278 }
1279 }
1280 let own_touched = model
1281 .ordered_children_of(&face.oriented(ogeom_topo::Orientation::Forward))?
1282 .iter()
1283 .map(|wire| model.ordered_children_of(wire))
1284 .collect::<OgeomResult<Vec<_>>>()?
1285 .iter()
1286 .filter(|edges| edges.iter().any(|e| rim_nodes.contains(&e.node())))
1287 .count();
1288 if !carried.is_empty() && own_touched != 1 {
1289 ogeom_bail!(
1290 Construction,
1291 "faces on one surface close as one only through a single wire of \
1292 the face they join"
1293 );
1294 }
1295 for wire in model.ordered_children_of(&face.oriented(ogeom_topo::Orientation::Forward))? {
1296 let edges = model.ordered_children_of(&wire)?;
1297 let touched = edges.iter().any(|e| rim_nodes.contains(&e.node()));
1298 if !touched {
1299 wires.push(edges);
1300 continue;
1301 }
1302 let mut rim_vertices: FastSet<TShapeId> = FastSet::default();
1305 for edge in &edges {
1306 if rim_nodes.contains(&edge.node()) {
1307 for v in model.ordered_children_of(edge)? {
1308 rim_vertices.insert(v.node());
1309 }
1310 }
1311 }
1312 let mut ring: Vec<(Shape, Option<Shape>)> = Vec::with_capacity(edges.len());
1319 for edge in &edges {
1320 if rim_nodes.contains(&edge.node()) {
1321 ring.push((edge.clone(), None));
1322 continue;
1323 }
1324 ring.push((
1325 edge.clone(),
1326 Some(survivor(
1327 model,
1328 edge,
1329 &rim_vertices,
1330 corners,
1331 extended,
1332 tol,
1333 )?),
1334 ));
1335 }
1336 if std::env::var_os("OGEOM_DEBUG_DEFEATURE").is_some() {
1337 let point = |v: &Shape| {
1338 model
1339 .node(v)
1340 .and_then(|n| n.data().as_vertex())
1341 .map(|d| d.point)
1342 };
1343 for (edge, spliced) in &ring {
1344 let (a, b) = edge_ends(model, spliced.as_ref().unwrap_or(edge))?;
1345 eprintln!(
1346 "DEFEATURE wire edge {} {} {:?} .. {:?}",
1347 edge.node().index(),
1348 if spliced.is_none() { "RIM" } else { "kept" },
1349 point(&a),
1350 point(&b)
1351 );
1352 }
1353 }
1354 let mut pool: Vec<Shape> = borders.to_vec();
1355 pool.append(&mut carried);
1356 let mut chained: Vec<Shape> = Vec::new();
1357 if ring.iter().all(|(_, spliced)| spliced.is_none()) {
1358 let Some(start) = pool.first().cloned() else {
1361 ogeom_bail!(
1362 Construction,
1363 "a neighbour lost a whole ring and no recovered edge \
1364 borders it; the wound needs a closure this operation \
1365 does not construct yet"
1366 );
1367 };
1368 let (from, _) = edge_ends(model, &start)?;
1369 chained = bridge_gap(model, &mut pool, &from, &from)?;
1370 let mut was = Vec::new();
1371 for (edge, _) in &ring {
1372 was.extend(sample_edge(model, edge, tol)?);
1373 }
1374 wind_like(model, &mut chained, &was, tol)?;
1375 } else {
1376 let first = ring
1379 .iter()
1380 .position(|(_, spliced)| spliced.is_some())
1381 .unwrap_or(0);
1382 ring.rotate_left(first);
1383 let mut index = 0;
1384 while index < ring.len() {
1385 if let Some(edge) = ring[index].1.clone() {
1386 chained.push(edge);
1387 index += 1;
1388 continue;
1389 }
1390 let run_end = ring[index..]
1391 .iter()
1392 .position(|(_, spliced)| spliced.is_some())
1393 .map_or(ring.len(), |k| index + k);
1394 let Some(previous) = chained.last() else {
1395 ogeom_bail!(Construction, "a wound's rim opens a wire that has no start");
1396 };
1397 let (_, from) = edge_ends(model, previous)?;
1398 let to = match ring.get(run_end).and_then(|(_, spliced)| spliced.as_ref()) {
1399 Some(next) => edge_ends(model, next)?.0,
1400 None => edge_ends(model, &chained[0])?.0,
1402 };
1403 let mut bridge = bridge_gap(model, &mut pool, &from, &to)?;
1404 if from.node() == to.node() {
1407 let mut was = Vec::new();
1408 for (edge, _) in &ring[index..run_end] {
1409 was.extend(sample_edge(model, edge, tol)?);
1410 }
1411 wind_like(model, &mut bridge, &was, tol)?;
1412 }
1413 chained.extend(bridge);
1414 index = run_end;
1415 }
1416 }
1417 if !pool.is_empty() {
1418 ogeom_bail!(
1419 Construction,
1420 "{} recovered edges border this face and its wound's rim has \
1421 nowhere to put them",
1422 pool.len()
1423 );
1424 }
1425 wires.push(chained);
1426 }
1427 let built = ogeom_algo::make_face_with_pcurves(model, surface, &wires, tol)?.shape;
1428 Ok(orient_like(face, built))
1433}
1434
1435fn survivor(
1441 model: &mut Model,
1442 edge: &Shape,
1443 rim_vertices: &FastSet<TShapeId>,
1444 corners: &[Shape],
1445 extended: &mut FastMap<TShapeId, Shape>,
1446 tol: Tolerances,
1447) -> OgeomResult<Shape> {
1448 let replaced = match extended.get(&edge.node()) {
1449 Some(found) => Some(found.clone()),
1450 None => {
1451 let dangles = model
1452 .ordered_children_of(edge)?
1453 .iter()
1454 .any(|v| rim_vertices.contains(&v.node()));
1455 match (dangles, corner_on_edge(model, edge, corners, tol)?) {
1456 (true, Some(corner)) => {
1457 Some(extend_to_corner(model, edge, &corner, extended, tol)?)
1458 }
1459 _ => None,
1460 }
1461 }
1462 };
1463 Ok(match replaced {
1466 Some(fresh) if edge.orientation() == ogeom_topo::Orientation::Reversed => fresh.reversed(),
1467 Some(fresh) => fresh,
1468 None => edge.clone(),
1469 })
1470}
1471
1472fn orient_like(was: &Shape, built: Shape) -> Shape {
1474 if was.orientation() == ogeom_topo::Orientation::Reversed {
1475 built.reversed()
1476 } else {
1477 built
1478 }
1479}
1480
1481fn sample_edge(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<Vec<Point>> {
1483 const STATIONS: usize = 12;
1484 let Some(geometry) = edge_geometry(model, edge, tol)? else {
1485 return Ok(Vec::new());
1486 };
1487 let Some(range) = model
1488 .node(edge)
1489 .and_then(|n| n.data().as_edge())
1490 .and_then(|d| match d.curve3d()? {
1491 ogeom_topo::EdgeRepr::Curve3d { range, .. } => Some(*range),
1492 _ => None,
1493 })
1494 else {
1495 return Ok(Vec::new());
1496 };
1497 let backwards = edge.orientation() == ogeom_topo::Orientation::Reversed;
1498 let mut out = Vec::with_capacity(STATIONS + 1);
1499 for i in 0..=STATIONS {
1500 #[allow(clippy::cast_precision_loss)]
1501 let f = i as f64 / STATIONS as f64;
1502 let f = if backwards { 1.0 - f } else { f };
1503 out.push(geometry.point_at(range.0 + (range.1 - range.0) * f, tol)?);
1504 }
1505 Ok(out)
1506}
1507
1508fn swept_area(points: &[Point]) -> ogeom_math::Vector {
1512 if points.len() < 3 {
1513 return ogeom_math::Vector::ZERO;
1514 }
1515 let mut sum = ogeom_math::Vector::ZERO;
1516 for p in points {
1517 sum += p.to_vector();
1518 }
1519 #[allow(clippy::cast_precision_loss)]
1520 let centre = Point::ORIGIN + sum / points.len() as f64;
1521 let mut area = ogeom_math::Vector::ZERO;
1522 for pair in points.windows(2) {
1523 area += (pair[0] - centre).cross(pair[1] - centre);
1524 }
1525 area
1526}
1527
1528fn edge_geometry(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<Option<Curve>> {
1534 let placement = edge.transform(model.datums())?;
1535 let Some(curve) = model
1536 .node(edge)
1537 .and_then(|n| n.data().as_edge())
1538 .and_then(|d| match d.curve3d()? {
1539 ogeom_topo::EdgeRepr::Curve3d { curve, .. } => Some(*curve),
1540 _ => None,
1541 })
1542 .and_then(|id| model.geometry().curve(id).cloned())
1543 else {
1544 return Ok(None);
1545 };
1546 Ok(Some(curve.transformed(&placement, tol)?))
1547}
1548
1549fn edge_ends(model: &Model, edge: &Shape) -> OgeomResult<(Shape, Shape)> {
1551 ogeom_algo::edge_vertices(model, edge)?
1552 .ok_or_else(|| ogeom_err!(Construction, "an edge of a rebuilt wire has no vertices"))
1553}
1554
1555fn wind_like(
1564 model: &Model,
1565 chain: &mut [Shape],
1566 was: &[Point],
1567 tol: Tolerances,
1568) -> OgeomResult<()> {
1569 let mut now = Vec::new();
1570 for edge in chain.iter() {
1571 now.extend(sample_edge(model, edge, tol)?);
1572 }
1573 if swept_area(&now).dot(swept_area(was)) >= 0.0 {
1574 return Ok(());
1575 }
1576 chain.reverse();
1577 for edge in chain.iter_mut() {
1578 *edge = edge.clone().reversed();
1579 }
1580 Ok(())
1581}
1582
1583fn bridge_gap(
1594 model: &Model,
1595 pool: &mut Vec<Shape>,
1596 from: &Shape,
1597 to: &Shape,
1598) -> OgeomResult<Vec<Shape>> {
1599 let mut chain = Vec::new();
1600 let mut here = from.node();
1601 loop {
1602 let mut found = None;
1603 for (index, edge) in pool.iter().enumerate() {
1604 let (a, b) = edge_ends(model, edge)?;
1605 if a.node() == here {
1606 found = Some((index, false, b));
1607 break;
1608 }
1609 if b.node() == here {
1610 found = Some((index, true, a));
1611 break;
1612 }
1613 }
1614 let Some((index, backwards, next)) = found else {
1615 if here == to.node() {
1616 return Ok(chain);
1619 }
1620 if std::env::var_os("OGEOM_DEBUG_DEFEATURE").is_some() {
1621 let point = |v: &Shape| {
1622 model
1623 .node(v)
1624 .and_then(|n| n.data().as_vertex())
1625 .map(|d| d.point)
1626 };
1627 eprintln!(
1628 "DEFEATURE bridge stuck at {:?} heading for {:?}",
1629 point(from),
1630 point(to)
1631 );
1632 for edge in pool.iter() {
1633 let (a, b) = edge_ends(model, edge)?;
1634 eprintln!(" border {:?} .. {:?}", point(&a), point(&b));
1635 }
1636 }
1637 ogeom_bail!(
1638 Construction,
1639 "no recovered edge bridges the wound's rim; the closure is \
1640 not constructible from what the neighbours meet along"
1641 );
1642 };
1643 let edge = pool.remove(index);
1644 chain.push(if backwards { edge.reversed() } else { edge });
1645 here = next.node();
1646 if here == to.node() {
1647 return Ok(chain);
1648 }
1649 if pool.is_empty() {
1650 ogeom_bail!(
1651 Construction,
1652 "the recovered edges do not reach across the wound's rim; \
1653 the closure is not constructible from them"
1654 );
1655 }
1656 }
1657}
1658
1659fn corner_on_edge(
1662 model: &Model,
1663 edge: &Shape,
1664 corners: &[Shape],
1665 tol: Tolerances,
1666) -> OgeomResult<Option<Shape>> {
1667 let placement = edge.transform(model.datums())?;
1668 let Some((curve, range)) = model
1669 .node(edge)
1670 .and_then(|n| n.data().as_edge())
1671 .and_then(|d| match d.curve3d()? {
1672 ogeom_topo::EdgeRepr::Curve3d { curve, range, .. } => Some((*curve, *range)),
1673 _ => None,
1674 })
1675 else {
1676 return Ok(None);
1677 };
1678 let Some(geometry) = model.geometry().curve(curve).cloned() else {
1679 return Ok(None);
1680 };
1681 let geometry = geometry.transformed(&placement, tol)?;
1682 let head = geometry.point_at(range.0, tol)?;
1683 let tail = geometry.point_at(range.1, tol)?;
1684 let mut best: Option<(f64, Shape)> = None;
1685 for corner in corners {
1686 let Some(p) = model
1687 .node(corner)
1688 .and_then(|n| n.data().as_vertex())
1689 .map(|d| d.point)
1690 else {
1691 continue;
1692 };
1693 let t = parameter_near(&geometry, p, tol)?;
1694 if geometry.point_at(t, tol)?.distance(p) > tol.confusion() * 1e3 {
1695 continue;
1696 }
1697 let gap = head.distance(p).min(tail.distance(p));
1698 if best.as_ref().is_none_or(|(g, _)| gap < *g) {
1699 best = Some((gap, corner.clone()));
1700 }
1701 }
1702 Ok(best.map(|(_, c)| c))
1703}
1704
1705fn extend_to_corner(
1709 model: &mut Model,
1710 edge: &Shape,
1711 corner: &Shape,
1712 extended: &mut FastMap<TShapeId, Shape>,
1713 tol: Tolerances,
1714) -> OgeomResult<Shape> {
1715 if let Some(found) = extended.get(&edge.node()) {
1716 return Ok(found.clone());
1717 }
1718 let placement = edge.transform(model.datums())?;
1719 let Some((curve, range)) = model
1720 .node(edge)
1721 .and_then(|n| n.data().as_edge())
1722 .and_then(|d| match d.curve3d()? {
1723 ogeom_topo::EdgeRepr::Curve3d { curve, range, .. } => Some((*curve, *range)),
1724 _ => None,
1725 })
1726 else {
1727 ogeom_bail!(Construction, "a dangling edge has no curve to extend");
1728 };
1729 let Some(geometry) = model.geometry().curve(curve).cloned() else {
1730 ogeom_bail!(Construction, "a dangling edge's curve is not in this model");
1731 };
1732 let geometry = geometry.transformed(&placement, tol)?;
1733 let corner_point = model
1734 .node(corner)
1735 .and_then(|n| n.data().as_vertex())
1736 .map(|d| d.point)
1737 .ok_or_else(|| ogeom_err!(Construction, "a corner vertex holds no point"))?;
1738
1739 let head = geometry.point_at(range.0, tol)?;
1742 let tail = geometry.point_at(range.1, tol)?;
1743 let t_corner = parameter_near(&geometry, corner_point, tol)?;
1744 let (vertices, new_range) = {
1745 let bounds = model.children_of(edge)?;
1748 let (Some(va), Some(vb)) = (bounds.first().cloned(), bounds.last().cloned()) else {
1749 ogeom_bail!(Construction, "a dangling edge has no vertices");
1750 };
1751 if head.distance(corner_point) <= tail.distance(corner_point) {
1752 ((corner.clone(), vb), (t_corner, range.1))
1753 } else {
1754 ((va, corner.clone()), (range.0, t_corner))
1755 }
1756 };
1757 if new_range.1 <= new_range.0 {
1758 ogeom_bail!(
1759 Construction,
1760 "extending an edge to its corner inverted its range; the corner \
1761 sits on the wrong side of the edge"
1762 );
1763 }
1764 let geometry = match geometry {
1767 Curve::Line(line) => {
1768 let (lo, hi) = ogeom_geom::Curve3d::domain(&line);
1769 Curve::Line(ogeom_geom::LineCurve::over(
1770 line.axis(),
1771 lo.min(new_range.0),
1772 hi.max(new_range.1),
1773 )?)
1774 }
1775 other => other,
1776 };
1777 let built = make_edge_between(model, geometry, new_range, &vertices.0, &vertices.1, tol)?;
1778 extended.insert(edge.node(), built.shape.clone());
1779 Ok(built.shape)
1780}
1781
1782fn parameter_near(curve: &Curve, p: Point, tol: Tolerances) -> OgeomResult<f64> {
1785 match curve {
1786 Curve::Line(line) => {
1787 let axis = line.axis();
1788 Ok((p - axis.location).dot(axis.direction.vector()))
1789 }
1790 Curve::Circle(c) => {
1791 let local = c.circle().frame().to_local(p);
1792 Ok(local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU))
1793 }
1794 _ => Ok(ogeom_algo::project_on_curve(curve, p, 64, tol)?.parameter),
1795 }
1796}
1797
1798fn nearest_distance(curve: &Curve, p: Point, tol: Tolerances) -> f64 {
1799 ogeom_algo::project_on_curve(curve, p, 32, tol).map_or(f64::INFINITY, |pr| pr.distance)
1800}
1801
1802fn edge_length(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<f64> {
1803 let Some((curve, range)) = model
1804 .node(edge)
1805 .and_then(|n| n.data().as_edge())
1806 .and_then(|d| match d.curve3d()? {
1807 ogeom_topo::EdgeRepr::Curve3d { curve, range, .. } => Some((*curve, *range)),
1808 _ => None,
1809 })
1810 else {
1811 return Ok(0.0);
1812 };
1813 let Some(geometry) = model.geometry().curve(curve) else {
1814 return Ok(0.0);
1815 };
1816 ogeom_algo::curve_length(geometry, range, tol)
1817}