1use ogeom_core::{FastMap, FastSet};
25
26use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
27use ogeom_geom::Curve3d;
28use ogeom_math::Point;
29use ogeom_topo::{
30 EdgeRepr, Filter, Model, NodeData, Orientation, Shape, ShapeType, TShapeId, explore,
31 explore_unique,
32};
33
34use crate::bins::Bins;
35use crate::build::{edge_vertices, make_face_on, make_shell, make_wire};
36use crate::history::{Built, History};
37
38pub mod roles {
40 use ogeom_core::Role;
41
42 pub const SEWN_EDGE: Role = Role::op_defined(40);
44 pub const SEWN_FACE: Role = Role::op_defined(41);
46}
47
48pub fn order_edges(model: &Model, edges: &[Shape], tol: Tolerances) -> OgeomResult<Vec<Shape>> {
66 if edges.is_empty() {
67 ogeom_bail!(Construction, "there are no edges to order");
68 }
69 if edges.len() == 1 {
70 return Ok(edges.to_vec());
71 }
72
73 let mut ends = Vec::with_capacity(edges.len());
74 for edge in edges {
75 let Some((start, finish)) = edge_vertices(model, edge)? else {
76 ogeom_bail!(
77 Construction,
78 "an unbounded edge cannot be shown to join anything"
79 );
80 };
81 ends.push((placed(model, &start)?, placed(model, &finish)?));
82 }
83
84 for i in 0..edges.len() {
87 for at in [ends[i].0, ends[i].1] {
88 let meeting = ends
89 .iter()
90 .filter(|(a, b)| a.is_equal(at, tol) || b.is_equal(at, tol))
91 .count();
92 if meeting > 2 {
93 ogeom_bail!(
94 Construction,
95 "{meeting} edges meet at {at:?}; that is a branching \
96 network rather than a wire, and choosing a path through it \
97 would silently drop the branches not chosen"
98 );
99 }
100 }
101 }
102
103 let start = (0..edges.len())
106 .find(|&i| {
107 !ends.iter().enumerate().any(|(j, (a, b))| {
108 j != i && (a.is_equal(ends[i].0, tol) || b.is_equal(ends[i].0, tol))
109 })
110 })
111 .unwrap_or(0);
112
113 let mut used = vec![false; edges.len()];
114 let mut out = Vec::with_capacity(edges.len());
115 used[start] = true;
116 out.push(edges[start].clone());
117 let mut reach = ends[start].1;
118
119 while out.len() < edges.len() {
120 let mut stepped = false;
121 for i in 0..edges.len() {
122 if used[i] {
123 continue;
124 }
125 let (a, b) = ends[i];
126 if a.is_equal(reach, tol) {
127 out.push(edges[i].clone());
128 reach = b;
129 } else if b.is_equal(reach, tol) {
130 out.push(edges[i].reversed());
135 reach = a;
136 } else {
137 continue;
138 }
139 used[i] = true;
140 stepped = true;
141 break;
142 }
143 if !stepped {
144 ogeom_bail!(
145 Construction,
146 "the edges do not form one connected path: {} of {} could not \
147 be reached from the first",
148 edges.len() - out.len(),
149 edges.len()
150 );
151 }
152 }
153 Ok(out)
154}
155
156pub fn make_wire_unordered(
164 model: &mut Model,
165 edges: &[Shape],
166 tol: Tolerances,
167) -> OgeomResult<Built> {
168 let ordered = order_edges(model, edges, tol)?;
169 make_wire(model, &ordered, tol)
170}
171
172#[derive(Debug, Clone)]
174pub struct Sewn {
175 pub shells: Vec<Shape>,
181 pub joined: usize,
183 pub free_edges: Vec<Shape>,
188 pub history: History,
190}
191
192impl Sewn {
193 pub fn edges_walked_one_way(&self, model: &Model) -> OgeomResult<Vec<Shape>> {
207 let mut out = Vec::new();
208 for shell in &self.shells {
209 let mut walks: FastMap<(TShapeId, ogeom_topo::Location), (Shape, usize, usize)> =
210 FastMap::default();
211 let mut order = Vec::new();
212 for face in explore(model, shell, Filter::OfType(ShapeType::Face))? {
213 for edge in explore(model, &face, Filter::OfType(ShapeType::Edge))? {
214 if model
215 .node(&edge)
216 .and_then(|n| n.data().as_edge())
217 .is_some_and(|d| d.degenerate)
218 {
219 continue;
220 }
221 let key = (edge.node(), edge.location().clone());
222 let walk = walks.entry(key.clone()).or_insert_with(|| {
223 order.push(key);
224 (edge.clone(), 0, 0)
225 });
226 walk.1 += 1;
227 if edge.orientation() == Orientation::Forward {
228 walk.2 += 1;
229 }
230 }
231 }
232 out.extend(order.into_iter().filter_map(|key| {
233 let (edge, count, forward) = walks.remove(&key)?;
234 (count == 2 && forward != 1).then_some(edge)
235 }));
236 }
237 Ok(out)
238 }
239}
240
241pub fn sew(model: &mut Model, faces: &[Shape], tol: Tolerances) -> OgeomResult<Sewn> {
273 sew_with(model, faces, 0.0, tol)
274}
275
276pub fn sew_within(
298 model: &mut Model,
299 faces: &[Shape],
300 gap: f64,
301 tol: Tolerances,
302) -> OgeomResult<Sewn> {
303 if !(gap.is_finite() && gap >= 0.0) {
304 ogeom_bail!(
305 Construction,
306 "the gap to sew across, {gap}, is not finite and non-negative"
307 );
308 }
309 sew_with(model, faces, gap, tol)
310}
311
312pub fn sew_around(
329 model: &mut Model,
330 faces: &[Shape],
331 settled: &[bool],
332 tol: Tolerances,
333) -> OgeomResult<Sewn> {
334 sew_around_within(model, faces, settled, 0.0, tol)
335}
336
337pub fn sew_around_within(
346 model: &mut Model,
347 faces: &[Shape],
348 settled: &[bool],
349 gap: f64,
350 tol: Tolerances,
351) -> OgeomResult<Sewn> {
352 if !(gap.is_finite() && gap >= 0.0) {
353 ogeom_bail!(
354 Construction,
355 "the gap to sew across, {gap}, is not finite and non-negative"
356 );
357 }
358 if settled.len() != faces.len() {
359 ogeom_bail!(
360 Construction,
361 "{} settled flags for {} faces; one flag per face",
362 settled.len(),
363 faces.len()
364 );
365 }
366 check_faces(model, faces)?;
367 model.begin_operation();
368 if let Some(sewn) = sew_faces(model, faces, settled, gap, tol)? {
369 return Ok(sewn);
370 }
371 let none = vec![false; faces.len()];
372 match sew_faces(model, faces, &none, gap, tol)? {
373 Some(sewn) => Ok(sewn),
374 None => ogeom_bail!(
375 Construction,
376 "sewing with no face settled moved a settled face"
377 ),
378 }
379}
380
381fn check_faces(model: &Model, faces: &[Shape]) -> OgeomResult<()> {
382 if faces.is_empty() {
383 ogeom_bail!(Construction, "there are no faces to sew");
384 }
385 for face in faces {
386 if model.kind_of(face)? != ShapeType::Face {
387 ogeom_bail!(Construction, "sewing joins faces");
388 }
389 }
390 Ok(())
391}
392
393fn sew_with(model: &mut Model, faces: &[Shape], gap: f64, tol: Tolerances) -> OgeomResult<Sewn> {
394 check_faces(model, faces)?;
395 model.begin_operation();
396 let given = faces;
400 let faces = model.unshare_each(given)?.shapes;
401 let none = vec![false; faces.len()];
402 let Some(mut sewn) = sew_faces(model, &faces, &none, gap, tol)? else {
403 ogeom_bail!(
404 Construction,
405 "sewing with no face settled moved a settled face"
406 );
407 };
408 let mut copied = History::new();
409 for (given, face) in given.iter().zip(&faces) {
410 if given.node() != face.node() {
411 copied.copy(given, face.clone());
412 }
413 }
414 if !copied.is_empty() {
415 sewn.history = copied.then(&sewn.history);
416 }
417 Ok(sewn)
418}
419
420fn sew_faces(
424 model: &mut Model,
425 all: &[Shape],
426 settled: &[bool],
427 gap: f64,
428 tol: Tolerances,
429) -> OgeomResult<Option<Sewn>> {
430 let reach = gap.max(tol.confusion());
431 let originals: Vec<Shape> = all
432 .iter()
433 .zip(settled)
434 .filter(|(_, settled)| !**settled)
435 .map(|(face, _)| face.clone())
436 .collect();
437 let mut held_edges: FastSet<TShapeId> = FastSet::default();
440 let mut held_vertices: FastSet<TShapeId> = FastSet::default();
441 for (face, _) in all.iter().zip(settled).filter(|(_, settled)| **settled) {
442 for edge in explore_unique(model, face, ShapeType::Edge)? {
443 held_edges.insert(edge.node());
444 }
445 for vertex in explore_unique(model, face, ShapeType::Vertex)? {
446 held_vertices.insert(vertex.node());
447 }
448 }
449
450 let mut baked = unshared(model, &originals, tol)?;
456 if gap > 0.0 {
457 baked = split_at_vertices(model, &baked, reach, tol)?;
458 }
459 let faces = baked.as_slice();
460
461 let mut vertices = merge_vertices(model, faces, reach, tol)?;
469 let mut rounds = 0;
476 let (rebuilt_edges, mut merged, mut joined) = loop {
477 let rebuilt_edges = rebuild_edges(model, faces, &vertices)?;
478
479 let mut catalogue: Vec<(TShapeId, Fingerprint)> = Vec::new();
482 let mut catalogued: FastSet<TShapeId> = FastSet::default();
483 for face in faces {
484 for edge in explore_unique(model, face, ShapeType::Edge)? {
485 let id = rebuilt_edges
486 .get(&edge.node())
487 .copied()
488 .unwrap_or(edge.node());
489 if !catalogued.insert(id) {
490 continue;
491 }
492 if let Some(mut print) = fingerprint(model, &Shape::of(id), tol)? {
493 print.width = print.width.max(reach);
496 print.ends = print.ends.map(|w| w.max(reach));
497 catalogue.push((id, print));
498 }
499 }
500 }
501
502 let mut merged: FastMap<TShapeId, (TShapeId, bool)> = FastMap::default();
505 let mut joined = 0;
506 let starts = StartBins::new(&catalogue, tol);
507 for i in 0..catalogue.len() {
508 if merged.contains_key(&catalogue[i].0) {
509 continue;
510 }
511 for j in starts.twin_candidates(&catalogue, i) {
512 if merged.contains_key(&catalogue[j].0) {
513 continue;
514 }
515 let Some(flipped) = catalogue[i].1.same_as(&catalogue[j].1, tol)? else {
516 continue;
517 };
518 let (kept_fp, dropped_fp) = (catalogue[i].1.clone(), catalogue[j].1.clone());
524 let survivor = Shape::of(catalogue[i].0);
525 let ends = if flipped {
526 [
527 (kept_fp.start, dropped_fp.end),
528 (kept_fp.end, dropped_fp.start),
529 ]
530 } else {
531 [
532 (kept_fp.start, dropped_fp.start),
533 (kept_fp.end, dropped_fp.end),
534 ]
535 };
536 let bounds = model.children_of(&survivor)?;
537 for vertex in &bounds {
538 if let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) {
539 let at = data.point;
540 let mut need = data.tolerance.get();
541 for (a, b) in &ends {
542 if at.distance(*a) <= need.max(tol.confusion() * 1e2) {
543 need = need.max(a.distance(*b) + tol.confusion());
544 }
545 }
546 if need > data.tolerance.get()
547 && let Some(node) = model.node_mut(vertex)
548 && let NodeData::Vertex(v) = node.data_mut()
549 {
550 v.tolerance = v.tolerance.widen_to(need);
551 }
552 }
553 }
554 if gap > 0.0 {
558 let mut need = 0.0_f64;
559 for p in [dropped_fp.start, dropped_fp.middle, dropped_fp.end] {
560 need = need.max(kept_fp.off(p, tol)?);
561 }
562 let need = need + tol.confusion();
563 model.widen(&survivor, ogeom_core::Tolerance::new(need)?)?;
566 }
567 merged.insert(catalogue[j].0, (catalogue[i].0, flipped));
568 joined += 1;
569 }
570 }
571 let apart = twin_ends_apart(model, &merged)?;
572 if apart.is_empty() || rounds == 3 {
573 break (rebuilt_edges, merged, joined);
574 }
575 rounds += 1;
576 for (gone, keep) in apart {
577 join_vertex(model, &mut vertices, gone, keep, tol)?;
578 }
579 };
580 if vertices.keys().any(|v| held_vertices.contains(v))
583 || rebuilt_edges.keys().any(|e| held_edges.contains(e))
584 || merged.keys().any(|e| held_edges.contains(e))
585 {
586 return Ok(None);
587 }
588
589 let mut carries: Vec<(TShapeId, (TShapeId, bool))> =
614 merged.iter().map(|(d, k)| (*d, *k)).collect();
615 carries.sort_by_key(|(dropped, _)| dropped.index());
616 for (dropped, (kept, flipped)) in carries {
617 let carried: Vec<EdgeRepr> = model
618 .node_by_id(dropped)
619 .and_then(|n| n.data().as_edge())
620 .map(|d| {
621 d.representations
622 .iter()
623 .filter(|r| r.is_parametric())
624 .cloned()
625 .collect()
626 })
627 .unwrap_or_default();
628 if carried.is_empty() {
629 continue;
630 }
631 let survivor = Shape::of(kept);
632 let Some((paced, missed)) = repaced_carry(
633 model,
634 &Shape::of(dropped),
635 &survivor,
636 flipped,
637 &carried,
638 reach,
639 tol,
640 )?
641 else {
642 merged.remove(&dropped);
646 joined -= 1;
647 continue;
648 };
649 let off = carried_off(model, &survivor, &paced, tol)?;
650 let stated = Some(off.map_or(missed, |o| o.max(missed)));
657 if parts_from_own(model, &survivor, &paced, stated, tol)? {
658 merged.remove(&dropped);
659 joined -= 1;
660 continue;
661 }
662 let Some(node) = model.node_mut(&survivor) else {
663 ogeom_bail!(Dangling, "an edge is not in this model");
664 };
665 let NodeData::Edge(data) = node.data_mut() else {
666 ogeom_bail!(Construction, "edge node holds no edge data");
667 };
668 let off = if missed > data.tolerance.get() {
669 Some(off.map_or(missed, |o| o.max(missed)))
670 } else {
671 off
672 };
673 for repr in paced {
674 data.add(repr);
675 }
676 if let Some(off) = off {
683 model.widen(
684 &survivor,
685 ogeom_core::Tolerance::new(off + ogeom_core::Tolerance::MIN.get())?,
686 )?;
687 }
688 }
689
690 let mut substitution: FastMap<TShapeId, (TShapeId, bool)> = FastMap::default();
693 for (original, rebuilt) in &rebuilt_edges {
694 let (final_id, flipped) = merged.get(rebuilt).copied().unwrap_or((*rebuilt, false));
695 substitution.insert(*original, (final_id, flipped));
696 }
697 for (dropped, kept) in &merged {
698 substitution.entry(*dropped).or_insert(*kept);
699 }
700 let mut collapsed: FastSet<TShapeId> = FastSet::default();
703 for (original, rebuilt) in &rebuilt_edges {
704 let ends = |id: TShapeId| -> Option<(TShapeId, TShapeId)> {
705 let children = model.node_by_id(id)?.children();
706 Some((children.first()?.node(), children.last()?.node()))
707 };
708 let (Some((a, b)), Some((c, d))) = (ends(*original), ends(*rebuilt)) else {
709 continue;
710 };
711 if a == b || c != d {
712 continue;
713 }
714 let Some(print) = fingerprint(model, &Shape::of(*rebuilt), tol)? else {
715 continue;
716 };
717 let at = placed(model, &Shape::of(c))?;
718 let held = model
719 .node_by_id(c)
720 .and_then(|n| n.data().as_vertex())
721 .map_or(0.0, |v| v.tolerance.get())
722 .max(reach);
723 let points = [
724 print.start,
725 print.middle,
726 print.end,
727 print.quarters[0],
728 print.quarters[1],
729 ];
730 if points.iter().all(|p| p.distance(at) <= held) {
731 let (id, _) = substitution
732 .get(original)
733 .copied()
734 .unwrap_or((*rebuilt, false));
735 collapsed.insert(id);
736 }
737 }
738
739 let mut history = History::new();
740 let mut rebuilt = Vec::with_capacity(all.len());
741 let mut sources: Vec<(Shape, bool)> = Vec::with_capacity(all.len());
743 let mut sewing = faces.iter().zip(&originals);
744 for (face, settled) in all.iter().zip(settled) {
745 if *settled {
746 model.set_derived(face, std::slice::from_ref(face), roles::SEWN_FACE)?;
749 rebuilt.push(face.clone());
750 sources.push((face.clone(), true));
751 continue;
752 }
753 let Some((face, original)) = sewing.next() else {
754 ogeom_bail!(Construction, "a face to sew was lost");
755 };
756 let Some((sewn, whole)) = rebuild_face(model, face, &substitution, &collapsed, tol)? else {
757 history.delete(original);
758 continue;
759 };
760 model.set_derived(&sewn, std::slice::from_ref(original), roles::SEWN_FACE)?;
761 rebuilt.push(sewn);
764 sources.push((original.clone(), whole && face == original));
765 }
766
767 for face in &rebuilt {
768 seam_walked_both_ways(model, face, tol)?;
769 }
770 let groups = connected_groups(model, &rebuilt)?;
771 let mut shells = Vec::with_capacity(groups.len());
772 for group in groups {
773 let members: Vec<Shape> = group.iter().map(|&i| rebuilt[i].clone()).collect();
774 let mut shell = make_shell(model, &members)?.shape;
775 if orient_group(model, &mut rebuilt, &group, tol)? {
776 let members: Vec<Shape> = group.iter().map(|&i| rebuilt[i].clone()).collect();
777 shell = make_shell(model, &members)?.shape;
778 }
779 for &i in &group {
780 let (original, copy) = &sources[i];
781 if *copy && rebuilt[i].orientation() == original.orientation() {
784 history.copy(original, rebuilt[i].clone());
785 } else {
786 history.modify(original, rebuilt[i].clone());
787 }
788 history.generate(&rebuilt[i], shell.clone());
789 }
790 shells.push(shell);
791 }
792
793 let free_edges = free_edges(model, &rebuilt)?;
794 Ok(Some(Sewn {
795 shells,
796 joined,
797 free_edges,
798 history,
799 }))
800}
801
802#[derive(Debug, Clone, PartialEq, Eq)]
805pub struct Agreement {
806 pub turn: Vec<bool>,
808 pub runs: Vec<Vec<usize>>,
812}
813
814pub fn agreeing_orientation(model: &Model, faces: &[Shape]) -> OgeomResult<Option<Agreement>> {
832 let mut walkers: FastMap<(TShapeId, ogeom_topo::Location), Vec<(usize, bool)>> =
835 FastMap::default();
836 for (k, face) in faces.iter().enumerate() {
837 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
838 if model
839 .node(&edge)
840 .and_then(|n| n.data().as_edge())
841 .is_some_and(|d| d.degenerate)
842 {
843 continue;
844 }
845 walkers
846 .entry((edge.node(), edge.location().clone()))
847 .or_default()
848 .push((k, edge.orientation() == Orientation::Forward));
849 }
850 }
851 let mut across: Vec<Vec<(usize, bool)>> = vec![Vec::new(); faces.len()];
855 for walks in walkers.values() {
856 if let [(a, a_forward), (b, b_forward)] = walks[..]
857 && a != b
858 {
859 let differ = a_forward == b_forward;
860 across[a].push((b, differ));
861 across[b].push((a, differ));
862 }
863 }
864 let mut turn: Vec<Option<bool>> = vec![None; faces.len()];
865 let mut runs = Vec::new();
866 for seed in 0..faces.len() {
867 if turn[seed].is_some() {
868 continue;
869 }
870 turn[seed] = Some(false);
871 let mut run = vec![seed];
872 let mut queue = std::collections::VecDeque::from([seed]);
873 while let Some(k) = queue.pop_front() {
874 let here = turn[k].unwrap_or(false);
875 for &(n, differ) in &across[k] {
876 let want = here != differ;
877 match turn[n] {
878 None => {
879 turn[n] = Some(want);
880 run.push(n);
881 queue.push_back(n);
882 }
883 Some(has) if has != want => return Ok(None),
884 Some(_) => {}
885 }
886 }
887 }
888 let against = run.iter().filter(|&&k| turn[k] == Some(true)).count();
890 if 2 * against > run.len() {
891 for &k in &run {
892 turn[k] = turn[k].map(|t| !t);
893 }
894 }
895 runs.push(run);
896 }
897 Ok(Some(Agreement {
898 turn: turn.into_iter().map(|t| t == Some(true)).collect(),
899 runs,
900 }))
901}
902
903fn orient_group(
911 model: &mut Model,
912 faces: &mut [Shape],
913 group: &[usize],
914 tol: Tolerances,
915) -> OgeomResult<bool> {
916 let uses = walks(model, group.iter().map(|&i| &faces[i]))?;
917 if walked_each_way(&uses) {
918 return Ok(false);
919 }
920 let closed = uses.values().all(|(count, _)| *count >= 2);
921 let members: Vec<Shape> = group.iter().map(|&i| faces[i].clone()).collect();
922 let Some(agreement) = agreeing_orientation(model, &members)? else {
923 return Ok(false);
924 };
925 if !agreement.turn.contains(&true) {
926 return Ok(false);
927 }
928 let mut turned: Vec<Shape> = group
929 .iter()
930 .zip(&agreement.turn)
931 .map(|(&i, t)| {
932 if *t {
933 faces[i].reversed()
934 } else {
935 faces[i].clone()
936 }
937 })
938 .collect();
939 if closed {
940 let candidate = model.add_shell(&turned)?;
941 if let Ok(mesh) =
942 ogeom_mesh::triangulate(model, &candidate, ogeom_mesh::Deflection::default(), tol)
943 && mesh.volume() < 0.0
944 {
945 for face in &mut turned {
946 *face = face.reversed();
947 }
948 }
949 }
950 let mut changed = false;
951 for (&i, face) in group.iter().zip(turned) {
952 changed |= face.orientation() != faces[i].orientation();
953 faces[i] = face;
954 }
955 Ok(changed)
956}
957
958fn unshared(model: &mut Model, faces: &[Shape], tol: Tolerances) -> OgeomResult<Vec<Shape>> {
965 let mut placements: FastMap<TShapeId, Vec<ogeom_topo::Location>> = FastMap::default();
966 let mut held: Vec<Vec<TShapeId>> = Vec::with_capacity(faces.len());
967 let mut placed: Vec<bool> = Vec::with_capacity(faces.len());
968 let mut seen: Vec<ogeom_topo::Location> = Vec::new();
969 for face in faces {
970 let mut nodes = Vec::new();
971 let mut moved = false;
972 for edge in ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge))? {
973 let list = placements.entry(edge.node()).or_default();
974 if !list.contains(edge.location()) {
975 list.push(edge.location().clone());
976 }
977 if !seen.contains(edge.location()) {
978 seen.push(edge.location().clone());
979 }
980 moved |= !edge.location().is_identity();
981 nodes.push(edge.node());
982 }
983 held.push(nodes);
984 placed.push(moved);
985 }
986 let mixed = seen.len() > 1;
987 let placed_twice: Vec<bool> = held
988 .iter()
989 .zip(&placed)
990 .map(|(nodes, moved)| {
991 (mixed && *moved)
992 || nodes
993 .iter()
994 .any(|n| placements.get(n).is_some_and(|l| l.len() > 1))
995 })
996 .collect();
997 let to_bake: Vec<Shape> = faces
998 .iter()
999 .zip(&placed_twice)
1000 .filter(|(_, twice)| **twice)
1001 .map(|(face, _)| face.clone())
1002 .collect();
1003 if to_bake.is_empty() {
1004 return Ok(faces.to_vec());
1005 }
1006 let shell = model.add_shell(&to_bake)?;
1009 let solid = model.add_solid(&[shell])?;
1010 let baked = crate::baked_shape(model, &solid, tol)?;
1011 let mut out = Vec::with_capacity(faces.len());
1012 for (face, twice) in faces.iter().zip(&placed_twice) {
1013 let now = if *twice {
1014 baked.history.modified(face).first().cloned()
1015 } else {
1016 None
1017 };
1018 out.push(now.unwrap_or_else(|| face.clone()));
1019 }
1020 Ok(out)
1021}
1022
1023fn split_at_vertices(
1028 model: &mut Model,
1029 faces: &[Shape],
1030 reach: f64,
1031 tol: Tolerances,
1032) -> OgeomResult<Vec<Shape>> {
1033 let mut vertices: Vec<(TShapeId, Point)> = Vec::new();
1034 let mut seen: FastSet<TShapeId> = FastSet::default();
1035 for face in faces {
1036 for vertex in explore_unique(model, face, ShapeType::Vertex)? {
1037 if vertex.location().is_identity() && seen.insert(vertex.node()) {
1038 vertices.push((vertex.node(), placed(model, &vertex)?));
1039 }
1040 }
1041 }
1042 let mut cuts: Vec<(TShapeId, Vec<(f64, TShapeId)>)> = Vec::new();
1044 let mut walked: FastSet<TShapeId> = FastSet::default();
1045 for face in faces {
1046 for edge in explore_unique(model, face, ShapeType::Edge)? {
1047 if !edge.location().is_identity() || !walked.insert(edge.node()) {
1048 continue;
1049 }
1050 let Some(print) = fingerprint(model, &edge, tol)? else {
1051 continue;
1052 };
1053 let own: Vec<TShapeId> = model.children_of(&edge)?.iter().map(Shape::node).collect();
1054 let deflection = ogeom_mesh::Deflection::default();
1055 let drawn = ogeom_mesh::polyline_of_edge(model, &edge, deflection, tol)?;
1056 let (mut lo, mut hi) = (drawn[0], drawn[0]);
1057 for p in &drawn {
1058 lo = Point::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
1059 hi = Point::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
1060 }
1061 let (r0, r1) = (
1062 print.range.0.min(print.range.1),
1063 print.range.0.max(print.range.1),
1064 );
1065 let margin = reach + deflection.chord;
1069 let mut here: Vec<(f64, TShapeId)> = Vec::new();
1070 for &(v, p) in &vertices {
1071 let outside = p.x < lo.x - margin
1072 || p.y < lo.y - margin
1073 || p.z < lo.z - margin
1074 || p.x > hi.x + margin
1075 || p.y > hi.y + margin
1076 || p.z > hi.z + margin;
1077 if own.contains(&v)
1078 || outside
1079 || p.distance(print.start) <= reach
1080 || p.distance(print.end) <= reach
1081 {
1082 continue;
1083 }
1084 let foot = crate::project_on_curve(&print.curve, p, 64, tol)?;
1085 if foot.distance > reach {
1086 continue;
1087 }
1088 let mut t = foot.parameter;
1089 if print.curve.is_periodic() {
1090 let (dlo, dhi) = print.curve.domain();
1091 let period = dhi - dlo;
1092 if period > 0.0 {
1093 t = r0 + (t - r0).rem_euclid(period);
1094 }
1095 }
1096 if t > r0 + tol.parametric() && t < r1 - tol.parametric() {
1097 here.push((t, v));
1098 }
1099 }
1100 if !here.is_empty() {
1101 here.sort_by(|a, b| a.0.total_cmp(&b.0));
1102 here.dedup_by_key(|c| c.1);
1103 cuts.push((edge.node(), here));
1104 }
1105 }
1106 }
1107 if cuts.is_empty() {
1108 return Ok(faces.to_vec());
1109 }
1110 let mut pieces: FastMap<TShapeId, Vec<Shape>> = FastMap::default();
1112 for (node, at) in cuts {
1113 let edge = Shape::of(node);
1114 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()).cloned() else {
1115 continue;
1116 };
1117 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
1118 continue;
1119 };
1120 let Some((first, last)) = edge_vertices(model, &edge)? else {
1121 continue;
1122 };
1123 let Some(geometry) = model.geometry().curve(curve).cloned() else {
1124 continue;
1125 };
1126 let start_at = geometry.point_at(range.0, tol)?;
1128 let (from, to) = if placed(model, &first)?.distance(start_at)
1129 <= placed(model, &last)?.distance(start_at)
1130 {
1131 (first, last)
1132 } else {
1133 (last, first)
1134 };
1135 let rising = range.1 >= range.0;
1136 let mut stops: Vec<f64> = at.iter().map(|c| c.0).collect();
1137 let mut through: Vec<Shape> = at.iter().map(|c| Shape::of(c.1)).collect();
1138 if !rising {
1139 stops.reverse();
1140 through.reverse();
1141 }
1142 let mut params = vec![range.0];
1143 params.extend(stops);
1144 params.push(range.1);
1145 let mut ends = vec![from];
1146 ends.extend(through);
1147 ends.push(to);
1148 let span = range.1 - range.0;
1149 let mut out = Vec::with_capacity(params.len() - 1);
1150 for k in 0..params.len() - 1 {
1151 let (a, b) = (params[k], params[k + 1]);
1152 let sub = |r: (f64, f64)| {
1155 let at = |t: f64| r.0 + (t - range.0) / span * (r.1 - r.0);
1156 (at(a), at(b))
1157 };
1158 let mut piece = data.clone();
1159 piece.representations.retain(|r| {
1160 matches!(
1161 r,
1162 EdgeRepr::Curve3d { .. } | EdgeRepr::PCurve { .. } | EdgeRepr::Seam { .. }
1163 )
1164 });
1165 for r in &mut piece.representations {
1166 match r {
1167 EdgeRepr::Curve3d { range: own, .. } => *own = (a, b),
1168 EdgeRepr::PCurve { range: own, .. } | EdgeRepr::Seam { range: own, .. } => {
1169 *own = sub(*own);
1170 }
1171 _ => {}
1172 }
1173 }
1174 for (vertex, t) in [(&ends[k], a), (&ends[k + 1], b)] {
1177 let need =
1178 placed(model, vertex)?.distance(geometry.point_at(t, tol)?) + tol.confusion();
1179 if let Some(node) = model.node_mut(vertex)
1180 && let NodeData::Vertex(v) = node.data_mut()
1181 && need > v.tolerance.get()
1182 {
1183 v.tolerance = v.tolerance.widen_to(need);
1184 }
1185 }
1186 out.push(model.add_edge(piece, &[ends[k].clone(), ends[k + 1].clone()])?);
1187 }
1188 pieces.insert(node, out);
1189 }
1190 let mut out = Vec::with_capacity(faces.len());
1191 for face in faces {
1192 let touched = explore_unique(model, face, ShapeType::Edge)?
1193 .iter()
1194 .any(|e| pieces.contains_key(&e.node()));
1195 let Some(data) = model.node(face).and_then(|n| n.data().as_face()).cloned() else {
1196 continue;
1197 };
1198 if !touched || !face.location().is_identity() {
1199 out.push(face.clone());
1200 continue;
1201 }
1202 let stored = face.oriented(Orientation::Forward);
1204 let mut wires = Vec::new();
1205 for wire in model.ordered_children_of(&stored)? {
1206 let mut ring = Vec::new();
1207 for edge in model.ordered_children_of(&wire)? {
1208 match pieces.get(&edge.node()) {
1209 Some(list) if edge.orientation() == Orientation::Reversed => {
1210 ring.extend(list.iter().rev().map(Shape::reversed));
1211 }
1212 Some(list) => ring.extend(list.iter().cloned()),
1213 None => ring.push(edge),
1214 }
1215 }
1216 wires.push(make_wire(model, &ring, tol)?.shape);
1217 }
1218 let split = make_face_on(model, data.surface, &wires, tol)?.shape;
1219 out.push(if face.orientation() == Orientation::Reversed {
1220 split.reversed()
1221 } else {
1222 split
1223 });
1224 }
1225 Ok(out)
1226}
1227
1228fn merge_vertices(
1232 model: &mut Model,
1233 faces: &[Shape],
1234 reach: f64,
1235 tol: Tolerances,
1236) -> OgeomResult<FastMap<TShapeId, TShapeId>> {
1237 let tolerance_of = |model: &Model, vertex: &Shape| {
1238 model
1239 .node(vertex)
1240 .and_then(|n| n.data().as_vertex())
1241 .map_or(0.0, |d| d.tolerance.get())
1242 };
1243 let mut loosest = reach;
1247 for face in faces {
1248 for vertex in explore_unique(model, face, ShapeType::Vertex)? {
1249 loosest = loosest.max(tolerance_of(model, &vertex));
1250 }
1251 }
1252 let mut bins = Bins::new(loosest);
1253 let mut seen: Vec<(TShapeId, Point, f64)> = Vec::new();
1254 let mut index_of: FastMap<TShapeId, usize> = FastMap::default();
1255 let mut widest = reach;
1256 let mut out = FastMap::default();
1257 for face in faces {
1258 for vertex in explore_unique(model, face, ShapeType::Vertex)? {
1259 if index_of.contains_key(&vertex.node()) {
1260 continue;
1261 }
1262 let at = placed(model, &vertex)?;
1263 let own = tolerance_of(model, &vertex);
1264 let meets =
1270 |(_, p, w): &&(TShapeId, Point, f64)| p.distance(at) <= reach.max(*w).max(own);
1271 let hit = match bins.near(at, widest.max(own)) {
1272 Some(near) => near.into_iter().map(|i| &seen[i]).find(meets),
1273 None => seen.iter().find(meets),
1274 }
1275 .map(|(kept, p, w)| (*kept, *p, *w));
1276 match hit {
1277 Some((kept, p, w)) => {
1278 let need = p.distance(at) + own + tol.confusion();
1282 if need > w
1283 && let Some(node) = model.node_mut(&Shape::of(kept))
1284 && let NodeData::Vertex(v) = node.data_mut()
1285 {
1286 v.tolerance = v.tolerance.widen_to(need);
1287 }
1288 if let Some(entry) = index_of.get(&kept).map(|&i| &mut seen[i]) {
1289 entry.2 = entry.2.max(need);
1290 widest = widest.max(entry.2);
1291 }
1292 out.insert(vertex.node(), kept);
1293 }
1294 None => {
1295 bins.insert(at, seen.len());
1296 index_of.insert(vertex.node(), seen.len());
1297 seen.push((vertex.node(), at, own));
1298 widest = widest.max(own);
1299 }
1300 }
1301 }
1302 }
1303 Ok(out)
1304}
1305
1306fn twin_ends_apart(
1309 model: &Model,
1310 merged: &FastMap<TShapeId, (TShapeId, bool)>,
1311) -> OgeomResult<Vec<(TShapeId, TShapeId)>> {
1312 let ends = |id: TShapeId| -> Option<(TShapeId, TShapeId)> {
1313 let children = model.node_by_id(id)?.children();
1314 Some((children.first()?.node(), children.last()?.node()))
1315 };
1316 let mut out = Vec::new();
1317 let mut pairs: Vec<(&TShapeId, &(TShapeId, bool))> = merged.iter().collect();
1318 pairs.sort_by_key(|(dropped, _)| dropped.index());
1319 for (dropped, (kept, flipped)) in pairs {
1320 let (Some((d0, d1)), Some((k0, k1))) = (ends(*dropped), ends(*kept)) else {
1321 continue;
1322 };
1323 let matched = if *flipped {
1324 [(d0, k1), (d1, k0)]
1325 } else {
1326 [(d0, k0), (d1, k1)]
1327 };
1328 for (d, k) in matched {
1329 if d != k && !out.contains(&(d, k)) {
1330 out.push((d, k));
1331 }
1332 }
1333 }
1334 Ok(out)
1335}
1336
1337fn join_vertex(
1340 model: &mut Model,
1341 vertices: &mut FastMap<TShapeId, TShapeId>,
1342 gone: TShapeId,
1343 keep: TShapeId,
1344 tol: Tolerances,
1345) -> OgeomResult<()> {
1346 let resolve = |vertices: &FastMap<TShapeId, TShapeId>, mut v: TShapeId| {
1347 while let Some(&next) = vertices.get(&v) {
1348 if next == v {
1349 break;
1350 }
1351 v = next;
1352 }
1353 v
1354 };
1355 let (gone, keep) = (resolve(vertices, gone), resolve(vertices, keep));
1356 if gone == keep {
1357 return Ok(());
1358 }
1359 let (Some(g), Some(k)) = (
1360 model
1361 .node_by_id(gone)
1362 .and_then(|n| n.data().as_vertex())
1363 .map(|d| (d.point, d.tolerance.get())),
1364 model
1365 .node_by_id(keep)
1366 .and_then(|n| n.data().as_vertex())
1367 .map(|d| d.point),
1368 ) else {
1369 return Ok(());
1370 };
1371 let need = g.0.distance(k) + g.1 + tol.confusion();
1372 if let Some(node) = model.node_mut(&Shape::of(keep))
1373 && let NodeData::Vertex(v) = node.data_mut()
1374 {
1375 v.tolerance = v.tolerance.widen_to(need);
1376 }
1377 for target in vertices.values_mut() {
1378 if *target == gone {
1379 *target = keep;
1380 }
1381 }
1382 vertices.insert(gone, keep);
1383 Ok(())
1384}
1385
1386fn parts_from_own(
1394 model: &Model,
1395 survivor: &Shape,
1396 carried: &[EdgeRepr],
1397 off: Option<f64>,
1398 tol: Tolerances,
1399) -> OgeomResult<bool> {
1400 let Some(data) = model.node(survivor).and_then(|n| n.data().as_edge()) else {
1401 ogeom_bail!(Dangling, "an edge is not in this model");
1402 };
1403 let stated = off.map_or(data.tolerance.get(), |o| {
1404 data.tolerance
1405 .get()
1406 .max(o + ogeom_core::Tolerance::MIN.get())
1407 });
1408 let reach = crate::check::CHART_GAP_SLACK * stated.max(tol.approximation());
1409 for repr in carried {
1410 let EdgeRepr::PCurve {
1411 curve,
1412 surface,
1413 location,
1414 range,
1415 } = repr
1416 else {
1417 continue;
1418 };
1419 for own in &data.representations {
1420 let EdgeRepr::PCurve {
1421 curve: own_curve,
1422 surface: own_surface,
1423 location: own_location,
1424 range: own_range,
1425 } = own
1426 else {
1427 continue;
1428 };
1429 if own_surface != surface || own_location != location {
1430 continue;
1431 }
1432 let (Some(on), Some(a), Some(b)) = (
1433 model.geometry().surface(*surface),
1434 model.geometry().pcurve(*own_curve),
1435 model.geometry().pcurve(*curve),
1436 ) else {
1437 ogeom_bail!(Dangling, "an edge names geometry not in this model");
1438 };
1439 if lifted_apart(on, (a, *own_range), (b, *range), tol)? > reach {
1440 return Ok(true);
1441 }
1442 }
1443 }
1444 Ok(false)
1445}
1446
1447fn lifted_apart(
1450 surface: &ogeom_geom::SurfaceGeometry,
1451 (a, ra): (&ogeom_geom::PlanarCurve, (f64, f64)),
1452 (b, rb): (&ogeom_geom::PlanarCurve, (f64, f64)),
1453 tol: Tolerances,
1454) -> OgeomResult<f64> {
1455 use ogeom_geom::{Curve2d as _, Surface as _};
1456 const SAMPLES: u32 = 4;
1457 let mut widest = 0.0_f64;
1458 for i in 0..=SAMPLES {
1459 let t = f64::from(i) / f64::from(SAMPLES);
1460 let pa = a.point_at(ra.0 + (ra.1 - ra.0) * t, tol)?;
1461 let pb = b.point_at(rb.0 + (rb.1 - rb.0) * t, tol)?;
1462 let apart = surface
1463 .point_at(pa.x, pa.y, tol)?
1464 .distance(surface.point_at(pb.x, pb.y, tol)?);
1465 widest = widest.max(apart);
1466 }
1467 Ok(widest)
1468}
1469
1470fn repaced_carry(
1485 model: &mut Model,
1486 dropped: &Shape,
1487 survivor: &Shape,
1488 flipped: bool,
1489 carried: &[EdgeRepr],
1490 floor: f64,
1491 tol: Tolerances,
1492) -> OgeomResult<Option<(Vec<EdgeRepr>, f64)>> {
1493 let as_is = || -> Option<(Vec<EdgeRepr>, f64)> {
1494 Some((
1495 carried
1496 .iter()
1497 .cloned()
1498 .map(|r| if flipped { reversed_repr(r) } else { r })
1499 .collect(),
1500 0.0,
1501 ))
1502 };
1503 let (Some(dropped_fp), Some(kept_fp)) = (
1504 fingerprint(&*model, dropped, tol)?,
1505 fingerprint(&*model, survivor, tol)?,
1506 ) else {
1507 return Ok(as_is());
1508 };
1509 let reach = floor.max(dropped_fp.width).max(kept_fp.width);
1510 if dropped_fp.middle.distance(kept_fp.middle) <= reach {
1511 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
1512 eprintln!(
1513 "SEW carry as is: flipped {flipped}, middles {:.2e} apart, kept {:?} {:?} dropped {:?} {:?}",
1514 dropped_fp.middle.distance(kept_fp.middle),
1515 kept_fp.start,
1516 kept_fp.end,
1517 dropped_fp.start,
1518 dropped_fp.end
1519 );
1520 }
1521 return Ok(as_is());
1522 }
1523 const SAMPLES: usize = 24;
1524 let (klo, khi) = (kept_fp.range.0, kept_fp.range.1);
1525 let closes = {
1526 let (da, db) = dropped_fp.curve.domain();
1527 db > da
1528 && dropped_fp
1529 .curve
1530 .point_at(da, tol)?
1531 .distance(dropped_fp.curve.point_at(db, tol)?)
1532 <= tol.confusion()
1533 };
1534 let stated = model
1539 .node(survivor)
1540 .and_then(|n| n.data().as_edge())
1541 .map_or(0.0, |d| d.tolerance.get());
1542 let cap = floor.max(tol.confusion() * crate::pcurve_fit::TRIM_CAP);
1543 let mut missed = 0.0_f64;
1544 let mut out = Vec::with_capacity(carried.len());
1545 let mut pending: Vec<(
1546 ogeom_topo::SurfaceId,
1547 ogeom_topo::Location,
1548 ogeom_geom::PlanarCurve,
1549 (f64, f64),
1550 )> = Vec::new();
1551 for repr in carried {
1552 let EdgeRepr::PCurve {
1553 curve: pc_id,
1554 surface,
1555 location,
1556 range: prange,
1557 } = repr
1558 else {
1559 out.push(if flipped {
1560 reversed_repr(repr.clone())
1561 } else {
1562 repr.clone()
1563 });
1564 continue;
1565 };
1566 let Some(pcurve) = model.geometry().pcurve(*pc_id) else {
1567 ogeom_bail!(Dangling, "pcurve is not in this model");
1568 };
1569 let periods = model.geometry().surface(*surface).map(|sg| {
1570 use ogeom_geom::Surface as _;
1571 let ((ua, ub), (va, vb)) = sg.domain();
1572 (
1573 if sg.is_periodic_u() { ub - ua } else { 0.0 },
1574 if sg.is_periodic_v() { vb - va } else { 0.0 },
1575 )
1576 });
1577 let (dlo, dhi) = (dropped_fp.range.0, dropped_fp.range.1);
1578 let mut params = Vec::with_capacity(SAMPLES + 1);
1579 let mut image: Vec<ogeom_math::Point2> = Vec::with_capacity(SAMPLES + 1);
1580 for k in 0..=SAMPLES {
1581 #[allow(clippy::cast_precision_loss)]
1582 let t = klo + (khi - klo) * (k as f64) / (SAMPLES as f64);
1583 let p = kept_fp.curve.point_at(t, tol)?;
1584 let foot = crate::project_on_curve(&dropped_fp.curve, p, 64, tol)?;
1585 let (lo, hi) = (dlo.min(dhi), dlo.max(dhi));
1592 let (da, db) = dropped_fp.curve.domain();
1593 let turn = db - da;
1594 let mut s = foot.parameter;
1595 if closes {
1596 if s < lo - tol.parametric() && s + turn <= hi + tol.parametric() {
1597 s += turn;
1598 } else if s > hi + tol.parametric() && s - turn >= lo - tol.parametric() {
1599 s -= turn;
1600 }
1601 }
1602 let s = if (s < lo || s > hi) && closes {
1607 let round = |a: f64, b: f64| {
1608 let d = (a - b).rem_euclid(turn);
1609 d.min(turn - d)
1610 };
1611 if round(s, lo) <= round(s, hi) { lo } else { hi }
1612 } else {
1613 s.clamp(lo, hi)
1614 };
1615 let pt = if (dhi - dlo).abs() <= f64::MIN_POSITIVE {
1616 prange.0
1617 } else {
1618 prange.0 + (prange.1 - prange.0) * (s - dlo) / (dhi - dlo)
1619 };
1620 let mut uv = ogeom_geom::Curve2d::point_at(pcurve, pt, tol)?;
1621 if let (Some((pu, pv)), Some(prev)) = (periods, image.last()) {
1622 for (coord, period, before) in [(&mut uv.x, pu, prev.x), (&mut uv.y, pv, prev.y)] {
1623 if period > 0.0 {
1624 while *coord - before > period / 2.0 {
1625 *coord -= period;
1626 }
1627 while before - *coord > period / 2.0 {
1628 *coord += period;
1629 }
1630 }
1631 }
1632 }
1633 params.push(t);
1634 image.push(uv);
1635 }
1636 let fitted =
1637 ogeom_geom::fit::fit_points_2d_at(¶ms, &image, 3, tol.confusion() * 10.0, tol)?;
1638 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
1639 use ogeom_geom::Surface as _;
1640 let mut worst_sample = 0.0_f64;
1641 let mut worst_fit = 0.0_f64;
1642 if let Some(sg) = model.geometry().surface(*surface) {
1643 for (t, uv) in params.iter().zip(&image) {
1644 let p = kept_fp.curve.point_at(*t, tol)?;
1645 worst_sample = worst_sample.max(sg.point_at(uv.x, uv.y, tol)?.distance(p));
1646 let f = ogeom_geom::Curve2d::point_at(&fitted.curve, *t, tol)?;
1647 worst_fit = worst_fit.max(sg.point_at(f.x, f.y, tol)?.distance(p));
1648 }
1649 }
1650 eprintln!(
1651 "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 {:?}",
1652 fitted.error,
1653 fitted.met,
1654 ogeom_geom::Curve2d::domain(&fitted.curve),
1655 dropped_fp.range
1656 );
1657 }
1658 if !fitted.met || fitted.error > reach.max(tol.confusion() * 1e3) {
1659 out.push(if flipped {
1663 reversed_repr(repr.clone())
1664 } else {
1665 repr.clone()
1666 });
1667 continue;
1668 }
1669 let planar: ogeom_geom::PlanarCurve = fitted.curve.into();
1670 let Some(chart) = model.geometry().surface(*surface) else {
1671 ogeom_bail!(Dangling, "surface is not in this model");
1672 };
1673 let placement = location.composed(model.datums())?;
1674 let gap = |f: f64| -> OgeomResult<Option<ogeom_math::Vector>> {
1675 let t = klo + (khi - klo) * f;
1676 let uv = ogeom_geom::Curve2d::point_at(&planar, t, tol)?;
1677 let Ok(lifted) = ogeom_geom::Surface::point_at(chart, uv.x, uv.y, tol) else {
1678 return Ok(None);
1679 };
1680 Ok(Some(
1681 placement.apply(lifted) - kept_fp.curve.point_at(t, tol)?,
1682 ))
1683 };
1684 let mut breaks = crate::pcurve_gap::curve_breaks(&kept_fp.curve, (klo, khi));
1685 breaks.extend(crate::pcurve_gap::pcurve_breaks(&planar, (klo, khi)));
1686 let off = crate::pcurve_gap::widest_refined(
1687 &gap,
1688 &breaks,
1689 stated,
1690 tol.confusion(),
1691 tol.parametric() / (khi - klo).abs(),
1692 )?;
1693 if off > cap {
1694 return Ok(None);
1695 }
1696 missed = missed.max(off);
1697 pending.push((*surface, location.clone(), planar, (klo, khi)));
1698 }
1699 for (surface, location, planar, range) in pending {
1700 let curve = model.geometry_mut().add_pcurve(planar);
1701 out.push(EdgeRepr::PCurve {
1702 curve,
1703 surface,
1704 location,
1705 range,
1706 });
1707 }
1708 Ok(Some((out, missed)))
1709}
1710
1711fn seam_walked_both_ways(model: &mut Model, face: &Shape, tol: Tolerances) -> OgeomResult<()> {
1723 use ogeom_geom::Curve2d as _;
1724 let Some(surface) = model
1725 .node(face)
1726 .and_then(|n| n.data().as_face())
1727 .map(|d| d.surface)
1728 else {
1729 return Ok(());
1730 };
1731 for wire in model.ordered_children_of(face)? {
1732 let walk = model.ordered_children_of(&wire)?;
1733 let n = walk.len();
1734 for (i, occurrence) in walk.iter().enumerate() {
1735 if occurrence.orientation() != ogeom_topo::Orientation::Forward {
1736 continue;
1737 }
1738 let Some(back) = walk.iter().position(|o| {
1739 o.node() == occurrence.node()
1740 && o.orientation() == ogeom_topo::Orientation::Reversed
1741 }) else {
1742 continue;
1743 };
1744 let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
1745 continue;
1746 };
1747 let plain: Vec<(ogeom_topo::PCurveId, (f64, f64), ogeom_topo::Location)> = data
1748 .representations
1749 .iter()
1750 .filter_map(|r| match r {
1751 EdgeRepr::PCurve {
1752 curve,
1753 surface: on,
1754 location,
1755 range,
1756 } if *on == surface => Some((*curve, *range, location.clone())),
1757 _ => None,
1758 })
1759 .collect();
1760 let [(first, range, at), (second, other_range, other_at)] = plain.as_slice() else {
1761 continue;
1762 };
1763 if at != other_at || range != other_range {
1764 continue;
1765 }
1766 let before = &walk[(i + n - 1) % n];
1769 if before.node() == occurrence.node() || back == (i + n - 1) % n {
1770 continue;
1771 }
1772 let Some(arrived) = chart_end(model, before, surface, tol)? else {
1773 continue;
1774 };
1775 let start = |id: ogeom_topo::PCurveId| -> OgeomResult<Option<ogeom_math::Point2>> {
1776 let Some(pc) = model.geometry().pcurve(id) else {
1777 return Ok(None);
1778 };
1779 Ok(Some(pc.point_at(range.0, tol)?))
1780 };
1781 let (Some(a), Some(b)) = (start(*first)?, start(*second)?) else {
1782 continue;
1783 };
1784 let (forward, reversed) = if a.distance(arrived) <= b.distance(arrived) {
1785 (*first, *second)
1786 } else {
1787 (*second, *first)
1788 };
1789 let (range, at) = (*range, at.clone());
1790 let Some(node) = model.node_mut(occurrence) else {
1791 continue;
1792 };
1793 let ogeom_topo::NodeData::Edge(data) = node.data_mut() else {
1794 continue;
1795 };
1796 data.representations.retain(|r| {
1797 !matches!(r, EdgeRepr::PCurve { curve, surface: on, .. }
1798 if *on == surface && (*curve == forward || *curve == reversed))
1799 });
1800 data.add(EdgeRepr::Seam {
1801 forward,
1802 reversed,
1803 surface,
1804 location: at,
1805 range,
1806 });
1807 }
1808 }
1809 Ok(())
1810}
1811
1812fn chart_end(
1816 model: &Model,
1817 occurrence: &Shape,
1818 surface: ogeom_topo::SurfaceId,
1819 tol: Tolerances,
1820) -> OgeomResult<Option<ogeom_math::Point2>> {
1821 use ogeom_geom::Curve2d as _;
1822 let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
1823 return Ok(None);
1824 };
1825 let reversed = occurrence.orientation() == ogeom_topo::Orientation::Reversed;
1826 let (id, range) = match data.pcurve_for(surface, occurrence.location()) {
1827 Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
1828 Some(EdgeRepr::Seam {
1829 forward,
1830 reversed: back,
1831 range,
1832 ..
1833 }) => (if reversed { *back } else { *forward }, *range),
1834 _ => return Ok(None),
1835 };
1836 let Some(pc) = model.geometry().pcurve(id) else {
1837 return Ok(None);
1838 };
1839 Ok(Some(
1840 pc.point_at(if reversed { range.0 } else { range.1 }, tol)?,
1841 ))
1842}
1843
1844fn carried_off(
1853 model: &Model,
1854 survivor: &Shape,
1855 carried: &[EdgeRepr],
1856 tol: Tolerances,
1857) -> OgeomResult<Option<f64>> {
1858 let Some(data) = model.node(survivor).and_then(|n| n.data().as_edge()) else {
1859 ogeom_bail!(Dangling, "an edge is not in this model");
1860 };
1861 let Some(EdgeRepr::Curve3d {
1862 curve,
1863 range,
1864 location,
1865 }) = data.curve3d()
1866 else {
1867 return Ok(None);
1868 };
1869 let Some(curve) = model.geometry().curve(*curve) else {
1870 ogeom_bail!(Dangling, "curve is not in this model");
1871 };
1872 let reach = data.tolerance.get();
1876 let mut widest: Option<f64> = None;
1877 for repr in carried {
1878 let (sides, prange, surface, at) = match repr {
1879 EdgeRepr::PCurve {
1880 curve,
1881 range,
1882 surface,
1883 location,
1884 } => ([Some(*curve), None], *range, *surface, location),
1885 EdgeRepr::Seam {
1886 forward,
1887 reversed,
1888 range,
1889 surface,
1890 location,
1891 } => (
1892 [Some(*forward), Some(*reversed)],
1893 *range,
1894 *surface,
1895 location,
1896 ),
1897 _ => continue,
1898 };
1899 if at != location {
1900 continue;
1901 }
1902 let Some(surface) = model.geometry().surface(surface) else {
1903 ogeom_bail!(Dangling, "surface is not in this model");
1904 };
1905 for id in sides.into_iter().flatten() {
1906 let Some(pcurve) = model.geometry().pcurve(id) else {
1907 ogeom_bail!(Dangling, "pcurve is not in this model");
1908 };
1909 let gap = |f: f64| -> OgeomResult<Option<ogeom_math::Vector>> {
1910 let on_curve = curve.point_at(range.0 + (range.1 - range.0) * f, tol)?;
1911 let uv = ogeom_geom::Curve2d::point_at(
1912 pcurve,
1913 prange.0 + (prange.1 - prange.0) * f,
1914 tol,
1915 )?;
1916 let Ok(lifted) = ogeom_geom::Surface::point_at(surface, uv.x, uv.y, tol) else {
1917 return Ok(None);
1918 };
1919 let along = lifted - on_curve;
1920 if along.magnitude() <= reach {
1921 return Ok(Some(along));
1922 }
1923 let (foot, off) =
1924 crate::pcurve_gap::nearest_point_on_stretch(curve, *range, lifted, tol)?;
1925 Ok(Some(if off < along.magnitude() {
1926 lifted - curve.point_at(foot, tol)?
1927 } else {
1928 along
1929 }))
1930 };
1931 let mut breaks = crate::pcurve_gap::curve_breaks(curve, *range);
1932 breaks.extend(crate::pcurve_gap::pcurve_breaks(pcurve, prange));
1933 let wide = (range.1 - range.0).abs().max((prange.1 - prange.0).abs());
1934 let off = crate::pcurve_gap::widest_refined(
1939 &gap,
1940 &breaks,
1941 reach,
1942 tol.confusion(),
1943 tol.parametric() / wide,
1944 )?
1945 .max(crate::pcurve_gap::pcurve_gap(
1946 (curve, *range),
1947 (pcurve, prange),
1948 surface,
1949 reach,
1950 tol,
1951 )?);
1952 if off > reach {
1953 widest = Some(widest.map_or(off, |w| w.max(off)));
1954 }
1955 }
1956 }
1957 Ok(widest)
1958}
1959
1960fn reversed_repr(repr: EdgeRepr) -> EdgeRepr {
1968 match repr {
1969 EdgeRepr::PCurve {
1970 curve,
1971 surface,
1972 location,
1973 range,
1974 } => EdgeRepr::PCurve {
1975 curve,
1976 surface,
1977 location,
1978 range: (range.1, range.0),
1979 },
1980 EdgeRepr::Seam {
1981 forward,
1982 reversed,
1983 surface,
1984 location,
1985 range,
1986 } => EdgeRepr::Seam {
1987 forward: reversed,
1988 reversed: forward,
1989 surface,
1990 location,
1991 range: (range.1, range.0),
1992 },
1993 other => other,
1994 }
1995}
1996
1997fn rebuild_edges(
2004 model: &mut Model,
2005 faces: &[Shape],
2006 vertices: &FastMap<TShapeId, TShapeId>,
2007) -> OgeomResult<FastMap<TShapeId, TShapeId>> {
2008 let mut out = FastMap::default();
2009 if vertices.is_empty() {
2010 return Ok(out);
2011 }
2012 let mut done: FastSet<TShapeId> = FastSet::default();
2013 for face in faces {
2014 for edge in explore_unique(model, face, ShapeType::Edge)? {
2015 if !done.insert(edge.node()) {
2016 continue;
2017 }
2018
2019 let Some(node) = model.node(&edge) else {
2020 ogeom_bail!(Dangling, "edge is not in this model");
2021 };
2022 let bounds: Vec<Shape> = node.children().to_vec();
2023 if !bounds.iter().any(|b| vertices.contains_key(&b.node())) {
2024 continue;
2025 }
2026 let NodeData::Edge(data) = node.data().clone() else {
2027 continue;
2028 };
2029 let moved: Vec<Shape> = bounds
2030 .iter()
2031 .map(|b| match vertices.get(&b.node()) {
2032 Some(kept) => Shape::new(*kept, b.location().clone(), b.orientation()),
2033 None => b.clone(),
2034 })
2035 .collect();
2036 let fresh = model.add_edge(*data, &moved)?;
2037 out.insert(edge.node(), fresh.node());
2038 }
2039 }
2040 Ok(out)
2041}
2042
2043#[derive(Debug, Clone)]
2045struct Fingerprint {
2046 start: Point,
2047 middle: Point,
2048 end: Point,
2049 quarters: [Point; 2],
2052 curve: ogeom_geom::Curve,
2055 range: (f64, f64),
2056 width: f64,
2061 ends: [f64; 2],
2065}
2066
2067impl Fingerprint {
2068 fn off(&self, p: Point, tol: Tolerances) -> OgeomResult<f64> {
2070 let closed = match &self.curve {
2073 ogeom_geom::Curve::Line(line) => {
2074 let t = ogeom_math::elementary::line_parameter(line.axis(), p);
2075 let at = ogeom_math::elementary::line_at(line.axis(), t).point;
2076 Some((t, p.distance(at)))
2077 }
2078 ogeom_geom::Curve::Circle(circle) => {
2079 match ogeom_math::elementary::circle_parameter(&circle.circle(), p, tol) {
2080 Ok(angle) => {
2081 let t = if circle.is_reversed() {
2082 (-angle).rem_euclid(core::f64::consts::TAU)
2083 } else {
2084 angle
2085 };
2086 let at = ogeom_math::elementary::circle_at(&circle.circle(), angle).point;
2087 Some((t, p.distance(at)))
2088 }
2089 Err(_) => None,
2090 }
2091 }
2092 _ => None,
2093 };
2094 let (parameter, distance) = match closed {
2095 Some(foot) => foot,
2096 None => {
2097 let foot = crate::project_on_curve(&self.curve, p, 64, tol)?;
2098 (foot.parameter, foot.distance)
2099 }
2100 };
2101 let (lo, hi) = (
2102 self.range.0.min(self.range.1),
2103 self.range.0.max(self.range.1),
2104 );
2105 let mut t = parameter;
2106 if self.curve.is_periodic() {
2107 let (dlo, dhi) = self.curve.domain();
2108 let period = dhi - dlo;
2109 if period > 0.0 {
2110 t = lo + (t - lo).rem_euclid(period);
2111 }
2112 }
2113 if t >= lo - tol.parametric() && t <= hi + tol.parametric() {
2114 return Ok(distance);
2115 }
2116 Ok(p.distance(self.start).min(p.distance(self.end)))
2117 }
2118
2119 fn same_as(&self, other: &Self, tol: Tolerances) -> OgeomResult<Option<bool>> {
2121 let reach = tol.confusion().max(self.width).max(other.width);
2122 let near = |a: Point, b: Point| a.distance(b) <= reach;
2123 let meet =
2128 |a: Point, wa: f64, b: Point, wb: f64| a.distance(b) <= tol.confusion().max(wa).max(wb);
2129 let [self_start, self_end] = self.ends;
2130 let [other_start, other_end] = other.ends;
2131 let along = meet(self.start, self_start, other.start, other_start)
2135 && meet(self.end, self_end, other.end, other_end);
2136 let against = meet(self.start, self_start, other.end, other_end)
2137 && meet(self.end, self_end, other.start, other_start);
2138 if !along && !against {
2139 return Ok(None);
2140 }
2141 if !near(self.middle, other.middle)
2150 && !(other.off(self.middle, tol)? <= reach && self.off(other.middle, tol)? <= reach)
2151 {
2152 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
2153 let foot = crate::project_on_curve(&other.curve, self.middle, 64, tol)?;
2154 eprintln!(
2155 "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}",
2156 other.off(self.middle, tol)?,
2157 self.off(other.middle, tol)?,
2158 self.width,
2159 other.width,
2160 self.middle,
2161 foot.parameter,
2162 other.range,
2163 other.curve.domain(),
2164 other.curve.is_periodic(),
2165 foot.distance
2166 );
2167 }
2168 return Ok(None);
2169 }
2170 if along && against {
2173 let [q1, q3] = self.quarters;
2174 let [p1, p3] = other.quarters;
2175 let forward = q1.distance(p1) + q3.distance(p3);
2176 let backward = q1.distance(p3) + q3.distance(p1);
2177 return Ok(Some(backward < forward));
2178 }
2179 Ok(Some(!along))
2180 }
2181}
2182
2183struct StartBins {
2191 tight: Bins,
2193 cell: f64,
2194 loose: Bins,
2196 widest: f64,
2197}
2198
2199impl StartBins {
2200 fn new(catalogue: &[(TShapeId, Fingerprint)], tol: Tolerances) -> Self {
2201 let mut widths: Vec<f64> = catalogue
2202 .iter()
2203 .map(|(_, print)| print.width.max(tol.confusion()))
2204 .collect();
2205 widths.sort_by(f64::total_cmp);
2206 let widest = widths.last().copied().unwrap_or(tol.confusion());
2207 let cell = widths
2209 .get(widths.len().saturating_sub(1) * 9 / 10)
2210 .copied()
2211 .unwrap_or(tol.confusion());
2212 let (mut tight, mut loose) = (Bins::new(cell), Bins::new(widest));
2213 for (index, (_, print)) in catalogue.iter().enumerate() {
2214 if print.width <= cell {
2215 tight.insert(print.start, index);
2216 } else {
2217 loose.insert(print.start, index);
2218 }
2219 }
2220 Self {
2221 tight,
2222 cell,
2223 loose,
2224 widest,
2225 }
2226 }
2227
2228 fn twin_candidates(&self, catalogue: &[(TShapeId, Fingerprint)], i: usize) -> Vec<usize> {
2232 let print = &catalogue[i].1;
2233 let near_tight = self.cell.max(print.width);
2236 let found = [
2237 self.tight.near(print.start, near_tight),
2238 self.tight.near(print.end, near_tight),
2239 self.loose.near(print.start, self.widest),
2240 self.loose.near(print.end, self.widest),
2241 ];
2242 if found.iter().any(Option::is_none) {
2245 let reach = self.widest.max(near_tight);
2246 return ((i + 1)..catalogue.len())
2247 .filter(|&j| {
2248 let start = catalogue[j].1.start;
2249 start.distance(print.start) <= reach || start.distance(print.end) <= reach
2250 })
2251 .collect();
2252 }
2253 let mut out: Vec<usize> = found
2254 .into_iter()
2255 .flatten()
2256 .flatten()
2257 .filter(|&j| j > i)
2258 .collect();
2259 out.sort_unstable();
2260 out.dedup();
2261 out
2262 }
2263}
2264
2265fn fingerprint(model: &Model, edge: &Shape, tol: Tolerances) -> OgeomResult<Option<Fingerprint>> {
2267 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
2268 return Ok(None);
2269 };
2270 let Some(EdgeRepr::Curve3d {
2271 curve,
2272 range,
2273 location,
2274 }) = data.curve3d()
2275 else {
2276 return Ok(None);
2279 };
2280 let Some(geometry) = model.geometry().curve(*curve) else {
2281 ogeom_bail!(Dangling, "curve is not in this model");
2282 };
2283 let own = location.composed(model.datums())?;
2287 let outer = edge.transform(model.datums())?;
2288 let own_width = data.tolerance.get();
2289 let bounds = model.children_of(edge)?;
2290 let reach_of = |vertex: Option<&Shape>| {
2291 vertex
2292 .and_then(|v| model.node(v))
2293 .and_then(|n| n.data().as_vertex())
2294 .map_or(own_width, |v| own_width.max(v.tolerance.get()))
2295 };
2296 let ends = [reach_of(bounds.first()), reach_of(bounds.last())];
2297 let mut width = own_width;
2298 for vertex in &bounds {
2299 if let Some(v) = model.node(vertex).and_then(|n| n.data().as_vertex()) {
2300 width = width.max(v.tolerance.get());
2301 }
2302 }
2303 use ogeom_geom::Transformable as _;
2304 let placement = |p: Point| outer.apply(own.apply(p));
2305 Ok(Some(Fingerprint {
2306 start: placement(geometry.point_at(range.0, tol)?),
2307 middle: placement(geometry.point_at(f64::midpoint(range.0, range.1), tol)?),
2308 end: placement(geometry.point_at(range.1, tol)?),
2309 quarters: [
2310 placement(geometry.point_at(range.0 + 0.25 * (range.1 - range.0), tol)?),
2311 placement(geometry.point_at(range.0 + 0.75 * (range.1 - range.0), tol)?),
2312 ],
2313 curve: geometry
2314 .clone()
2315 .transformed(&own, tol)?
2316 .transformed(&outer, tol)?,
2317 range: *range,
2318 width,
2319 ends,
2320 }))
2321}
2322
2323fn placed(model: &Model, vertex: &Shape) -> OgeomResult<Point> {
2325 let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
2326 ogeom_bail!(Construction, "expected a vertex");
2327 };
2328 Ok(vertex.transform(model.datums())?.apply(data.point))
2329}
2330
2331fn rebuild_face(
2333 model: &mut Model,
2334 face: &Shape,
2335 merged: &FastMap<TShapeId, (TShapeId, bool)>,
2336 collapsed: &FastSet<TShapeId>,
2337 tol: Tolerances,
2338) -> OgeomResult<Option<(Shape, bool)>> {
2339 let mut whole = true;
2340 let Some(data) = model.node(face).and_then(|n| n.data().as_face()).cloned() else {
2341 ogeom_bail!(Construction, "expected a face");
2342 };
2343 let mut touched = false;
2346
2347 let stored = face.oriented(Orientation::Forward);
2351 let mut wires = Vec::new();
2352 for wire in model.ordered_children_of(&stored)? {
2353 let mut ring = Vec::new();
2354 let mut shrunk = false;
2355 for edge in model.ordered_children_of(&wire)? {
2356 match merged.get(&edge.node()) {
2357 Some((kept, flipped)) => {
2358 touched = true;
2359 if collapsed.contains(kept) {
2360 shrunk = true;
2361 continue;
2362 }
2363 let mut replacement =
2364 Shape::new(*kept, edge.location().clone(), edge.orientation());
2365 if *flipped {
2366 replacement = replacement.reversed();
2367 }
2368 ring.push(replacement);
2369 }
2370 None => ring.push(edge),
2371 }
2372 }
2373 let one_edge = !ring.is_empty() && ring.iter().all(|e| e.node() == ring[0].node());
2380 let out_and_back = shrunk
2381 && ring.iter().all(|e| {
2382 let walks = |o: Orientation| {
2383 ring.iter()
2384 .filter(|f| f.node() == e.node() && f.orientation() == o)
2385 .count()
2386 };
2387 walks(Orientation::Forward) == 1 && walks(Orientation::Reversed) == 1
2388 });
2389 if (one_edge && ring.len() >= 2) || out_and_back {
2390 if wires.is_empty() {
2391 return Ok(None);
2392 }
2393 whole = false;
2394 continue;
2395 }
2396 let wire = match make_wire(model, &ring, tol) {
2397 Ok(w) => w.shape,
2398 Err(e) => {
2399 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
2400 eprintln!("SEW WIRE FAIL: {e}");
2401 for edge in &ring {
2402 if let Some((a, b)) = edge_vertices(model, edge)? {
2403 let (pa, pb) = (placed(model, &a)?, placed(model, &b)?);
2404 let fp = fingerprint(model, edge, tol)?;
2405 eprintln!(
2406 " edge {:?}{} ({:.5},{:.5},{:.5}) v{} -> ({:.5},{:.5},{:.5}) v{} width {:.2e}",
2407 edge.node(),
2408 if edge.orientation() == ogeom_topo::Orientation::Reversed {
2409 " rev"
2410 } else {
2411 ""
2412 },
2413 pa.x,
2414 pa.y,
2415 pa.z,
2416 a.node().index(),
2417 pb.x,
2418 pb.y,
2419 pb.z,
2420 b.node().index(),
2421 fp.map_or(0.0, |f| f.width)
2422 );
2423 }
2424 }
2425 }
2426 return Err(e);
2427 }
2428 };
2429 wires.push(wire);
2430 }
2431 if !touched {
2432 return Ok(Some((face.clone(), whole)));
2433 }
2434 if wires.is_empty() {
2435 return Ok(None);
2436 }
2437 let sewn = match make_face_on(model, data.surface, &wires, tol) {
2438 Ok(built) => built.shape,
2439 Err(e) => {
2440 if std::env::var_os("OGEOM_DEBUG_SEW").is_some() {
2441 eprintln!("SEW FACE FAIL: {e}");
2442 for (wi, wire) in wires.iter().enumerate() {
2443 for edge in model.ordered_children_of(wire)? {
2444 if let Some((a, b)) = edge_vertices(model, &edge)? {
2445 let (pa, pb) = (placed(model, &a)?, placed(model, &b)?);
2446 eprintln!(
2447 " wire {wi} edge {:?}{} ({:.5},{:.5},{:.5}) v{} -> ({:.5},{:.5},{:.5}) v{}",
2448 edge.node(),
2449 if edge.orientation() == ogeom_topo::Orientation::Reversed {
2450 " rev"
2451 } else {
2452 ""
2453 },
2454 pa.x,
2455 pa.y,
2456 pa.z,
2457 a.node().index(),
2458 pb.x,
2459 pb.y,
2460 pb.z,
2461 b.node().index()
2462 );
2463 }
2464 }
2465 }
2466 }
2467 return Err(e);
2468 }
2469 };
2470 Ok(Some((
2471 if face.orientation() == Orientation::Reversed {
2472 sewn.reversed()
2473 } else {
2474 sewn
2475 },
2476 whole,
2477 )))
2478}
2479
2480fn walks<'a>(
2483 model: &Model,
2484 faces: impl Iterator<Item = &'a Shape>,
2485) -> OgeomResult<FastMap<(TShapeId, ogeom_topo::Location), (usize, usize)>> {
2486 let mut uses: FastMap<(TShapeId, ogeom_topo::Location), (usize, usize)> = FastMap::default();
2487 for face in faces {
2488 for edge in explore(model, face, Filter::OfType(ShapeType::Edge))? {
2489 if model
2490 .node(&edge)
2491 .and_then(|n| n.data().as_edge())
2492 .is_some_and(|d| d.degenerate)
2493 {
2494 continue;
2495 }
2496 let walk = uses
2497 .entry((edge.node(), edge.location().clone()))
2498 .or_default();
2499 walk.0 += 1;
2500 if edge.orientation() == Orientation::Forward {
2501 walk.1 += 1;
2502 }
2503 }
2504 }
2505 Ok(uses)
2506}
2507
2508fn walked_each_way(uses: &FastMap<(TShapeId, ogeom_topo::Location), (usize, usize)>) -> bool {
2510 uses.values()
2511 .all(|(count, forward)| *count != 2 || *forward == 1)
2512}
2513
2514fn connected_groups(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Vec<usize>>> {
2516 let mut group_of: Vec<usize> = (0..faces.len()).collect();
2517 let mut edges_of = Vec::with_capacity(faces.len());
2518 for face in faces {
2519 edges_of.push(
2520 explore_unique(model, face, ShapeType::Edge)?
2521 .into_iter()
2522 .map(|e| e.node())
2523 .collect::<Vec<_>>(),
2524 );
2525 }
2526
2527 let mut first_user: FastMap<TShapeId, usize> = FastMap::default();
2530 for (i, edges) in edges_of.iter().enumerate() {
2531 for edge in edges {
2532 let j = *first_user.entry(*edge).or_insert(i);
2533 let (a, b) = (find(&mut group_of, j), find(&mut group_of, i));
2534 if a != b {
2535 group_of[b] = a;
2536 }
2537 }
2538 }
2539
2540 let mut groups: FastMap<usize, Vec<usize>> = FastMap::default();
2541 for i in 0..faces.len() {
2542 groups.entry(find(&mut group_of, i)).or_default().push(i);
2543 }
2544 let mut out: Vec<Vec<usize>> = groups.into_values().collect();
2545 out.sort_by_key(|group| group.first().map(|&i| faces[i].node()));
2548 Ok(out)
2549}
2550
2551fn find(parent: &mut [usize], mut i: usize) -> usize {
2553 while parent[i] != i {
2555 parent[i] = parent[parent[i]];
2556 i = parent[i];
2557 }
2558 i
2559}
2560
2561fn free_edges(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Shape>> {
2563 let mut uses: FastMap<TShapeId, (usize, Shape)> = FastMap::default();
2564 for face in faces {
2565 for wire in model.children_of(face)? {
2566 for edge in model.children_of(&wire)? {
2567 if model
2568 .node(&edge)
2569 .and_then(|n| n.data().as_edge())
2570 .is_some_and(|d| d.degenerate)
2571 {
2572 continue;
2573 }
2574 let entry = uses.entry(edge.node()).or_insert((0, edge.clone()));
2575 entry.0 += 1;
2576 }
2577 }
2578 }
2579 let mut out: Vec<Shape> = uses
2580 .into_values()
2581 .filter(|(count, _)| count % 2 == 1)
2582 .map(|(_, edge)| edge)
2583 .collect();
2584 out.sort_by_key(Shape::node);
2585 Ok(out)
2586}
2587
2588#[cfg(test)]
2589#[allow(clippy::unwrap_used, clippy::expect_used)]
2590mod tests {
2591 use super::*;
2592 use crate::{check_tessellation, is_shell_closed, make_box, make_polygon};
2593 use ogeom_geom::PlaneSurface;
2594 use ogeom_math::{Frame, Plane, Vector};
2595 use ogeom_topo::Location;
2596
2597 const T: Tolerances = Tolerances::millimetres();
2598
2599 fn fine() -> ogeom_mesh::Deflection {
2600 ogeom_mesh::Deflection {
2601 chord: 0.02,
2602 ..ogeom_mesh::Deflection::default()
2603 }
2604 }
2605
2606 fn loose_polygon<const N: usize>(model: &mut Model, corners: [Point; N]) -> Shape {
2609 let wire = make_polygon(model, &corners, true, T).unwrap().shape;
2610 let normal =
2611 ogeom_math::Direction::from_cross(corners[1] - corners[0], corners[2] - corners[1], T)
2612 .unwrap();
2613 let frame = ogeom_math::Frame::new(
2614 corners[0],
2615 normal,
2616 ogeom_math::Direction::new(corners[1] - corners[0], T).unwrap(),
2617 T,
2618 )
2619 .unwrap();
2620 let surface = model
2621 .geometry_mut()
2622 .add_surface(PlaneSurface::new(Plane::new(frame)).into());
2623 for edge in model.children_of(&wire).unwrap() {
2624 let (a, b) = crate::edge_vertices(model, &edge).unwrap().unwrap();
2625 let (pa, pb) = (placed(model, &a).unwrap(), placed(model, &b).unwrap());
2626 let flat = |p: Point| {
2627 let l = frame.to_local(p);
2628 ogeom_math::Point2::new(l.x, l.y)
2629 };
2630 crate::attach_pcurve(
2631 model,
2632 &edge,
2633 ogeom_geom::Line2d::segment(flat(pa), flat(pb), T)
2634 .unwrap()
2635 .into(),
2636 surface,
2637 Location::identity(),
2638 (0.0, pa.distance(pb)),
2639 )
2640 .unwrap();
2641 }
2642 crate::make_face_on(model, surface, std::slice::from_ref(&wire), T)
2643 .unwrap()
2644 .shape
2645 }
2646
2647 #[test]
2651 fn squares_a_little_apart_sew_across_the_gap_asked_for() {
2652 let at = |x: f64, y: f64| Point::new(x, y, 0.0);
2653 for (gap, shells, joined) in [(0.1, 1, 1), (0.01, 2, 0)] {
2654 let mut model = Model::new();
2655 let first = loose_polygon(
2656 &mut model,
2657 [at(0.0, 0.0), at(1.0, 0.0), at(1.0, 1.0), at(0.0, 1.0)],
2658 );
2659 let second = loose_polygon(
2660 &mut model,
2661 [at(1.05, 0.0), at(2.05, 0.0), at(2.05, 1.0), at(1.05, 1.0)],
2662 );
2663 let sewn = sew_within(&mut model, &[first, second], gap, T).unwrap();
2664 assert_eq!(sewn.shells.len(), shells, "gap {gap}");
2665 assert_eq!(sewn.joined, joined, "gap {gap}");
2666 if shells == 1 {
2667 let d = crate::check(&model, &sewn.shells[0], T).unwrap();
2668 assert!(d.is_usable(), "{d}");
2669 }
2670 }
2671 }
2672
2673 #[test]
2681 fn a_strip_narrower_than_the_gap_between_faces_goes() {
2682 let at = |x: f64, y: f64| Point::new(x, y, 0.0);
2683 let mut model = Model::new();
2684 let below = loose_polygon(
2685 &mut model,
2686 [
2687 at(1.0, -1.0),
2688 at(1.0, 0.0),
2689 at(0.5, 0.0),
2690 at(0.0, 0.0),
2691 at(0.0, -1.0),
2692 ],
2693 );
2694 let strip = loose_polygon(
2695 &mut model,
2696 [
2697 at(1.0, 0.0),
2698 at(1.0, 0.05),
2699 at(0.5, 0.05),
2700 at(0.0, 0.05),
2701 at(0.0, 0.0),
2702 at(0.5, 0.0),
2703 ],
2704 );
2705 let above = loose_polygon(
2706 &mut model,
2707 [
2708 at(1.0, 0.05),
2709 at(1.0, 1.05),
2710 at(0.0, 1.05),
2711 at(0.0, 0.05),
2712 at(0.5, 0.05),
2713 ],
2714 );
2715 let beside = loose_polygon(
2716 &mut model,
2717 [
2718 at(2.0, -1.0),
2719 at(2.0, 1.05),
2720 at(1.0, 1.05),
2721 at(1.0, 0.05),
2722 at(1.0, 0.0),
2723 at(1.0, -1.0),
2724 ],
2725 );
2726 let sewn = sew_within(&mut model, &[below, strip, above, beside], 0.1, T).unwrap();
2727 assert_eq!(sewn.shells.len(), 1);
2728 let shell = &sewn.shells[0];
2729 let faces = explore_unique(&model, shell, ShapeType::Face).unwrap();
2730 assert_eq!(faces.len(), 3);
2731 for face in &faces {
2732 for edge in explore_unique(&model, face, ShapeType::Edge).unwrap() {
2733 let (a, b) = crate::edge_vertices(&model, &edge).unwrap().unwrap();
2734 assert!(!a.is_same(&b), "an edge shrunk to a point is walked");
2735 }
2736 }
2737 let d = crate::check(&model, shell, T).unwrap();
2738 assert!(d.is_usable(), "{d}");
2739 }
2740
2741 #[test]
2746 fn an_edge_sewn_across_a_bow_widens_its_vertices_with_it() {
2747 let at = |x: f64, y: f64| Point::new(x, y, 0.0);
2748 let mut model = Model::new();
2749 let circle =
2750 ogeom_math::Circle::through(at(1.0, 0.0), at(0.5, 0.05), at(0.0, 0.0), T).unwrap();
2751 let param = |p: Point| ogeom_math::elementary::circle_parameter(&circle, p, T).unwrap();
2752 let (from, middle) = (param(at(1.0, 0.0)), param(at(0.5, 0.05)));
2753 let to = param(at(0.0, 0.0));
2754 let span = |a: f64, b: f64| (b - a).rem_euclid(core::f64::consts::TAU);
2755 let (lo, hi) = if span(from, middle) < span(from, to) {
2756 (from, from + span(from, to))
2757 } else {
2758 (to, to + span(to, from))
2759 };
2760 let corners = [at(0.0, -1.0), at(1.0, -1.0), at(1.0, 0.0), at(0.0, 0.0)];
2761 let vertices: Vec<Shape> = corners
2762 .iter()
2763 .map(|&p| crate::make_vertex(&mut model, p).shape)
2764 .collect();
2765 let mut edges = Vec::new();
2766 for k in [0, 1, 3] {
2767 let (a, b) = (corners[k], corners[(k + 1) % 4]);
2768 let line = ogeom_geom::LineCurve::segment(a, b, T).unwrap();
2769 edges.push(
2770 crate::make_edge_between(
2771 &mut model,
2772 line.into(),
2773 (0.0, a.distance(b)),
2774 &vertices[k],
2775 &vertices[(k + 1) % 4],
2776 T,
2777 )
2778 .unwrap()
2779 .shape,
2780 );
2781 }
2782 let (start, end) = if lo == from { (2, 3) } else { (3, 2) };
2783 let arc = crate::make_edge_between(
2784 &mut model,
2785 ogeom_geom::CircleCurve::new(circle).into(),
2786 (lo, hi),
2787 &vertices[start],
2788 &vertices[end],
2789 T,
2790 )
2791 .unwrap()
2792 .shape;
2793 edges.insert(2, if start == 2 { arc } else { arc.reversed() });
2794 let plane = || PlaneSurface::new(Plane::new(Frame::WORLD)).into();
2795 let below = crate::make_face_with_pcurves(&mut model, plane(), &[edges], T)
2796 .unwrap()
2797 .shape;
2798 let wire = make_polygon(
2799 &mut model,
2800 &[at(0.0, 0.0), at(1.0, 0.0), at(1.0, 1.0), at(0.0, 1.0)],
2801 true,
2802 T,
2803 )
2804 .unwrap()
2805 .shape;
2806 let ring = model.ordered_children_of(&wire).unwrap();
2807 let above = crate::make_face_with_pcurves(&mut model, plane(), &[ring], T)
2808 .unwrap()
2809 .shape;
2810 let sewn = sew_within(&mut model, &[below, above], 0.1, T).unwrap();
2811 assert_eq!((sewn.shells.len(), sewn.joined), (1, 1));
2812 let d = crate::check(&model, &sewn.shells[0], T).unwrap();
2813 assert!(d.is_usable(), "{d}");
2814 }
2815
2816 #[test]
2819 fn a_long_edge_against_two_short_ones_is_split_and_sewn() {
2820 let at = |x: f64, y: f64| Point::new(x, y, 0.0);
2821 let mut model = Model::new();
2822 let long = loose_polygon(
2823 &mut model,
2824 [at(0.0, 0.0), at(10.0, 0.0), at(10.0, 10.0), at(0.0, 10.0)],
2825 );
2826 let left = loose_polygon(
2827 &mut model,
2828 [at(0.0, -5.0), at(5.0, -5.0), at(5.0, 0.0), at(0.0, 0.0)],
2829 );
2830 let right = loose_polygon(
2831 &mut model,
2832 [at(5.0, -5.0), at(10.0, -5.0), at(10.0, 0.0), at(5.0, 0.0)],
2833 );
2834 let sewn = sew_within(&mut model, &[long, left, right], 1e-3, T).unwrap();
2835 assert_eq!(sewn.shells.len(), 1);
2836 assert_eq!(sewn.joined, 3);
2837 let d = crate::check(&model, &sewn.shells[0], T).unwrap();
2838 assert!(d.is_usable(), "{d}");
2839 }
2840
2841 #[test]
2846 fn an_arc_is_split_at_a_vertex_on_its_bulge() {
2847 use ogeom_geom::{CircleCurve, Curve, LineCurve, SurfaceGeometry};
2848 let mut model = Model::new();
2849 let turn = 130_f64.to_radians();
2850 let circle = |r: f64| -> Curve {
2851 CircleCurve::new(ogeom_math::Circle::new(Frame::WORLD, r, T).unwrap()).into()
2852 };
2853 let on = |r: f64, a: f64| Point::new(r * a.cos(), r * a.sin(), 0.0);
2854 let ring = |model: &mut Model, corners: &[(f64, f64)]| -> Vec<Shape> {
2858 let vertices: Vec<Shape> = corners
2859 .iter()
2860 .map(|&(r, a)| crate::make_vertex(model, on(r, a)).shape)
2861 .collect();
2862 let n = corners.len();
2863 (0..n)
2864 .map(|k| {
2865 let ((r, a), (_, b)) = (corners[k], corners[(k + 1) % n]);
2866 let (from, to) = (&vertices[k], &vertices[(k + 1) % n]);
2867 if (corners[(k + 1) % n].0 - r).abs() > 0.0 {
2868 let curve =
2869 LineCurve::segment(on(r, a), on(corners[(k + 1) % n].0, b), T).unwrap();
2870 let range = ogeom_geom::Curve3d::domain(&curve);
2871 crate::make_edge_between(model, Curve::Line(curve), range, from, to, T)
2872 .unwrap()
2873 .shape
2874 } else if a < b {
2875 crate::make_edge_between(model, circle(r), (a, b), from, to, T)
2876 .unwrap()
2877 .shape
2878 } else {
2879 crate::make_edge_between(model, circle(r), (b, a), to, from, T)
2880 .unwrap()
2881 .shape
2882 .reversed()
2883 }
2884 })
2885 .collect()
2886 };
2887 let plane: SurfaceGeometry = PlaneSurface::new(Plane::new(Frame::WORLD)).into();
2888 let quarter = core::f64::consts::FRAC_PI_2;
2889 let segment = {
2891 let a = crate::make_vertex(&mut model, on(10.0, 0.0)).shape;
2892 let b = crate::make_vertex(&mut model, on(10.0, turn)).shape;
2893 let chord = LineCurve::segment(on(10.0, turn), on(10.0, 0.0), T).unwrap();
2894 let range = ogeom_geom::Curve3d::domain(&chord);
2895 vec![
2896 crate::make_edge_between(&mut model, circle(10.0), (0.0, turn), &a, &b, T)
2897 .unwrap()
2898 .shape,
2899 crate::make_edge_between(&mut model, Curve::Line(chord), range, &b, &a, T)
2900 .unwrap()
2901 .shape,
2902 ]
2903 };
2904 let first = ring(
2905 &mut model,
2906 &[(10.0, 0.0), (12.0, 0.0), (12.0, quarter), (10.0, quarter)],
2907 );
2908 let second = ring(
2909 &mut model,
2910 &[(10.0, quarter), (12.0, quarter), (12.0, turn), (10.0, turn)],
2911 );
2912 let faces: Vec<Shape> = [segment, first, second]
2913 .into_iter()
2914 .map(|ring| {
2915 crate::make_face_with_pcurves(&mut model, plane.clone(), &[ring], T)
2916 .unwrap()
2917 .shape
2918 })
2919 .collect();
2920 let sewn = sew_within(&mut model, &faces, 1e-6, T).unwrap();
2921 assert_eq!(sewn.shells.len(), 1);
2922 assert_eq!(sewn.joined, 3);
2925 }
2926
2927 #[test]
2931 fn a_prism_s_walls_sew_to_lids_at_both_ends() {
2932 let mut model = Model::new();
2933 let at = |x: f64, y: f64, z: f64| Point::new(x, y, z);
2934 let profile = make_polygon(
2935 &mut model,
2936 &[
2937 at(0.0, 0.0, 0.0),
2938 at(10.0, 0.0, 0.0),
2939 at(10.0, 10.0, 0.0),
2940 at(0.0, 10.0, 0.0),
2941 ],
2942 true,
2943 T,
2944 )
2945 .unwrap()
2946 .shape;
2947 let walls = crate::make_prism(&mut model, &profile, Vector::new(0.0, 0.0, 5.0), T)
2948 .unwrap()
2949 .shape;
2950 let mut faces = explore_unique(&model, &walls, ShapeType::Face).unwrap();
2951 faces.push(loose_polygon(
2952 &mut model,
2953 [
2954 at(0.0, 0.0, 0.0),
2955 at(0.0, 10.0, 0.0),
2956 at(10.0, 10.0, 0.0),
2957 at(10.0, 0.0, 0.0),
2958 ],
2959 ));
2960 faces.push(loose_polygon(
2961 &mut model,
2962 [
2963 at(0.0, 0.0, 5.0),
2964 at(10.0, 0.0, 5.0),
2965 at(10.0, 10.0, 5.0),
2966 at(0.0, 10.0, 5.0),
2967 ],
2968 ));
2969 let sewn = sew(&mut model, &faces, T).unwrap();
2970 assert_eq!(sewn.shells.len(), 1);
2971 assert_eq!(sewn.joined, 8);
2972 assert!(sewn.free_edges.is_empty());
2973 assert!(is_shell_closed(&model, &sewn.shells[0]).unwrap());
2974 let solid = crate::make_solid(&mut model, &sewn.shells).unwrap().shape;
2975 let volume = crate::volume_properties(&model, &solid, fine(), T)
2976 .unwrap()
2977 .mass;
2978 assert!((volume.abs() - 500.0).abs() < 1e-6, "{volume}");
2979 }
2980
2981 fn box_faces(model: &mut Model, walls_in: bool, bottom_in: bool) -> Vec<Shape> {
2986 box_faces_in(model, walls_in, bottom_in, Frame::WORLD)
2987 }
2988
2989 fn box_faces_in(
2991 model: &mut Model,
2992 walls_in: bool,
2993 bottom_in: bool,
2994 frame: Frame,
2995 ) -> Vec<Shape> {
2996 let at = |x: f64, y: f64, z: f64| {
2997 frame.origin()
2998 + frame.x().vector() * x
2999 + frame.y().vector() * y
3000 + frame.z().vector() * z
3001 };
3002 let square = |z: f64| {
3003 [
3004 at(0.0, 0.0, z),
3005 at(10.0, 0.0, z),
3006 at(10.0, 10.0, z),
3007 at(0.0, 10.0, z),
3008 ]
3009 };
3010 let profile = make_polygon(model, &square(0.0), true, T).unwrap().shape;
3011 let walls = crate::make_prism(model, &profile, frame.z().vector() * 5.0, T)
3012 .unwrap()
3013 .shape;
3014 let mut faces: Vec<Shape> = explore_unique(model, &walls, ShapeType::Face)
3015 .unwrap()
3016 .into_iter()
3017 .map(|f| if walls_in { f.reversed() } else { f })
3018 .collect();
3019 let mut bottom = square(0.0);
3020 if !bottom_in {
3021 bottom.reverse();
3022 }
3023 faces.push(loose_polygon(model, bottom));
3024 faces.push(loose_polygon(model, square(5.0)));
3025 faces
3026 }
3027
3028 fn solid_volume(model: &mut Model, shell: &Shape) -> f64 {
3031 let solid = crate::make_solid(model, std::slice::from_ref(shell))
3032 .unwrap()
3033 .shape;
3034 let d = crate::check(model, &solid, T).unwrap();
3035 assert!(d.is_valid(), "{d}");
3036 assert!(
3037 crate::inside_out_faces(model, &solid, T)
3038 .unwrap()
3039 .is_empty()
3040 );
3041 crate::volume_properties(model, &solid, ogeom_mesh::Deflection::default(), T)
3042 .unwrap()
3043 .mass
3044 }
3045
3046 #[test]
3050 fn a_lid_facing_into_the_box_is_turned_over_when_sewn() {
3051 let mut model = Model::new();
3052 let faces = box_faces(&mut model, false, true);
3053 let sewn = sew(&mut model, &faces, T).unwrap();
3054 assert!(sewn.free_edges.is_empty());
3055 assert_eq!(
3056 crate::check::edges_walked_one_way(&model, &sewn.shells[0]),
3057 0
3058 );
3059 let image = sewn.history.modified(&faces[4]);
3060 assert_eq!(image.len(), 1);
3061 assert_ne!(image[0].orientation(), faces[4].orientation());
3062 let volume = solid_volume(&mut model, &sewn.shells[0]);
3063 assert!((volume - 500.0).abs() < 1e-9, "{volume}");
3064 }
3065
3066 #[test]
3070 fn a_closed_shell_whose_faces_disagree_is_turned_to_face_out() {
3071 let mut model = Model::new();
3072 let faces = box_faces(&mut model, true, false);
3073 let sewn = sew(&mut model, &faces, T).unwrap();
3074 for face in &faces[..4] {
3075 assert_ne!(
3076 sewn.history.modified(face)[0].orientation(),
3077 face.orientation()
3078 );
3079 }
3080 for face in &faces[4..] {
3081 assert!(sewn.history.copy_of(face).is_some());
3082 }
3083 let volume = solid_volume(&mut model, &sewn.shells[0]);
3084 assert!((volume - 500.0).abs() < 1e-9, "{volume}");
3085 }
3086
3087 #[test]
3092 fn a_closed_shell_far_from_the_origin_whose_faces_disagree_is_turned_to_face_out() {
3093 for o in [0.0, 1e6, 1e7, 2e7] {
3094 let mut model = Model::new();
3095 let n = ogeom_math::Direction::new(Vector::new(1.0, 1.0, 1.0), T).unwrap();
3096 let frame = Frame::new(Point::new(o, -o, o), n, ogeom_math::Direction::X, T).unwrap();
3097 let faces = box_faces_in(&mut model, true, false, frame);
3098 let sewn = sew(&mut model, &faces, T).unwrap();
3099 for face in &faces[..4] {
3100 assert_ne!(
3101 sewn.history.modified(face)[0].orientation(),
3102 face.orientation(),
3103 "o {o:e}: a wall facing in is turned"
3104 );
3105 }
3106 let mesh = ogeom_mesh::triangulate(
3107 &model,
3108 &sewn.shells[0],
3109 ogeom_mesh::Deflection::default(),
3110 T,
3111 )
3112 .unwrap();
3113 let volume = mesh.volume();
3114 assert!((volume - 500.0).abs() < 1e-6, "o {o:e}: {volume}");
3115 }
3116 }
3117
3118 #[test]
3122 fn a_closed_shell_facing_in_throughout_makes_a_solid_facing_out() {
3123 let mut model = Model::new();
3124 let mut faces = box_faces(&mut model, true, true);
3125 faces[5] = faces[5].reversed();
3126 let sewn = sew(&mut model, &faces, T).unwrap();
3127 assert!(sewn.free_edges.is_empty());
3128 for face in &faces {
3129 let image = sewn
3130 .history
3131 .copy_of(face)
3132 .cloned()
3133 .unwrap_or_else(|| sewn.history.modified(face)[0].clone());
3134 assert_eq!(image.orientation(), face.orientation());
3135 }
3136 let volume = solid_volume(&mut model, &sewn.shells[0]);
3137 assert!((volume - 500.0).abs() < 1e-9, "{volume}");
3138 }
3139
3140 #[test]
3144 fn a_sheet_turns_the_one_face_walking_against_its_neighbours() {
3145 let mut model = Model::new();
3146 let at = |x: f64, y: f64| Point::new(x, y, 0.0);
3147 let square = |x: f64| [at(x, 0.0), at(x + 1.0, 0.0), at(x + 1.0, 1.0), at(x, 1.0)];
3148 let mut middle = square(1.0);
3149 middle.reverse();
3150 let faces = vec![
3151 loose_polygon(&mut model, square(0.0)),
3152 loose_polygon(&mut model, middle),
3153 loose_polygon(&mut model, square(2.0)),
3154 ];
3155 let sewn = sew(&mut model, &faces, T).unwrap();
3156 assert_eq!(sewn.shells.len(), 1);
3157 assert_eq!(sewn.joined, 2);
3158 assert!(sewn.edges_walked_one_way(&model).unwrap().is_empty());
3159 let image = sewn.history.modified(&faces[1]);
3160 assert_eq!(image.len(), 1);
3161 assert_ne!(image[0].orientation(), faces[1].orientation());
3162 for face in [&faces[0], &faces[2]] {
3163 assert!(sewn.history.copy_of(face).is_some());
3164 }
3165 }
3166
3167 #[test]
3172 fn a_half_twisted_band_is_kept_and_named() {
3173 let mut model = Model::new();
3174 let (radius, half_width, steps) = (5.0, 1.0, 6_u32);
3175 let at = |k: u32, side: f64| {
3176 let u = std::f64::consts::TAU * f64::from(k) / f64::from(steps);
3177 let (radial, up) = ((u / 2.0).cos(), (u / 2.0).sin());
3178 Point::new(
3179 (radius + side * half_width * radial) * u.cos(),
3180 (radius + side * half_width * radial) * u.sin(),
3181 side * half_width * up,
3182 )
3183 };
3184 let mut faces = Vec::new();
3185 for k in 0..steps {
3186 faces.push(loose_polygon(
3187 &mut model,
3188 [at(k, -1.0), at(k + 1, -1.0), at(k, 1.0)],
3189 ));
3190 faces.push(loose_polygon(
3191 &mut model,
3192 [at(k + 1, -1.0), at(k + 1, 1.0), at(k, 1.0)],
3193 ));
3194 }
3195 let sewn = sew(&mut model, &faces, T).unwrap();
3196 assert_eq!(sewn.shells.len(), 1);
3197 assert_eq!(sewn.joined, 12);
3199 assert_eq!(sewn.free_edges.len(), 12);
3200 for face in &faces {
3201 assert!(sewn.history.copy_of(face).is_some());
3202 }
3203 assert!(!sewn.edges_walked_one_way(&model).unwrap().is_empty());
3204 }
3205
3206 #[test]
3207 fn edges_in_any_order_come_back_as_a_path() {
3208 let mut model = Model::new();
3209 let corners = [
3210 Point::new(0.0, 0.0, 0.0),
3211 Point::new(1.0, 0.0, 0.0),
3212 Point::new(1.0, 1.0, 0.0),
3213 Point::new(0.0, 1.0, 0.0),
3214 ];
3215 let wire = make_polygon(&mut model, &corners, true, T).unwrap().shape;
3216 let mut edges = model.children_of(&wire).unwrap();
3217 edges.swap(0, 2);
3219 edges[1] = edges[1].reversed();
3220 edges[3] = edges[3].reversed();
3221
3222 let ordered = order_edges(&model, &edges, T).unwrap();
3223 assert_eq!(ordered.len(), 4);
3224 let rebuilt = make_wire(&mut model, &ordered, T).unwrap().shape;
3227 assert!(crate::is_wire_closed(&model, &rebuilt, T).unwrap());
3228 }
3229
3230 #[test]
3231 fn edges_that_do_not_form_one_path_are_refused() {
3232 let mut model = Model::new();
3233 let a = make_polygon(
3234 &mut model,
3235 &[Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
3236 false,
3237 T,
3238 )
3239 .unwrap()
3240 .shape;
3241 let b = make_polygon(
3242 &mut model,
3243 &[Point::new(5.0, 0.0, 0.0), Point::new(6.0, 0.0, 0.0)],
3244 false,
3245 T,
3246 )
3247 .unwrap()
3248 .shape;
3249 let mut edges = model.children_of(&a).unwrap();
3250 edges.extend(model.children_of(&b).unwrap());
3251
3252 let err = order_edges(&model, &edges, T).unwrap_err();
3253 assert!(
3254 err.to_string().contains("connected path"),
3255 "unexpected message: {err}"
3256 );
3257 assert!(order_edges(&model, &[], T).is_err());
3258 }
3259
3260 #[test]
3261 fn a_branching_network_is_refused_rather_than_arbitrarily_walked() {
3262 let mut model = Model::new();
3265 let hub = Point::ORIGIN;
3266 let mut edges = Vec::new();
3267 for tip in [
3268 Point::new(1.0, 0.0, 0.0),
3269 Point::new(0.0, 1.0, 0.0),
3270 Point::new(0.0, 0.0, 1.0),
3271 ] {
3272 let w = make_polygon(&mut model, &[hub, tip], false, T)
3273 .unwrap()
3274 .shape;
3275 edges.extend(model.children_of(&w).unwrap());
3276 }
3277 let err = order_edges(&model, &edges, T).unwrap_err();
3278 assert!(
3279 err.to_string().contains("branching"),
3280 "unexpected message: {err}"
3281 );
3282 }
3283
3284 #[test]
3285 fn two_faces_that_touch_are_sewn_into_one_shell() {
3286 let mut model = Model::new();
3287 let left = loose_polygon(
3288 &mut model,
3289 [
3290 Point::new(0.0, 0.0, 0.0),
3291 Point::new(1.0, 0.0, 0.0),
3292 Point::new(1.0, 1.0, 0.0),
3293 Point::new(0.0, 1.0, 0.0),
3294 ],
3295 );
3296 let right = loose_polygon(
3297 &mut model,
3298 [
3299 Point::new(1.0, 0.0, 0.0),
3300 Point::new(2.0, 0.0, 0.0),
3301 Point::new(2.0, 1.0, 0.0),
3302 Point::new(1.0, 1.0, 0.0),
3303 ],
3304 );
3305
3306 let before = explore_unique(&model, &left, ShapeType::Edge)
3308 .unwrap()
3309 .len()
3310 + explore_unique(&model, &right, ShapeType::Edge)
3311 .unwrap()
3312 .len();
3313 assert_eq!(before, 8);
3314
3315 let sewn = sew(&mut model, &[left.clone(), right.clone()], T).unwrap();
3316 assert_eq!(sewn.shells.len(), 1, "they touch, so they are one shell");
3317 assert_eq!(sewn.joined, 1, "one shared edge");
3318 assert_eq!(
3319 explore_unique(&model, &sewn.shells[0], ShapeType::Edge)
3320 .unwrap()
3321 .len(),
3322 7,
3323 "the shared edge is one edge now, not two"
3324 );
3325 assert_eq!(sewn.free_edges.len(), 6);
3328 assert!(!is_shell_closed(&model, &sewn.shells[0]).unwrap());
3329 assert!(sewn.history.is_affected(&left));
3330 }
3331
3332 #[test]
3337 fn a_reversed_face_rebuilt_onto_a_shared_edge_keeps_its_walk() {
3338 let at = |x: f64, y: f64| Point::new(x, y, 0.0);
3339 let mut model = Model::new();
3340 let left = loose_polygon(
3341 &mut model,
3342 [at(0.0, 0.0), at(1.0, 0.0), at(1.0, 1.0), at(0.0, 1.0)],
3343 );
3344 let right = loose_polygon(
3347 &mut model,
3348 [at(1.0, 0.0), at(1.0, 1.0), at(2.0, 1.0), at(2.0, 0.0)],
3349 )
3350 .reversed();
3351 let sewn = sew(&mut model, &[left, right], T).unwrap();
3352 assert_eq!(sewn.joined, 1, "one shared edge");
3353 let mut walks: FastMap<TShapeId, (usize, usize)> = FastMap::default();
3354 for face in ogeom_topo::explore(
3355 &model,
3356 &sewn.shells[0],
3357 ogeom_topo::Filter::OfType(ShapeType::Face),
3358 )
3359 .unwrap()
3360 {
3361 for edge in
3362 ogeom_topo::explore(&model, &face, ogeom_topo::Filter::OfType(ShapeType::Edge))
3363 .unwrap()
3364 {
3365 let entry = walks.entry(edge.node()).or_default();
3366 if edge.orientation() == Orientation::Forward {
3367 entry.0 += 1;
3368 } else {
3369 entry.1 += 1;
3370 }
3371 }
3372 }
3373 let shared: Vec<_> = walks.values().filter(|(f, r)| f + r == 2).collect();
3374 assert_eq!(shared, [&(1, 1)], "the shared edge, walked once each way");
3375 }
3376
3377 #[test]
3384 fn twin_edges_join_the_vertices_they_end_on() {
3385 let mut model = Model::new();
3386 let left = loose_polygon(
3387 &mut model,
3388 [
3389 Point::new(0.0, 0.0, 0.0),
3390 Point::new(1.0, 0.0, 0.0),
3391 Point::new(1.0, 1.0, 0.0),
3392 Point::new(0.0, 1.0, 0.0),
3393 ],
3394 );
3395 let right = loose_polygon(
3396 &mut model,
3397 [
3398 Point::new(1.0, -0.01, 0.0),
3399 Point::new(2.0, 0.0, 0.0),
3400 Point::new(2.0, 1.0, 0.0),
3401 Point::new(1.0, 1.0, 0.0),
3402 ],
3403 );
3404 for edge in explore_unique(&model, &right, ShapeType::Edge).unwrap() {
3405 let (a, b) = crate::edge_vertices(&model, &edge).unwrap().unwrap();
3406 let (pa, pb) = (placed(&model, &a).unwrap(), placed(&model, &b).unwrap());
3407 if (pa.x - 1.0).abs() < 1e-9
3408 && (pb.x - 1.0).abs() < 1e-9
3409 && let Some(node) = model.node_mut(&edge)
3410 && let NodeData::Edge(data) = node.data_mut()
3411 {
3412 data.tolerance = data.tolerance.widen_to(0.012);
3413 }
3414 }
3415 let sewn = sew(&mut model, &[left, right], T).unwrap();
3416 assert_eq!(sewn.joined, 1, "the loose edge is the left square's");
3417 assert_eq!(sewn.shells.len(), 1);
3418 assert_eq!(
3419 explore_unique(&model, &sewn.shells[0], ShapeType::Vertex)
3420 .unwrap()
3421 .len(),
3422 6,
3423 "the two corners at the bottom of the shared edge are one"
3424 );
3425 assert_eq!(sewn.free_edges.len(), 6);
3426 }
3427
3428 #[test]
3429 fn faces_that_do_not_meet_stay_in_separate_shells() {
3430 let mut model = Model::new();
3432 let here = loose_polygon(
3433 &mut model,
3434 [
3435 Point::new(0.0, 0.0, 0.0),
3436 Point::new(1.0, 0.0, 0.0),
3437 Point::new(1.0, 1.0, 0.0),
3438 Point::new(0.0, 1.0, 0.0),
3439 ],
3440 );
3441 let far = loose_polygon(
3442 &mut model,
3443 [
3444 Point::new(50.0, 0.0, 0.0),
3445 Point::new(51.0, 0.0, 0.0),
3446 Point::new(51.0, 1.0, 0.0),
3447 Point::new(50.0, 1.0, 0.0),
3448 ],
3449 );
3450 let sewn = sew(&mut model, &[here, far], T).unwrap();
3451 assert_eq!(sewn.shells.len(), 2);
3452 assert_eq!(sewn.joined, 0);
3453 assert_eq!(sewn.free_edges.len(), 8);
3454 }
3455
3456 #[test]
3457 fn a_boxs_faces_taken_apart_and_sewn_back_close_again() {
3458 let mut model = Model::new();
3463 let solid = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T)
3464 .unwrap()
3465 .shape;
3466 let faces = explore_unique(&model, &solid, ShapeType::Face).unwrap();
3467
3468 let sewn = sew(&mut model, &faces, T).unwrap();
3469 assert_eq!(sewn.shells.len(), 1);
3470 assert!(sewn.free_edges.is_empty(), "a box has no free edges");
3471 assert!(is_shell_closed(&model, &sewn.shells[0]).unwrap());
3472 assert!(
3473 check_tessellation(&model, &sewn.shells[0], fine(), T)
3474 .unwrap()
3475 .is_valid()
3476 );
3477 }
3478
3479 #[test]
3480 fn two_arcs_between_the_same_vertices_are_not_the_same_edge() {
3481 let mut model = Model::new();
3485 let circle = ogeom_math::Circle::new(Frame::WORLD, 1.0, T).unwrap();
3486 let upper = crate::make_edge(
3487 &mut model,
3488 ogeom_geom::CircleCurve::new(circle).into(),
3489 (0.0, std::f64::consts::PI),
3490 T,
3491 )
3492 .unwrap()
3493 .shape;
3494 let lower = crate::make_edge(
3495 &mut model,
3496 ogeom_geom::CircleCurve::new(circle).into(),
3497 (std::f64::consts::PI, std::f64::consts::TAU),
3498 T,
3499 )
3500 .unwrap()
3501 .shape;
3502
3503 let a = fingerprint(&model, &upper, T).unwrap().unwrap();
3504 let b = fingerprint(&model, &lower, T).unwrap().unwrap();
3505 assert!(
3506 a.same_as(&b, T).unwrap().is_none(),
3507 "two different arcs were called the same edge"
3508 );
3509 assert!(a.same_as(&a, T).unwrap() == Some(false));
3510 }
3511
3512 #[test]
3513 fn a_wide_vertex_at_one_end_does_not_join_edges_apart_at_the_other() {
3514 let mut model = Model::new();
3519 let shared = Point::new(0.0, 0.0, 0.0);
3520 let side = |model: &mut Model, to: Point| {
3521 crate::make_edge(
3522 model,
3523 ogeom_geom::LineCurve::segment(shared, to, T)
3524 .unwrap()
3525 .into(),
3526 (0.0, to.distance(shared)),
3527 T,
3528 )
3529 .unwrap()
3530 .shape
3531 };
3532 let a = side(&mut model, Point::new(6e-3, 0.0, 0.0));
3533 let b = side(&mut model, Point::new(6e-3, 2.7e-4, 0.0));
3534 for edge in [&a, &b] {
3535 let start = model.children_of(edge).unwrap()[0].clone();
3536 model
3537 .widen(&start, ogeom_core::Tolerance::new(8.8e-4).unwrap())
3538 .unwrap();
3539 }
3540 let pa = fingerprint(&model, &a, T).unwrap().unwrap();
3541 let pb = fingerprint(&model, &b, T).unwrap().unwrap();
3542 assert!(pa.same_as(&pb, T).unwrap().is_none());
3543 }
3544
3545 #[test]
3546 fn an_edge_found_the_other_way_round_is_reversed_rather_than_dropped() {
3547 let mut model = Model::new();
3548 let up = Point::new(0.0, 0.0, 1.0);
3549 let down = Point::new(0.0, 0.0, 0.0);
3550 let a = crate::make_edge(
3551 &mut model,
3552 ogeom_geom::LineCurve::segment(down, up, T).unwrap().into(),
3553 (0.0, 1.0),
3554 T,
3555 )
3556 .unwrap()
3557 .shape;
3558 let b = crate::make_edge(
3559 &mut model,
3560 ogeom_geom::LineCurve::segment(up, down, T).unwrap().into(),
3561 (0.0, 1.0),
3562 T,
3563 )
3564 .unwrap()
3565 .shape;
3566
3567 let pa = fingerprint(&model, &a, T).unwrap().unwrap();
3568 let pb = fingerprint(&model, &b, T).unwrap().unwrap();
3569 assert_eq!(
3570 pa.same_as(&pb, T).unwrap(),
3571 Some(true),
3572 "the same edge, running the other way"
3573 );
3574 }
3575
3576 #[test]
3577 fn flipped_merges_carry_their_pcurves_the_right_way_round() {
3578 let mut model = Model::new();
3593 let c = Point::new;
3594 let faces = [
3595 [c(0., 0., 0.), c(0., 1., 0.), c(1., 1., 0.), c(1., 0., 0.)],
3596 [c(0., 0., 1.), c(1., 0., 1.), c(1., 1., 1.), c(0., 1., 1.)],
3597 [c(0., 0., 0.), c(1., 0., 0.), c(1., 0., 1.), c(0., 0., 1.)],
3598 [c(0., 1., 0.), c(0., 1., 1.), c(1., 1., 1.), c(1., 1., 0.)],
3599 [c(0., 0., 0.), c(0., 0., 1.), c(0., 1., 1.), c(0., 1., 0.)],
3600 [c(1., 0., 0.), c(1., 1., 0.), c(1., 1., 1.), c(1., 0., 1.)],
3601 ];
3602 let built: Vec<Shape> = faces
3603 .iter()
3604 .map(|f| loose_polygon(&mut model, *f))
3605 .collect();
3606 let sewn = sew(&mut model, &built, T).unwrap();
3607 assert_eq!(sewn.joined, 12, "every edge pair merged");
3608 assert!(sewn.free_edges.is_empty());
3609 for face in ogeom_topo::explore(
3610 &model,
3611 &sewn.shells[0],
3612 ogeom_topo::Filter::OfType(ShapeType::Face),
3613 )
3614 .unwrap()
3615 {
3616 let mesh = ogeom_mesh::triangulate(&model, &face, fine(), T).unwrap();
3617 assert_eq!(
3618 mesh.triangles.len(),
3619 2,
3620 "a unit square triangulates into two triangles, whichever twin \
3621 survived and whichever way it runs"
3622 );
3623 }
3624 }
3625
3626 #[test]
3629 fn one_loose_edge_does_not_widen_every_search() {
3630 let mut model = Model::new();
3631 let mut catalogue = Vec::new();
3632 for k in 0..200 {
3633 let x = f64::from(k);
3634 let wire = make_polygon(
3635 &mut model,
3636 &[Point::new(x, 0.0, 0.0), Point::new(x + 0.5, 0.0, 0.0)],
3637 false,
3638 T,
3639 )
3640 .unwrap()
3641 .shape;
3642 let edge = model.children_of(&wire).unwrap()[0].clone();
3643 let print = fingerprint(&model, &edge, T).unwrap().unwrap();
3644 catalogue.push((edge.node(), print));
3645 }
3646 let loose = 150;
3647 catalogue[loose].1.width = 20.0;
3648 let bins = StartBins::new(&catalogue, T);
3649 assert!(bins.twin_candidates(&catalogue, 0).len() <= 3);
3650 assert!(bins.twin_candidates(&catalogue, 140).contains(&loose));
3652 let from_loose = bins.twin_candidates(&catalogue, loose);
3653 assert!((151..=170).all(|j| from_loose.contains(&j)));
3654 }
3655
3656 #[test]
3657 fn sewing_nothing_and_sewing_the_wrong_kind_are_refused() {
3658 let mut model = Model::new();
3659 assert!(sew(&mut model, &[], T).is_err());
3660 let vertex = model.add_point(Point::ORIGIN);
3661 assert!(sew(&mut model, &[vertex], T).is_err());
3662 }
3663
3664 #[test]
3672 fn a_pcurve_carried_from_an_open_twin_keeps_its_far_end() {
3673 use ogeom_geom::{BSpline2d, BSplineCurve, Curve2d as _};
3674 use ogeom_math::{KnotVector, Point2};
3675 let mut model = Model::new();
3676 let line = BSplineCurve::new(
3677 KnotVector::clamped_uniform(1, 2).unwrap(),
3678 vec![Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
3679 T,
3680 )
3681 .unwrap();
3682 let kept = crate::make_edge(&mut model, line.into(), (0.0, 1.0), T)
3683 .unwrap()
3684 .shape;
3685 let square = BSplineCurve::new(
3686 KnotVector::clamped_uniform(2, 3).unwrap(),
3687 vec![Point::ORIGIN, Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
3688 T,
3689 )
3690 .unwrap();
3691 let short = 1.0 - 1e-6;
3692 let dropped = crate::make_edge(&mut model, square.into(), (0.0, short), T)
3693 .unwrap()
3694 .shape;
3695 let frame = Frame::new(
3696 Point::ORIGIN,
3697 ogeom_math::Direction::Z,
3698 ogeom_math::Direction::X,
3699 T,
3700 )
3701 .unwrap();
3702 let surface = model
3703 .geometry_mut()
3704 .add_surface(PlaneSurface::new(Plane::new(frame)).into());
3705 let trace = BSpline2d::new(
3706 KnotVector::clamped_uniform(2, 3).unwrap(),
3707 vec![
3708 Point2::new(0.0, 0.0),
3709 Point2::new(0.0, 0.0),
3710 Point2::new(1.0, 0.0),
3711 ],
3712 T,
3713 )
3714 .unwrap();
3715 let curve = model.geometry_mut().add_pcurve(trace.into());
3716 let carried = [EdgeRepr::PCurve {
3717 curve,
3718 surface,
3719 location: Location::identity(),
3720 range: (0.0, short),
3721 }];
3722 let paced = repaced_carry(
3723 &mut model,
3724 &dropped,
3725 &kept,
3726 false,
3727 &carried,
3728 T.confusion(),
3729 T,
3730 )
3731 .unwrap()
3732 .unwrap()
3733 .0;
3734 let [EdgeRepr::PCurve { curve, range, .. }] = paced.as_slice() else {
3735 panic!("{paced:?}");
3736 };
3737 assert_eq!(*range, (0.0, 1.0));
3738 let pcurve = model.geometry().pcurve(*curve).unwrap();
3739 let gap = 1.0 - short * short;
3741 for k in 0..=64 {
3742 let t = f64::from(k) / 64.0;
3743 let uv = pcurve.point_at(t, T).unwrap();
3744 assert!(
3745 uv.distance(Point2::new(t, 0.0)) <= gap + T.confusion(),
3746 "at {t}: {uv:?}"
3747 );
3748 }
3749 }
3750
3751 fn strips_meeting_on(
3758 model: &mut Model,
3759 length: f64,
3760 bottom: (ogeom_geom::Curve, (f64, f64)),
3761 trace: ogeom_geom::PlanarCurve,
3762 ) -> ([Shape; 2], ogeom_topo::SurfaceId) {
3763 use ogeom_geom::{Line2d, LineCurve};
3764 use ogeom_math::Point2;
3765 let at = |x: f64, y: f64| Point::new(x, y, 0.0);
3766 let below = loose_polygon(
3767 model,
3768 [
3769 at(0.0, -1.0),
3770 at(length, -1.0),
3771 at(length, 0.0),
3772 at(0.0, 0.0),
3773 ],
3774 );
3775 let surface = model
3776 .geometry_mut()
3777 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3778 let corners = [at(0.0, 0.0), at(length, 0.0), at(length, 1.0), at(0.0, 1.0)];
3779 let vertices: Vec<Shape> = corners
3780 .iter()
3781 .map(|p| crate::make_vertex(model, *p).shape)
3782 .collect();
3783 let mut edges = vec![
3784 crate::make_edge_between(model, bottom.0, bottom.1, &vertices[0], &vertices[1], T)
3785 .unwrap()
3786 .shape,
3787 ];
3788 crate::attach_pcurve(
3789 model,
3790 &edges[0],
3791 trace,
3792 surface,
3793 Location::identity(),
3794 (0.0, 1.0),
3795 )
3796 .unwrap();
3797 for k in 1..4 {
3798 let (a, b) = (corners[k], corners[(k + 1) % 4]);
3799 let edge = crate::make_edge_between(
3800 model,
3801 LineCurve::segment(a, b, T).unwrap().into(),
3802 (0.0, a.distance(b)),
3803 &vertices[k],
3804 &vertices[(k + 1) % 4],
3805 T,
3806 )
3807 .unwrap()
3808 .shape;
3809 crate::attach_pcurve(
3810 model,
3811 &edge,
3812 Line2d::segment(Point2::new(a.x, a.y), Point2::new(b.x, b.y), T)
3813 .unwrap()
3814 .into(),
3815 surface,
3816 Location::identity(),
3817 (0.0, a.distance(b)),
3818 )
3819 .unwrap();
3820 edges.push(edge);
3821 }
3822 let wire = crate::make_wire(model, &edges, T).unwrap().shape;
3823 let above = crate::make_face_on(model, surface, std::slice::from_ref(&wire), T)
3824 .unwrap()
3825 .shape;
3826 ([below, above], surface)
3827 }
3828
3829 fn squared_pace(length: f64) -> (ogeom_geom::Curve, (f64, f64)) {
3831 use ogeom_geom::BSplineCurve;
3832 use ogeom_math::KnotVector;
3833 let curve = BSplineCurve::new(
3834 KnotVector::clamped_uniform(2, 3).unwrap(),
3835 vec![Point::ORIGIN, Point::ORIGIN, Point::new(length, 0.0, 0.0)],
3836 T,
3837 )
3838 .unwrap();
3839 (curve.into(), (0.0, 1.0))
3840 }
3841
3842 fn traced_back_to_its_start(length: f64) -> ogeom_geom::PlanarCurve {
3848 use ogeom_geom::BSpline2d;
3849 use ogeom_math::{KnotVector, Point2};
3850 const N: u32 = 64;
3851 let reach = 1.0 - 1e-3;
3852 let mut knots = vec![0.0, 0.0];
3853 let mut control = Vec::new();
3854 for i in 0..=N {
3855 let s = f64::from(i) / f64::from(N);
3856 if i > 0 {
3857 knots.push(s * reach);
3858 }
3859 control.push(Point2::new(length * s * s, 0.0));
3860 }
3861 knots.extend([1.0, 1.0]);
3862 control.push(Point2::new(0.0, 0.0));
3863 BSpline2d::new(KnotVector::new(knots, 1).unwrap(), control, T)
3864 .unwrap()
3865 .into()
3866 }
3867
3868 fn matched_miss(model: &Model, edge: &Shape, surface: ogeom_topo::SurfaceId) -> f64 {
3871 use ogeom_geom::{Curve2d as _, Surface as _};
3872 let data = model.node(edge).unwrap().data().as_edge().unwrap();
3873 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
3874 panic!("no curve");
3875 };
3876 let curve = model.geometry().curve(*curve).unwrap();
3877 let Some(EdgeRepr::PCurve {
3878 curve: pc,
3879 range: prange,
3880 ..
3881 }) = data.pcurve_for(surface, &Location::identity())
3882 else {
3883 panic!("no pcurve on the surface");
3884 };
3885 let pcurve = model.geometry().pcurve(*pc).unwrap();
3886 let chart = model.geometry().surface(surface).unwrap();
3887 let mut widest = 0.0_f64;
3888 for k in 0..=4096 {
3889 let f = f64::from(k) / 4096.0;
3890 let uv = pcurve
3891 .point_at(prange.0 + (prange.1 - prange.0) * f, T)
3892 .unwrap();
3893 let lifted = chart.point_at(uv.x, uv.y, T).unwrap();
3894 let on = curve
3895 .point_at(range.0 + (range.1 - range.0) * f, T)
3896 .unwrap();
3897 widest = widest.max(lifted.distance(on));
3898 }
3899 widest
3900 }
3901
3902 fn shared_edge(model: &Model, sewn: &Sewn) -> Shape {
3904 let mut uses: FastMap<TShapeId, (usize, Shape)> = FastMap::default();
3905 for shell in &sewn.shells {
3906 for face in explore_unique(model, shell, ShapeType::Face).unwrap() {
3907 for edge in explore_unique(model, &face, ShapeType::Edge).unwrap() {
3908 uses.entry(edge.node()).or_insert((0, edge.clone())).0 += 1;
3909 }
3910 }
3911 }
3912 let shared: Vec<Shape> = uses
3913 .into_values()
3914 .filter(|(n, _)| *n == 2)
3915 .map(|(_, e)| e)
3916 .collect();
3917 let [edge] = shared.as_slice() else {
3918 panic!("{} shared edges", shared.len());
3919 };
3920 edge.clone()
3921 }
3922
3923 #[test]
3929 fn a_twin_whose_carried_pcurve_ends_at_its_start_is_kept_apart() {
3930 let mut model = Model::new();
3931 let length = 2.0;
3932 let (strips, _) = strips_meeting_on(
3933 &mut model,
3934 length,
3935 squared_pace(length),
3936 traced_back_to_its_start(length),
3937 );
3938 let sewn = sew(&mut model, &strips, T).unwrap();
3939 assert_eq!(sewn.joined, 0);
3940 assert_eq!(sewn.shells.len(), 2);
3941 }
3942
3943 #[test]
3947 fn a_twin_whose_carried_pcurve_ends_at_its_start_states_its_miss() {
3948 let mut model = Model::new();
3949 let length = 0.5;
3950 let (strips, surface) = strips_meeting_on(
3951 &mut model,
3952 length,
3953 squared_pace(length),
3954 traced_back_to_its_start(length),
3955 );
3956 let sewn = sew(&mut model, &strips, T).unwrap();
3957 assert_eq!(sewn.joined, 1);
3958 let edge = shared_edge(&model, &sewn);
3959 let stated = model
3960 .node(&edge)
3961 .unwrap()
3962 .data()
3963 .as_edge()
3964 .unwrap()
3965 .tolerance
3966 .get();
3967 let miss = matched_miss(&model, &edge, surface);
3968 assert!(miss >= length, "{miss}");
3969 assert!(stated >= miss, "states {stated} against a miss of {miss}");
3970 }
3971
3972 #[test]
3976 fn a_twin_carried_at_its_own_pace_keeps_the_kept_edge_s_tolerance() {
3977 use ogeom_geom::BSpline2d;
3978 use ogeom_math::{KnotVector, Point2};
3979 let mut model = Model::new();
3980 let length = 2.0;
3981 let trace = BSpline2d::new(
3982 KnotVector::clamped_uniform(2, 3).unwrap(),
3983 vec![
3984 Point2::new(0.0, 0.0),
3985 Point2::new(0.0, 0.0),
3986 Point2::new(length, 0.0),
3987 ],
3988 T,
3989 )
3990 .unwrap();
3991 let (strips, surface) =
3992 strips_meeting_on(&mut model, length, squared_pace(length), trace.into());
3993 let before: Vec<f64> = explore_unique(&model, &strips[0], ShapeType::Edge)
3994 .unwrap()
3995 .iter()
3996 .map(|e| {
3997 model
3998 .node(e)
3999 .unwrap()
4000 .data()
4001 .as_edge()
4002 .unwrap()
4003 .tolerance
4004 .get()
4005 })
4006 .collect();
4007 let sewn = sew(&mut model, &strips, T).unwrap();
4008 assert_eq!(sewn.joined, 1);
4009 let edge = shared_edge(&model, &sewn);
4010 let stated = model
4011 .node(&edge)
4012 .unwrap()
4013 .data()
4014 .as_edge()
4015 .unwrap()
4016 .tolerance
4017 .get();
4018 assert!(
4019 before.iter().all(|b| *b == stated),
4020 "{before:?} against {stated}"
4021 );
4022 assert!(matched_miss(&model, &edge, surface) <= T.confusion());
4023 }
4024
4025 #[test]
4030 fn a_rise_in_a_carried_pcurve_between_samples_is_stated() {
4031 use ogeom_geom::{BSpline2d, LineCurve};
4032 use ogeom_math::{KnotVector, Point2};
4033 let mut model = Model::new();
4034 let length = 1.0;
4035 let (rise, step) = (1e-2, 1.0 / 32.0);
4036 let (from, top, to) = (10.2 * step, 10.25 * step, 10.3 * step);
4037 let trace = BSpline2d::new(
4038 KnotVector::new(vec![0.0, 0.0, from, top, to, 1.0, 1.0], 1).unwrap(),
4039 vec![
4040 Point2::new(0.0, 0.0),
4041 Point2::new(from * length, 0.0),
4042 Point2::new(top * length, rise),
4043 Point2::new(to * length, 0.0),
4044 Point2::new(length, 0.0),
4045 ],
4046 T,
4047 )
4048 .unwrap();
4049 let line = LineCurve::segment(Point::ORIGIN, Point::new(length, 0.0, 0.0), T).unwrap();
4050 let (strips, _) = strips_meeting_on(
4051 &mut model,
4052 length,
4053 (line.into(), (0.0, length)),
4054 trace.into(),
4055 );
4056 let sewn = sew(&mut model, &strips, T).unwrap();
4057 assert_eq!(sewn.joined, 1);
4058 let edge = shared_edge(&model, &sewn);
4059 let stated = model
4060 .node(&edge)
4061 .unwrap()
4062 .data()
4063 .as_edge()
4064 .unwrap()
4065 .tolerance
4066 .get();
4067 assert!(stated >= rise, "states {stated} against a rise of {rise}");
4068 assert!(
4069 stated <= rise * (1.0 + 1e-3),
4070 "states {stated} against a rise of {rise}"
4071 );
4072 }
4073
4074 #[test]
4075 fn sewing_does_not_move_anything() {
4076 let mut model = Model::new();
4081 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
4082 .unwrap()
4083 .shape;
4084 let faces = explore_unique(&model, &solid, ShapeType::Face).unwrap();
4085 let before: Vec<Point> = explore_unique(&model, &solid, ShapeType::Vertex)
4086 .unwrap()
4087 .iter()
4088 .map(|v| placed(&model, v).unwrap())
4089 .collect();
4090
4091 let sewn = sew(&mut model, &faces, T).unwrap();
4092 let after: Vec<Point> = explore_unique(&model, &sewn.shells[0], ShapeType::Vertex)
4093 .unwrap()
4094 .iter()
4095 .map(|v| placed(&model, v).unwrap())
4096 .collect();
4097 assert_eq!(before.len(), after.len());
4098 for p in &after {
4099 assert!(
4100 before.iter().any(|q| q.is_equal(*p, T)),
4101 "a vertex moved: {p:?}"
4102 );
4103 }
4104 let _ = Vector::ZERO;
4105 }
4106}