1use std::collections::{HashMap, HashSet};
25
26use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
27use ogeom_geom::Curve3d;
28use ogeom_math::Point;
29use ogeom_topo::{
30 EdgeRepr, Model, NodeData, Orientation, Shape, ShapeType, TShapeId, explore_unique,
31};
32
33use crate::bins::Bins;
34use crate::build::{edge_vertices, make_face_on, make_shell, make_wire};
35use crate::history::{Built, History};
36
37pub mod roles {
39 use ogeom_core::Role;
40
41 pub const SEWN_EDGE: Role = Role::op_defined(40);
43 pub const SEWN_FACE: Role = Role::op_defined(41);
45}
46
47pub fn order_edges(model: &Model, edges: &[Shape], tol: Tolerances) -> OgeomResult<Vec<Shape>> {
65 if edges.is_empty() {
66 ogeom_bail!(Construction, "there are no edges to order");
67 }
68 if edges.len() == 1 {
69 return Ok(edges.to_vec());
70 }
71
72 let mut ends = Vec::with_capacity(edges.len());
73 for edge in edges {
74 let Some((start, finish)) = edge_vertices(model, edge)? else {
75 ogeom_bail!(
76 Construction,
77 "an unbounded edge cannot be shown to join anything"
78 );
79 };
80 ends.push((placed(model, &start)?, placed(model, &finish)?));
81 }
82
83 for i in 0..edges.len() {
86 for at in [ends[i].0, ends[i].1] {
87 let meeting = ends
88 .iter()
89 .filter(|(a, b)| a.is_equal(at, tol) || b.is_equal(at, tol))
90 .count();
91 if meeting > 2 {
92 ogeom_bail!(
93 Construction,
94 "{meeting} edges meet at {at:?}; that is a branching \
95 network rather than a wire, and choosing a path through it \
96 would silently drop the branches not chosen"
97 );
98 }
99 }
100 }
101
102 let start = (0..edges.len())
105 .find(|&i| {
106 !ends.iter().enumerate().any(|(j, (a, b))| {
107 j != i && (a.is_equal(ends[i].0, tol) || b.is_equal(ends[i].0, tol))
108 })
109 })
110 .unwrap_or(0);
111
112 let mut used = vec![false; edges.len()];
113 let mut out = Vec::with_capacity(edges.len());
114 used[start] = true;
115 out.push(edges[start].clone());
116 let mut reach = ends[start].1;
117
118 while out.len() < edges.len() {
119 let mut stepped = false;
120 for i in 0..edges.len() {
121 if used[i] {
122 continue;
123 }
124 let (a, b) = ends[i];
125 if a.is_equal(reach, tol) {
126 out.push(edges[i].clone());
127 reach = b;
128 } else if b.is_equal(reach, tol) {
129 out.push(edges[i].reversed());
134 reach = a;
135 } else {
136 continue;
137 }
138 used[i] = true;
139 stepped = true;
140 break;
141 }
142 if !stepped {
143 ogeom_bail!(
144 Construction,
145 "the edges do not form one connected path: {} of {} could not \
146 be reached from the first",
147 edges.len() - out.len(),
148 edges.len()
149 );
150 }
151 }
152 Ok(out)
153}
154
155pub fn make_wire_unordered(
163 model: &mut Model,
164 edges: &[Shape],
165 tol: Tolerances,
166) -> OgeomResult<Built> {
167 let ordered = order_edges(model, edges, tol)?;
168 make_wire(model, &ordered, tol)
169}
170
171#[derive(Debug, Clone)]
173pub struct Sewn {
174 pub shells: Vec<Shape>,
180 pub joined: usize,
182 pub free_edges: Vec<Shape>,
187 pub history: History,
189}
190
191pub fn sew(model: &mut Model, faces: &[Shape], tol: Tolerances) -> OgeomResult<Sewn> {
208 if faces.is_empty() {
209 ogeom_bail!(Construction, "there are no faces to sew");
210 }
211 for face in faces {
212 if model.kind_of(face)? != ShapeType::Face {
213 ogeom_bail!(Construction, "sewing joins faces");
214 }
215 }
216 model.begin_operation();
217
218 let mut vertices = merge_vertices(model, faces, tol)?;
226 let mut rounds = 0;
233 let (rebuilt_edges, merged, joined) = loop {
234 let rebuilt_edges = rebuild_edges(model, faces, &vertices)?;
235
236 let mut catalogue: Vec<(TShapeId, Fingerprint)> = Vec::new();
239 let mut catalogued: HashSet<TShapeId> = HashSet::new();
240 for face in faces {
241 for edge in explore_unique(model, face, ShapeType::Edge)? {
242 let id = rebuilt_edges
243 .get(&edge.node())
244 .copied()
245 .unwrap_or(edge.node());
246 if !catalogued.insert(id) {
247 continue;
248 }
249 if let Some(print) = fingerprint(model, &Shape::of(id), tol)? {
250 catalogue.push((id, print));
251 }
252 }
253 }
254
255 let mut merged: HashMap<TShapeId, (TShapeId, bool)> = HashMap::new();
258 let mut joined = 0;
259 let reach = catalogue
261 .iter()
262 .fold(tol.confusion(), |acc, (_, print)| acc.max(print.width));
263 let mut starts = Bins::new(reach);
264 for (index, (_, print)) in catalogue.iter().enumerate() {
265 starts.insert(print.start, index);
266 }
267 for i in 0..catalogue.len() {
268 if merged.contains_key(&catalogue[i].0) {
269 continue;
270 }
271 for j in twin_candidates(&catalogue, &starts, reach, i) {
272 if merged.contains_key(&catalogue[j].0) {
273 continue;
274 }
275 let Some(flipped) = catalogue[i].1.same_as(&catalogue[j].1, tol)? else {
276 continue;
277 };
278 let (kept_fp, dropped_fp) = (catalogue[i].1.clone(), catalogue[j].1.clone());
284 let survivor = Shape::of(catalogue[i].0);
285 let ends = if flipped {
286 [
287 (kept_fp.start, dropped_fp.end),
288 (kept_fp.end, dropped_fp.start),
289 ]
290 } else {
291 [
292 (kept_fp.start, dropped_fp.start),
293 (kept_fp.end, dropped_fp.end),
294 ]
295 };
296 let bounds = model.children_of(&survivor)?;
297 for vertex in &bounds {
298 if let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) {
299 let at = data.point;
300 let mut need = data.tolerance.get();
301 for (a, b) in &ends {
302 if at.distance(*a) <= need.max(tol.confusion() * 1e2) {
303 need = need.max(a.distance(*b) + tol.confusion());
304 }
305 }
306 if need > data.tolerance.get()
307 && let Some(node) = model.node_mut(vertex)
308 && let NodeData::Vertex(v) = node.data_mut()
309 {
310 v.tolerance = v.tolerance.widen_to(need);
311 }
312 }
313 }
314 merged.insert(catalogue[j].0, (catalogue[i].0, flipped));
315 joined += 1;
316 }
317 }
318 let apart = twin_ends_apart(model, &merged)?;
319 if apart.is_empty() || rounds == 3 {
320 break (rebuilt_edges, merged, joined);
321 }
322 rounds += 1;
323 for (gone, keep) in apart {
324 join_vertex(model, &mut vertices, gone, keep, tol)?;
325 }
326 };
327
328 for (dropped, (kept, flipped)) in merged.clone() {
351 let carried: Vec<EdgeRepr> = model
352 .node_by_id(dropped)
353 .and_then(|n| n.data().as_edge())
354 .map(|d| {
355 d.representations
356 .iter()
357 .filter(|r| r.is_parametric())
358 .cloned()
359 .collect()
360 })
361 .unwrap_or_default();
362 if carried.is_empty() {
363 continue;
364 }
365 let survivor = Shape::of(kept);
366 let paced = repaced_carry(
367 model,
368 &Shape::of(dropped),
369 &survivor,
370 flipped,
371 &carried,
372 tol,
373 )?;
374 let Some(node) = model.node_mut(&survivor) else {
375 ogeom_bail!(Dangling, "an edge is not in this model");
376 };
377 let NodeData::Edge(data) = node.data_mut() else {
378 ogeom_bail!(Construction, "edge node holds no edge data");
379 };
380 for repr in paced {
381 data.add(repr);
382 }
383 }
384
385 let mut substitution: HashMap<TShapeId, (TShapeId, bool)> = HashMap::new();
388 for (original, rebuilt) in &rebuilt_edges {
389 let (final_id, flipped) = merged.get(rebuilt).copied().unwrap_or((*rebuilt, false));
390 substitution.insert(*original, (final_id, flipped));
391 }
392 for (dropped, kept) in &merged {
393 substitution.entry(*dropped).or_insert(*kept);
394 }
395
396 let mut history = History::new();
397 let mut rebuilt = Vec::with_capacity(faces.len());
398 for face in faces {
399 let Some(sewn) = rebuild_face(model, face, &substitution, tol)? else {
400 history.delete(face);
401 continue;
402 };
403 model.set_derived(&sewn, std::slice::from_ref(face), roles::SEWN_FACE)?;
404 history.modify(face, sewn.clone());
405 rebuilt.push(sewn);
406 }
407
408 let groups = connected_groups(model, &rebuilt)?;
409 let mut shells = Vec::with_capacity(groups.len());
410 for group in groups {
411 let shell = make_shell(model, &group)?.shape;
412 for face in &group {
413 history.generate(face, shell.clone());
414 }
415 shells.push(shell);
416 }
417
418 let free_edges = free_edges(model, &rebuilt)?;
419 Ok(Sewn {
420 shells,
421 joined,
422 free_edges,
423 history,
424 })
425}
426
427fn merge_vertices(
431 model: &mut Model,
432 faces: &[Shape],
433 tol: Tolerances,
434) -> OgeomResult<HashMap<TShapeId, TShapeId>> {
435 let tolerance_of = |model: &Model, vertex: &Shape| {
436 model
437 .node(vertex)
438 .and_then(|n| n.data().as_vertex())
439 .map_or(0.0, |d| d.tolerance.get())
440 };
441 let mut loosest = tol.confusion();
445 for face in faces {
446 for vertex in explore_unique(model, face, ShapeType::Vertex)? {
447 loosest = loosest.max(tolerance_of(model, &vertex));
448 }
449 }
450 let mut bins = Bins::new(loosest);
451 let mut seen: Vec<(TShapeId, Point, f64)> = Vec::new();
452 let mut index_of: HashMap<TShapeId, usize> = HashMap::new();
453 let mut widest = tol.confusion();
454 let mut out = HashMap::new();
455 for face in faces {
456 for vertex in explore_unique(model, face, ShapeType::Vertex)? {
457 if index_of.contains_key(&vertex.node()) {
458 continue;
459 }
460 let at = placed(model, &vertex)?;
461 let own = tolerance_of(model, &vertex);
462 let meets = |(_, p, w): &&(TShapeId, Point, f64)| {
468 p.distance(at) <= tol.confusion().max(*w).max(own)
469 };
470 let hit = match bins.near(at, widest.max(own)) {
471 Some(near) => near.into_iter().map(|i| &seen[i]).find(meets),
472 None => seen.iter().find(meets),
473 }
474 .map(|(kept, p, w)| (*kept, *p, *w));
475 match hit {
476 Some((kept, p, w)) => {
477 let need = p.distance(at) + own + tol.confusion();
481 if need > w
482 && let Some(node) = model.node_mut(&Shape::of(kept))
483 && let NodeData::Vertex(v) = node.data_mut()
484 {
485 v.tolerance = v.tolerance.widen_to(need);
486 }
487 if let Some(entry) = index_of.get(&kept).map(|&i| &mut seen[i]) {
488 entry.2 = entry.2.max(need);
489 widest = widest.max(entry.2);
490 }
491 out.insert(vertex.node(), kept);
492 }
493 None => {
494 bins.insert(at, seen.len());
495 index_of.insert(vertex.node(), seen.len());
496 seen.push((vertex.node(), at, own));
497 widest = widest.max(own);
498 }
499 }
500 }
501 }
502 Ok(out)
503}
504
505fn twin_ends_apart(
508 model: &Model,
509 merged: &HashMap<TShapeId, (TShapeId, bool)>,
510) -> OgeomResult<Vec<(TShapeId, TShapeId)>> {
511 let ends = |id: TShapeId| -> Option<(TShapeId, TShapeId)> {
512 let children = model.node_by_id(id)?.children();
513 Some((children.first()?.node(), children.last()?.node()))
514 };
515 let mut out = Vec::new();
516 let mut pairs: Vec<(&TShapeId, &(TShapeId, bool))> = merged.iter().collect();
517 pairs.sort_by_key(|(dropped, _)| dropped.index());
518 for (dropped, (kept, flipped)) in pairs {
519 let (Some((d0, d1)), Some((k0, k1))) = (ends(*dropped), ends(*kept)) else {
520 continue;
521 };
522 let matched = if *flipped {
523 [(d0, k1), (d1, k0)]
524 } else {
525 [(d0, k0), (d1, k1)]
526 };
527 for (d, k) in matched {
528 if d != k && !out.contains(&(d, k)) {
529 out.push((d, k));
530 }
531 }
532 }
533 Ok(out)
534}
535
536fn join_vertex(
539 model: &mut Model,
540 vertices: &mut HashMap<TShapeId, TShapeId>,
541 gone: TShapeId,
542 keep: TShapeId,
543 tol: Tolerances,
544) -> OgeomResult<()> {
545 let resolve = |vertices: &HashMap<TShapeId, TShapeId>, mut v: TShapeId| {
546 while let Some(&next) = vertices.get(&v) {
547 if next == v {
548 break;
549 }
550 v = next;
551 }
552 v
553 };
554 let (gone, keep) = (resolve(vertices, gone), resolve(vertices, keep));
555 if gone == keep {
556 return Ok(());
557 }
558 let (Some(g), Some(k)) = (
559 model
560 .node_by_id(gone)
561 .and_then(|n| n.data().as_vertex())
562 .map(|d| (d.point, d.tolerance.get())),
563 model
564 .node_by_id(keep)
565 .and_then(|n| n.data().as_vertex())
566 .map(|d| d.point),
567 ) else {
568 return Ok(());
569 };
570 let need = g.0.distance(k) + g.1 + tol.confusion();
571 if let Some(node) = model.node_mut(&Shape::of(keep))
572 && let NodeData::Vertex(v) = node.data_mut()
573 {
574 v.tolerance = v.tolerance.widen_to(need);
575 }
576 for target in vertices.values_mut() {
577 if *target == gone {
578 *target = keep;
579 }
580 }
581 vertices.insert(gone, keep);
582 Ok(())
583}
584
585fn repaced_carry(
591 model: &mut Model,
592 dropped: &Shape,
593 survivor: &Shape,
594 flipped: bool,
595 carried: &[EdgeRepr],
596 tol: Tolerances,
597) -> OgeomResult<Vec<EdgeRepr>> {
598 let as_is = || -> Vec<EdgeRepr> {
599 carried
600 .iter()
601 .cloned()
602 .map(|r| if flipped { reversed_repr(r) } else { r })
603 .collect()
604 };
605 let (Some(dropped_fp), Some(kept_fp)) = (
606 fingerprint(&*model, dropped, tol)?,
607 fingerprint(&*model, survivor, tol)?,
608 ) else {
609 return Ok(as_is());
610 };
611 let reach = tol.confusion().max(dropped_fp.width).max(kept_fp.width);
612 if dropped_fp.middle.distance(kept_fp.middle) <= reach {
613 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
614 eprintln!(
615 "SEW carry as is: flipped {flipped}, middles {:.2e} apart, kept {:?} {:?} dropped {:?} {:?}",
616 dropped_fp.middle.distance(kept_fp.middle),
617 kept_fp.start,
618 kept_fp.end,
619 dropped_fp.start,
620 dropped_fp.end
621 );
622 }
623 return Ok(as_is());
624 }
625 const SAMPLES: usize = 24;
626 let (klo, khi) = (kept_fp.range.0, kept_fp.range.1);
627 let mut out = Vec::with_capacity(carried.len());
628 let mut pending: Vec<(
629 ogeom_topo::SurfaceId,
630 ogeom_topo::Location,
631 ogeom_geom::PlanarCurve,
632 (f64, f64),
633 )> = Vec::new();
634 for repr in carried {
635 let EdgeRepr::PCurve {
636 curve: pc_id,
637 surface,
638 location,
639 range: prange,
640 } = repr
641 else {
642 out.push(if flipped {
643 reversed_repr(repr.clone())
644 } else {
645 repr.clone()
646 });
647 continue;
648 };
649 let Some(pcurve) = model.geometry().pcurve(*pc_id) else {
650 ogeom_bail!(Dangling, "pcurve is not in this model");
651 };
652 let periods = model.geometry().surface(*surface).map(|sg| {
653 use ogeom_geom::Surface as _;
654 let ((ua, ub), (va, vb)) = sg.domain();
655 (
656 if sg.is_periodic_u() { ub - ua } else { 0.0 },
657 if sg.is_periodic_v() { vb - va } else { 0.0 },
658 )
659 });
660 let (dlo, dhi) = (dropped_fp.range.0, dropped_fp.range.1);
661 let mut params = Vec::with_capacity(SAMPLES + 1);
662 let mut image: Vec<ogeom_math::Point2> = Vec::with_capacity(SAMPLES + 1);
663 for k in 0..=SAMPLES {
664 #[allow(clippy::cast_precision_loss)]
665 let t = klo + (khi - klo) * (k as f64) / (SAMPLES as f64);
666 let p = kept_fp.curve.point_at(t, tol)?;
667 let foot = crate::project_on_curve(&dropped_fp.curve, p, 64, tol)?;
668 let (lo, hi) = (dlo.min(dhi), dlo.max(dhi));
675 let (da, db) = dropped_fp.curve.domain();
676 let turn = db - da;
677 let mut s = foot.parameter;
678 if turn > 0.0 {
679 if s < lo - tol.parametric() && s + turn <= hi + tol.parametric() {
680 s += turn;
681 } else if s > hi + tol.parametric() && s - turn >= lo - tol.parametric() {
682 s -= turn;
683 }
684 }
685 let s = s.clamp(lo, hi);
686 let pt = if (dhi - dlo).abs() <= f64::MIN_POSITIVE {
687 prange.0
688 } else {
689 prange.0 + (prange.1 - prange.0) * (s - dlo) / (dhi - dlo)
690 };
691 let mut uv = ogeom_geom::Curve2d::point_at(pcurve, pt, tol)?;
692 if let (Some((pu, pv)), Some(prev)) = (periods, image.last()) {
693 for (coord, period, before) in [(&mut uv.x, pu, prev.x), (&mut uv.y, pv, prev.y)] {
694 if period > 0.0 {
695 while *coord - before > period / 2.0 {
696 *coord -= period;
697 }
698 while before - *coord > period / 2.0 {
699 *coord += period;
700 }
701 }
702 }
703 }
704 params.push(t);
705 image.push(uv);
706 }
707 let fitted =
708 ogeom_geom::fit::fit_points_2d_at(¶ms, &image, 3, tol.confusion() * 10.0, tol)?;
709 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
710 use ogeom_geom::Surface as _;
711 let mut worst_sample = 0.0_f64;
712 let mut worst_fit = 0.0_f64;
713 if let Some(sg) = model.geometry().surface(*surface) {
714 for (t, uv) in params.iter().zip(&image) {
715 let p = kept_fp.curve.point_at(*t, tol)?;
716 worst_sample = worst_sample.max(sg.point_at(uv.x, uv.y, tol)?.distance(p));
717 let f = ogeom_geom::Curve2d::point_at(&fitted.curve, *t, tol)?;
718 worst_fit = worst_fit.max(sg.point_at(f.x, f.y, tol)?.distance(p));
719 }
720 }
721 eprintln!(
722 "SEW refit: samples off surface by {worst_sample:.2e}, fit off by {worst_fit:.2e} (fit error {:.2e} met {}) domain {:?} over ({klo:.4}, {khi:.4}); dropped range {:?}",
723 fitted.error,
724 fitted.met,
725 ogeom_geom::Curve2d::domain(&fitted.curve),
726 dropped_fp.range
727 );
728 }
729 if !fitted.met || fitted.error > reach.max(tol.confusion() * 1e3) {
730 out.push(if flipped {
734 reversed_repr(repr.clone())
735 } else {
736 repr.clone()
737 });
738 continue;
739 }
740 pending.push((*surface, location.clone(), fitted.curve.into(), (klo, khi)));
741 }
742 for (surface, location, planar, range) in pending {
743 let curve = model.geometry_mut().add_pcurve(planar);
744 out.push(EdgeRepr::PCurve {
745 curve,
746 surface,
747 location,
748 range,
749 });
750 }
751 Ok(out)
752}
753
754fn reversed_repr(repr: EdgeRepr) -> EdgeRepr {
762 match repr {
763 EdgeRepr::PCurve {
764 curve,
765 surface,
766 location,
767 range,
768 } => EdgeRepr::PCurve {
769 curve,
770 surface,
771 location,
772 range: (range.1, range.0),
773 },
774 EdgeRepr::Seam {
775 forward,
776 reversed,
777 surface,
778 location,
779 range,
780 } => EdgeRepr::Seam {
781 forward: reversed,
782 reversed: forward,
783 surface,
784 location,
785 range: (range.1, range.0),
786 },
787 other => other,
788 }
789}
790
791fn rebuild_edges(
798 model: &mut Model,
799 faces: &[Shape],
800 vertices: &HashMap<TShapeId, TShapeId>,
801) -> OgeomResult<HashMap<TShapeId, TShapeId>> {
802 let mut out = HashMap::new();
803 if vertices.is_empty() {
804 return Ok(out);
805 }
806 let mut done: HashSet<TShapeId> = HashSet::new();
807 for face in faces {
808 for edge in explore_unique(model, face, ShapeType::Edge)? {
809 if !done.insert(edge.node()) {
810 continue;
811 }
812
813 let Some(node) = model.node(&edge) else {
814 ogeom_bail!(Dangling, "edge is not in this model");
815 };
816 let bounds: Vec<Shape> = node.children().to_vec();
817 if !bounds.iter().any(|b| vertices.contains_key(&b.node())) {
818 continue;
819 }
820 let NodeData::Edge(data) = node.data().clone() else {
821 continue;
822 };
823 let moved: Vec<Shape> = bounds
824 .iter()
825 .map(|b| match vertices.get(&b.node()) {
826 Some(kept) => Shape::new(*kept, b.location().clone(), b.orientation()),
827 None => b.clone(),
828 })
829 .collect();
830 let fresh = model.add_edge(*data, &moved)?;
831 out.insert(edge.node(), fresh.node());
832 }
833 }
834 Ok(out)
835}
836
837#[derive(Debug, Clone)]
839struct Fingerprint {
840 start: Point,
841 middle: Point,
842 end: Point,
843 curve: ogeom_geom::Curve,
846 range: (f64, f64),
847 width: f64,
852}
853
854impl Fingerprint {
855 fn off(&self, p: Point, tol: Tolerances) -> OgeomResult<f64> {
857 let foot = crate::project_on_curve(&self.curve, p, 64, tol)?;
858 let (lo, hi) = (
859 self.range.0.min(self.range.1),
860 self.range.0.max(self.range.1),
861 );
862 let mut t = foot.parameter;
863 if self.curve.is_periodic() {
864 let (dlo, dhi) = self.curve.domain();
865 let period = dhi - dlo;
866 if period > 0.0 {
867 t = lo + (t - lo).rem_euclid(period);
868 }
869 }
870 if t >= lo - tol.parametric() && t <= hi + tol.parametric() {
871 return Ok(foot.distance);
872 }
873 Ok(p.distance(self.start).min(p.distance(self.end)))
874 }
875
876 fn same_as(&self, other: &Self, tol: Tolerances) -> OgeomResult<Option<bool>> {
878 let reach = tol.confusion().max(self.width).max(other.width);
879 let near = |a: Point, b: Point| a.distance(b) <= reach;
880 let along = near(self.start, other.start) && near(self.end, other.end);
884 let against = near(self.start, other.end) && near(self.end, other.start);
885 if !along && !against {
886 return Ok(None);
887 }
888 if !near(self.middle, other.middle)
897 && !(other.off(self.middle, tol)? <= reach && self.off(other.middle, tol)? <= reach)
898 {
899 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
900 let foot = crate::project_on_curve(&other.curve, self.middle, 64, tol)?;
901 eprintln!(
902 "SEW near miss: ends agree within {reach:.2e}, middles off {:.2e} / {:.2e} (widths {:.2e}, {:.2e}) at {:?}; foot on other at {:.6} (range {:?}, domain {:?}, periodic {}) distance {:.2e}",
903 other.off(self.middle, tol)?,
904 self.off(other.middle, tol)?,
905 self.width,
906 other.width,
907 self.middle,
908 foot.parameter,
909 other.range,
910 other.curve.domain(),
911 other.curve.is_periodic(),
912 foot.distance
913 );
914 }
915 return Ok(None);
916 }
917 Ok(Some(!along))
918 }
919}
920
921fn twin_candidates(
930 catalogue: &[(TShapeId, Fingerprint)],
931 starts: &Bins,
932 reach: f64,
933 i: usize,
934) -> Vec<usize> {
935 let print = &catalogue[i].1;
936 let (Some(from_start), Some(from_end)) = (
937 starts.near(print.start, reach),
938 starts.near(print.end, reach),
939 ) else {
940 return ((i + 1)..catalogue.len()).collect();
941 };
942 let mut out: Vec<usize> = from_start
943 .into_iter()
944 .chain(from_end)
945 .filter(|&j| j > i)
946 .collect();
947 out.sort_unstable();
948 out.dedup();
949 out
950}
951
952fn fingerprint(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<Option<Fingerprint>> {
954 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
955 return Ok(None);
956 };
957 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
958 return Ok(None);
961 };
962 let Some(geometry) = model.geometry().curve(*curve) else {
963 ogeom_bail!(Dangling, "curve is not in this model");
964 };
965 let placement = edge.transform(model.datums())?;
966 let mut width = data.tolerance.get();
967 for vertex in model.children_of(edge)? {
968 if let Some(v) = model.node(&vertex).and_then(|n| n.data().as_vertex()) {
969 width = width.max(v.tolerance.get());
970 }
971 }
972 use ogeom_geom::Transformable as _;
973 Ok(Some(Fingerprint {
974 start: placement.apply(geometry.point_at(range.0, tol)?),
975 middle: placement.apply(geometry.point_at(f64::midpoint(range.0, range.1), tol)?),
976 end: placement.apply(geometry.point_at(range.1, tol)?),
977 curve: geometry.clone().transformed(&placement, tol)?,
978 range: *range,
979 width,
980 }))
981}
982
983fn placed(model: &Model, vertex: &Shape) -> OgeomResult<Point> {
985 let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
986 ogeom_bail!(Construction, "expected a vertex");
987 };
988 Ok(vertex.transform(model.datums())?.apply(data.point))
989}
990
991fn rebuild_face(
993 model: &mut Model,
994 face: &Shape,
995 merged: &HashMap<TShapeId, (TShapeId, bool)>,
996 tol: Tolerances,
997) -> OgeomResult<Option<Shape>> {
998 let Some(data) = model.node(face).and_then(|n| n.data().as_face()).cloned() else {
999 ogeom_bail!(Construction, "expected a face");
1000 };
1001 let mut touched = false;
1004
1005 let mut wires = Vec::new();
1006 for wire in model.ordered_children_of(face)? {
1007 let mut ring = Vec::new();
1008 for edge in model.ordered_children_of(&wire)? {
1009 match merged.get(&edge.node()) {
1010 Some((kept, flipped)) => {
1011 touched = true;
1012 let mut replacement =
1013 Shape::new(*kept, edge.location().clone(), edge.orientation());
1014 if *flipped {
1015 replacement = replacement.reversed();
1016 }
1017 ring.push(replacement);
1018 }
1019 None => ring.push(edge),
1020 }
1021 }
1022 let one_edge = !ring.is_empty() && ring.iter().all(|e| e.node() == ring[0].node());
1027 if one_edge && ring.len() >= 2 {
1028 if wires.is_empty() {
1029 return Ok(None);
1030 }
1031 continue;
1032 }
1033 let wire = match make_wire(model, &ring, tol) {
1034 Ok(w) => w.shape,
1035 Err(e) => {
1036 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
1037 eprintln!("SEW WIRE FAIL: {e}");
1038 for edge in &ring {
1039 if let Some((a, b)) = edge_vertices(model, edge)? {
1040 let (pa, pb) = (placed(model, &a)?, placed(model, &b)?);
1041 let fp = fingerprint(model, edge, tol)?;
1042 eprintln!(
1043 " edge {:?}{} ({:.5},{:.5},{:.5}) v{} -> ({:.5},{:.5},{:.5}) v{} width {:.2e}",
1044 edge.node(),
1045 if edge.orientation() == ogeom_topo::Orientation::Reversed {
1046 " rev"
1047 } else {
1048 ""
1049 },
1050 pa.x,
1051 pa.y,
1052 pa.z,
1053 a.node().index(),
1054 pb.x,
1055 pb.y,
1056 pb.z,
1057 b.node().index(),
1058 fp.map_or(0.0, |f| f.width)
1059 );
1060 }
1061 }
1062 }
1063 return Err(e);
1064 }
1065 };
1066 wires.push(wire);
1067 }
1068 if !touched {
1069 return Ok(Some(face.clone()));
1070 }
1071 if wires.is_empty() {
1072 return Ok(None);
1073 }
1074 let sewn = match make_face_on(model, data.surface, &wires, tol) {
1075 Ok(built) => built.shape,
1076 Err(e) => {
1077 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
1078 eprintln!("SEW FACE FAIL: {e}");
1079 for (wi, wire) in wires.iter().enumerate() {
1080 for edge in model.ordered_children_of(wire)? {
1081 if let Some((a, b)) = edge_vertices(model, &edge)? {
1082 let (pa, pb) = (placed(model, &a)?, placed(model, &b)?);
1083 eprintln!(
1084 " wire {wi} edge {:?}{} ({:.5},{:.5},{:.5}) v{} -> ({:.5},{:.5},{:.5}) v{}",
1085 edge.node(),
1086 if edge.orientation() == ogeom_topo::Orientation::Reversed {
1087 " rev"
1088 } else {
1089 ""
1090 },
1091 pa.x,
1092 pa.y,
1093 pa.z,
1094 a.node().index(),
1095 pb.x,
1096 pb.y,
1097 pb.z,
1098 b.node().index()
1099 );
1100 }
1101 }
1102 }
1103 }
1104 return Err(e);
1105 }
1106 };
1107 Ok(if face.orientation() == Orientation::Reversed {
1108 Some(sewn.reversed())
1109 } else {
1110 Some(sewn)
1111 })
1112}
1113
1114fn connected_groups(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Vec<Shape>>> {
1116 let mut group_of: Vec<usize> = (0..faces.len()).collect();
1117 let mut edges_of = Vec::with_capacity(faces.len());
1118 for face in faces {
1119 edges_of.push(
1120 explore_unique(model, face, ShapeType::Edge)?
1121 .into_iter()
1122 .map(|e| e.node())
1123 .collect::<Vec<_>>(),
1124 );
1125 }
1126
1127 let mut first_user: HashMap<TShapeId, usize> = HashMap::new();
1130 for (i, edges) in edges_of.iter().enumerate() {
1131 for edge in edges {
1132 let j = *first_user.entry(*edge).or_insert(i);
1133 let (a, b) = (find(&mut group_of, j), find(&mut group_of, i));
1134 if a != b {
1135 group_of[b] = a;
1136 }
1137 }
1138 }
1139
1140 let mut groups: HashMap<usize, Vec<Shape>> = HashMap::new();
1141 for (i, face) in faces.iter().enumerate() {
1142 groups
1143 .entry(find(&mut group_of, i))
1144 .or_default()
1145 .push(face.clone());
1146 }
1147 let mut out: Vec<Vec<Shape>> = groups.into_values().collect();
1148 out.sort_by_key(|group| group.first().map(Shape::node));
1151 Ok(out)
1152}
1153
1154fn find(parent: &mut [usize], mut i: usize) -> usize {
1156 while parent[i] != i {
1158 parent[i] = parent[parent[i]];
1159 i = parent[i];
1160 }
1161 i
1162}
1163
1164fn free_edges(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Shape>> {
1166 let mut uses: HashMap<TShapeId, (usize, Shape)> = HashMap::new();
1167 for face in faces {
1168 for wire in model.children_of(face)? {
1169 for edge in model.children_of(&wire)? {
1170 if model
1171 .node(&edge)
1172 .and_then(|n| n.data().as_edge())
1173 .is_some_and(|d| d.degenerate)
1174 {
1175 continue;
1176 }
1177 let entry = uses.entry(edge.node()).or_insert((0, edge.clone()));
1178 entry.0 += 1;
1179 }
1180 }
1181 }
1182 let mut out: Vec<Shape> = uses
1183 .into_values()
1184 .filter(|(count, _)| count % 2 == 1)
1185 .map(|(_, edge)| edge)
1186 .collect();
1187 out.sort_by_key(Shape::node);
1188 Ok(out)
1189}
1190
1191#[cfg(test)]
1192#[allow(clippy::unwrap_used, clippy::expect_used)]
1193mod tests {
1194 use super::*;
1195 use crate::{check_tessellation, is_shell_closed, make_box, make_polygon};
1196 use ogeom_geom::PlaneSurface;
1197 use ogeom_math::{Frame, Plane, Vector};
1198 use ogeom_topo::Location;
1199
1200 const T: Tolerances = Tolerances::millimetres();
1201
1202 fn fine() -> ogeom_mesh::Deflection {
1203 ogeom_mesh::Deflection {
1204 chord: 0.02,
1205 ..ogeom_mesh::Deflection::default()
1206 }
1207 }
1208
1209 fn loose_square(model: &mut Model, corners: [Point; 4]) -> Shape {
1212 let wire = make_polygon(model, &corners, true, T).unwrap().shape;
1213 let normal =
1214 ogeom_math::Direction::from_cross(corners[1] - corners[0], corners[2] - corners[1], T)
1215 .unwrap();
1216 let frame = ogeom_math::Frame::new(
1217 corners[0],
1218 normal,
1219 ogeom_math::Direction::new(corners[1] - corners[0], T).unwrap(),
1220 T,
1221 )
1222 .unwrap();
1223 let surface = model
1224 .geometry_mut()
1225 .add_surface(PlaneSurface::new(Plane::new(frame)).into());
1226 for edge in model.children_of(&wire).unwrap() {
1227 let (a, b) = crate::edge_vertices(model, &edge).unwrap().unwrap();
1228 let (pa, pb) = (placed(model, &a).unwrap(), placed(model, &b).unwrap());
1229 let flat = |p: Point| {
1230 let l = frame.to_local(p);
1231 ogeom_math::Point2::new(l.x, l.y)
1232 };
1233 crate::attach_pcurve(
1234 model,
1235 &edge,
1236 ogeom_geom::Line2d::segment(flat(pa), flat(pb), T)
1237 .unwrap()
1238 .into(),
1239 surface,
1240 Location::identity(),
1241 (0.0, pa.distance(pb)),
1242 )
1243 .unwrap();
1244 }
1245 crate::make_face_on(model, surface, std::slice::from_ref(&wire), T)
1246 .unwrap()
1247 .shape
1248 }
1249
1250 #[test]
1251 fn edges_in_any_order_come_back_as_a_path() {
1252 let mut model = Model::new();
1253 let corners = [
1254 Point::new(0.0, 0.0, 0.0),
1255 Point::new(1.0, 0.0, 0.0),
1256 Point::new(1.0, 1.0, 0.0),
1257 Point::new(0.0, 1.0, 0.0),
1258 ];
1259 let wire = make_polygon(&mut model, &corners, true, T).unwrap().shape;
1260 let mut edges = model.children_of(&wire).unwrap();
1261 edges.swap(0, 2);
1263 edges[1] = edges[1].reversed();
1264 edges[3] = edges[3].reversed();
1265
1266 let ordered = order_edges(&model, &edges, T).unwrap();
1267 assert_eq!(ordered.len(), 4);
1268 let rebuilt = make_wire(&mut model, &ordered, T).unwrap().shape;
1271 assert!(crate::is_wire_closed(&model, &rebuilt, T).unwrap());
1272 }
1273
1274 #[test]
1275 fn edges_that_do_not_form_one_path_are_refused() {
1276 let mut model = Model::new();
1277 let a = make_polygon(
1278 &mut model,
1279 &[Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
1280 false,
1281 T,
1282 )
1283 .unwrap()
1284 .shape;
1285 let b = make_polygon(
1286 &mut model,
1287 &[Point::new(5.0, 0.0, 0.0), Point::new(6.0, 0.0, 0.0)],
1288 false,
1289 T,
1290 )
1291 .unwrap()
1292 .shape;
1293 let mut edges = model.children_of(&a).unwrap();
1294 edges.extend(model.children_of(&b).unwrap());
1295
1296 let err = order_edges(&model, &edges, T).unwrap_err();
1297 assert!(
1298 err.to_string().contains("connected path"),
1299 "unexpected message: {err}"
1300 );
1301 assert!(order_edges(&model, &[], T).is_err());
1302 }
1303
1304 #[test]
1305 fn a_branching_network_is_refused_rather_than_arbitrarily_walked() {
1306 let mut model = Model::new();
1309 let hub = Point::ORIGIN;
1310 let mut edges = Vec::new();
1311 for tip in [
1312 Point::new(1.0, 0.0, 0.0),
1313 Point::new(0.0, 1.0, 0.0),
1314 Point::new(0.0, 0.0, 1.0),
1315 ] {
1316 let w = make_polygon(&mut model, &[hub, tip], false, T)
1317 .unwrap()
1318 .shape;
1319 edges.extend(model.children_of(&w).unwrap());
1320 }
1321 let err = order_edges(&model, &edges, T).unwrap_err();
1322 assert!(
1323 err.to_string().contains("branching"),
1324 "unexpected message: {err}"
1325 );
1326 }
1327
1328 #[test]
1329 fn two_faces_that_touch_are_sewn_into_one_shell() {
1330 let mut model = Model::new();
1331 let left = loose_square(
1332 &mut model,
1333 [
1334 Point::new(0.0, 0.0, 0.0),
1335 Point::new(1.0, 0.0, 0.0),
1336 Point::new(1.0, 1.0, 0.0),
1337 Point::new(0.0, 1.0, 0.0),
1338 ],
1339 );
1340 let right = loose_square(
1341 &mut model,
1342 [
1343 Point::new(1.0, 0.0, 0.0),
1344 Point::new(2.0, 0.0, 0.0),
1345 Point::new(2.0, 1.0, 0.0),
1346 Point::new(1.0, 1.0, 0.0),
1347 ],
1348 );
1349
1350 let before = explore_unique(&model, &left, ShapeType::Edge)
1352 .unwrap()
1353 .len()
1354 + explore_unique(&model, &right, ShapeType::Edge)
1355 .unwrap()
1356 .len();
1357 assert_eq!(before, 8);
1358
1359 let sewn = sew(&mut model, &[left.clone(), right.clone()], T).unwrap();
1360 assert_eq!(sewn.shells.len(), 1, "they touch, so they are one shell");
1361 assert_eq!(sewn.joined, 1, "one shared edge");
1362 assert_eq!(
1363 explore_unique(&model, &sewn.shells[0], ShapeType::Edge)
1364 .unwrap()
1365 .len(),
1366 7,
1367 "the shared edge is one edge now, not two"
1368 );
1369 assert_eq!(sewn.free_edges.len(), 6);
1372 assert!(!is_shell_closed(&model, &sewn.shells[0]).unwrap());
1373 assert!(sewn.history.is_affected(&left));
1374 }
1375
1376 #[test]
1383 fn twin_edges_join_the_vertices_they_end_on() {
1384 let mut model = Model::new();
1385 let left = loose_square(
1386 &mut model,
1387 [
1388 Point::new(0.0, 0.0, 0.0),
1389 Point::new(1.0, 0.0, 0.0),
1390 Point::new(1.0, 1.0, 0.0),
1391 Point::new(0.0, 1.0, 0.0),
1392 ],
1393 );
1394 let right = loose_square(
1395 &mut model,
1396 [
1397 Point::new(1.0, -0.01, 0.0),
1398 Point::new(2.0, 0.0, 0.0),
1399 Point::new(2.0, 1.0, 0.0),
1400 Point::new(1.0, 1.0, 0.0),
1401 ],
1402 );
1403 for edge in explore_unique(&model, &right, ShapeType::Edge).unwrap() {
1404 let (a, b) = crate::edge_vertices(&model, &edge).unwrap().unwrap();
1405 let (pa, pb) = (placed(&model, &a).unwrap(), placed(&model, &b).unwrap());
1406 if (pa.x - 1.0).abs() < 1e-9
1407 && (pb.x - 1.0).abs() < 1e-9
1408 && let Some(node) = model.node_mut(&edge)
1409 && let NodeData::Edge(data) = node.data_mut()
1410 {
1411 data.tolerance = data.tolerance.widen_to(0.012);
1412 }
1413 }
1414 let sewn = sew(&mut model, &[left, right], T).unwrap();
1415 assert_eq!(sewn.joined, 1, "the loose edge is the left square's");
1416 assert_eq!(sewn.shells.len(), 1);
1417 assert_eq!(
1418 explore_unique(&model, &sewn.shells[0], ShapeType::Vertex)
1419 .unwrap()
1420 .len(),
1421 6,
1422 "the two corners at the bottom of the shared edge are one"
1423 );
1424 assert_eq!(sewn.free_edges.len(), 6);
1425 }
1426
1427 #[test]
1428 fn faces_that_do_not_meet_stay_in_separate_shells() {
1429 let mut model = Model::new();
1431 let here = loose_square(
1432 &mut model,
1433 [
1434 Point::new(0.0, 0.0, 0.0),
1435 Point::new(1.0, 0.0, 0.0),
1436 Point::new(1.0, 1.0, 0.0),
1437 Point::new(0.0, 1.0, 0.0),
1438 ],
1439 );
1440 let far = loose_square(
1441 &mut model,
1442 [
1443 Point::new(50.0, 0.0, 0.0),
1444 Point::new(51.0, 0.0, 0.0),
1445 Point::new(51.0, 1.0, 0.0),
1446 Point::new(50.0, 1.0, 0.0),
1447 ],
1448 );
1449 let sewn = sew(&mut model, &[here, far], T).unwrap();
1450 assert_eq!(sewn.shells.len(), 2);
1451 assert_eq!(sewn.joined, 0);
1452 assert_eq!(sewn.free_edges.len(), 8);
1453 }
1454
1455 #[test]
1456 fn a_boxs_faces_taken_apart_and_sewn_back_close_again() {
1457 let mut model = Model::new();
1462 let solid = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T)
1463 .unwrap()
1464 .shape;
1465 let faces = explore_unique(&model, &solid, ShapeType::Face).unwrap();
1466
1467 let sewn = sew(&mut model, &faces, T).unwrap();
1468 assert_eq!(sewn.shells.len(), 1);
1469 assert!(sewn.free_edges.is_empty(), "a box has no free edges");
1470 assert!(is_shell_closed(&model, &sewn.shells[0]).unwrap());
1471 assert!(
1472 check_tessellation(&model, &sewn.shells[0], fine(), T)
1473 .unwrap()
1474 .is_valid()
1475 );
1476 }
1477
1478 #[test]
1479 fn two_arcs_between_the_same_vertices_are_not_the_same_edge() {
1480 let mut model = Model::new();
1484 let circle = ogeom_math::Circle::new(Frame::WORLD, 1.0, T).unwrap();
1485 let upper = crate::make_edge(
1486 &mut model,
1487 ogeom_geom::CircleCurve::new(circle).into(),
1488 (0.0, std::f64::consts::PI),
1489 T,
1490 )
1491 .unwrap()
1492 .shape;
1493 let lower = crate::make_edge(
1494 &mut model,
1495 ogeom_geom::CircleCurve::new(circle).into(),
1496 (std::f64::consts::PI, std::f64::consts::TAU),
1497 T,
1498 )
1499 .unwrap()
1500 .shape;
1501
1502 let a = fingerprint(&model, &upper, T).unwrap().unwrap();
1503 let b = fingerprint(&model, &lower, T).unwrap().unwrap();
1504 assert!(
1505 a.same_as(&b, T).unwrap().is_none(),
1506 "two different arcs were called the same edge"
1507 );
1508 assert!(a.same_as(&a, T).unwrap() == Some(false));
1509 }
1510
1511 #[test]
1512 fn an_edge_found_the_other_way_round_is_reversed_rather_than_dropped() {
1513 let mut model = Model::new();
1514 let up = Point::new(0.0, 0.0, 1.0);
1515 let down = Point::new(0.0, 0.0, 0.0);
1516 let a = crate::make_edge(
1517 &mut model,
1518 ogeom_geom::LineCurve::segment(down, up, T).unwrap().into(),
1519 (0.0, 1.0),
1520 T,
1521 )
1522 .unwrap()
1523 .shape;
1524 let b = crate::make_edge(
1525 &mut model,
1526 ogeom_geom::LineCurve::segment(up, down, T).unwrap().into(),
1527 (0.0, 1.0),
1528 T,
1529 )
1530 .unwrap()
1531 .shape;
1532
1533 let pa = fingerprint(&model, &a, T).unwrap().unwrap();
1534 let pb = fingerprint(&model, &b, T).unwrap().unwrap();
1535 assert_eq!(
1536 pa.same_as(&pb, T).unwrap(),
1537 Some(true),
1538 "the same edge, running the other way"
1539 );
1540 }
1541
1542 #[test]
1543 fn flipped_merges_carry_their_pcurves_the_right_way_round() {
1544 let mut model = Model::new();
1561 let c = Point::new;
1562 let faces = [
1563 [c(0., 0., 0.), c(0., 1., 0.), c(1., 1., 0.), c(1., 0., 0.)],
1564 [c(0., 0., 1.), c(1., 0., 1.), c(1., 1., 1.), c(0., 1., 1.)],
1565 [c(0., 0., 0.), c(1., 0., 0.), c(1., 0., 1.), c(0., 0., 1.)],
1566 [c(0., 1., 0.), c(0., 1., 1.), c(1., 1., 1.), c(1., 1., 0.)],
1567 [c(0., 0., 0.), c(0., 0., 1.), c(0., 1., 1.), c(0., 1., 0.)],
1568 [c(1., 0., 0.), c(1., 1., 0.), c(1., 1., 1.), c(1., 0., 1.)],
1569 ];
1570 let built: Vec<Shape> = faces.iter().map(|f| loose_square(&mut model, *f)).collect();
1571 let sewn = sew(&mut model, &built, T).unwrap();
1572 assert_eq!(sewn.joined, 12, "every edge pair merged");
1573 assert!(sewn.free_edges.is_empty());
1574 for face in ogeom_topo::explore(
1575 &model,
1576 &sewn.shells[0],
1577 ogeom_topo::Filter::OfType(ShapeType::Face),
1578 )
1579 .unwrap()
1580 {
1581 let mesh = ogeom_mesh::triangulate(&model, &face, fine(), T).unwrap();
1582 assert_eq!(
1583 mesh.triangles.len(),
1584 2,
1585 "a unit square triangulates into two triangles, whichever twin \
1586 survived and whichever way it runs"
1587 );
1588 }
1589 }
1590
1591 #[test]
1592 fn sewing_nothing_and_sewing_the_wrong_kind_are_refused() {
1593 let mut model = Model::new();
1594 assert!(sew(&mut model, &[], T).is_err());
1595 let vertex = model.add_point(Point::ORIGIN);
1596 assert!(sew(&mut model, &[vertex], T).is_err());
1597 }
1598
1599 #[test]
1600 fn sewing_does_not_move_anything() {
1601 let mut model = Model::new();
1606 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
1607 .unwrap()
1608 .shape;
1609 let faces = explore_unique(&model, &solid, ShapeType::Face).unwrap();
1610 let before: Vec<Point> = explore_unique(&model, &solid, ShapeType::Vertex)
1611 .unwrap()
1612 .iter()
1613 .map(|v| placed(&model, v).unwrap())
1614 .collect();
1615
1616 let sewn = sew(&mut model, &faces, T).unwrap();
1617 let after: Vec<Point> = explore_unique(&model, &sewn.shells[0], ShapeType::Vertex)
1618 .unwrap()
1619 .iter()
1620 .map(|v| placed(&model, v).unwrap())
1621 .collect();
1622 assert_eq!(before.len(), after.len());
1623 for p in &after {
1624 assert!(
1625 before.iter().any(|q| q.is_equal(*p, T)),
1626 "a vertex moved: {p:?}"
1627 );
1628 }
1629 let _ = Vector::ZERO;
1630 }
1631}