1use ogeom_core::FastMap;
22
23use ogeom_algo::{
24 Built, History, is_shell_closed, make_band_of_rings, make_edge_between, make_face_on,
25 make_shell, make_solid, make_vertex, make_wire,
26};
27use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
28use ogeom_geom::{Curve, Curve2d as _, Curve3d as _, Surface as _, SurfaceGeometry};
29use ogeom_math::{Axis2, Direction2, Point, Point2, Vector2};
30use ogeom_topo::{
31 EdgeData, EdgeRepr, Filter, Location, Model, NodeData, Shape, ShapeType, TShapeId, explore,
32 explore_unique,
33};
34
35use crate::reanchor::{attach_face_pcurves, circle_parameter};
36
37pub fn seam_periodic_faces(
55 model: &mut Model,
56 shape: &Shape,
57 tol: Tolerances,
58) -> OgeomResult<(Built, usize)> {
59 if model.kind_of(shape)? != ShapeType::Solid {
60 ogeom_bail!(Construction, "seaming heals solids");
61 }
62 let mut substitution: FastMap<TShapeId, Vec<Shape>> = FastMap::default();
63 let mut face_map: FastMap<TShapeId, Shape> = FastMap::default();
64 let mut settled: ogeom_core::FastSet<TShapeId> = ogeom_core::FastSet::default();
66 for face in explore_unique(model, shape, ShapeType::Face)? {
67 let Some((surface_id, surface)) = seamless_periodic(model, &face)? else {
68 continue;
69 };
70 let rings: Vec<Vec<Shape>> = model
73 .ordered_children_of(&face.oriented(ogeom_topo::Orientation::Forward))?
74 .iter()
75 .map(|w| model.ordered_children_of(w))
76 .collect::<OgeomResult<_>>()?;
77 let resolved: Vec<Vec<Shape>> = rings
83 .iter()
84 .map(|ring| {
85 ring.iter()
86 .flat_map(|e| match substitution.get(&e.node()) {
87 Some(new_edges) if e.orientation() == ogeom_topo::Orientation::Reversed => {
88 new_edges.iter().rev().map(Shape::reversed).collect()
89 }
90 Some(new_edges) => new_edges.clone(),
91 None => vec![e.clone()],
92 })
93 .collect()
94 })
95 .collect();
96 if resolved != rings {
97 let rebuilt = match &resolved[..] {
98 [ring] if lone_circle(model, ring).is_some() => {
99 cap(model, &face, &surface, ring, tol)
100 }
101 [ring] => meridian_poles(
102 model,
103 &face,
104 (surface_id, &surface),
105 ring,
106 &settled,
107 true,
108 tol,
109 )
110 .map(|done| done.and_then(|(rebuilt, more)| more.is_empty().then_some(rebuilt))),
111 _ => Ok(None),
112 };
113 if let Ok(Some(rebuilt)) = rebuilt {
114 settled.extend(
115 explore_unique(model, &rebuilt, ShapeType::Edge)?
116 .iter()
117 .map(Shape::node),
118 );
119 face_map.insert(face.node(), rebuilt);
120 }
121 continue;
122 }
123 if let [ring] = &rings[..]
124 && let Ok(Some((rebuilt, added))) = meridian_poles(
125 model,
126 &face,
127 (surface_id, &surface),
128 ring,
129 &settled,
130 false,
131 tol,
132 )
133 {
134 settled.extend(
135 explore_unique(model, &face, ShapeType::Edge)?
136 .iter()
137 .map(Shape::node),
138 );
139 substitution.extend(added);
140 face_map.insert(face.node(), rebuilt);
141 continue;
142 }
143 let walks: Vec<Option<Walk>> = rings
144 .iter()
145 .map(|ring| walk(model, surface_id, &surface, ring, tol))
146 .collect::<OgeomResult<_>>()?;
147 let repaired = match (rings.len(), &walks[..]) {
148 (2, [Some(a), Some(b)]) if a.wraps && b.wraps => band(
149 model,
150 &surface,
151 [&rings[0], &rings[1]],
152 [a, b],
153 (&mut substitution, &settled),
154 tol,
155 ),
156 (1, [Some(a)]) if a.wraps => match (0..rings[0].len()).find(|&i| a.passes_once_at(i)) {
157 Some(i) => {
158 let mut ring = rings[0].clone();
159 ring.rotate_left(i);
160 cap(model, &face, &surface, &ring, tol)
161 }
162 None => Ok(None),
163 },
164 (1, [Some(a)]) => pole_gap(model, surface_id, &surface, &rings[0], a, tol),
165 _ => Ok(None),
166 };
167 if let Ok(Some(rebuilt)) = repaired {
169 settled.extend(
170 explore_unique(model, &face, ShapeType::Edge)?
171 .iter()
172 .map(Shape::node),
173 );
174 face_map.insert(face.node(), rebuilt);
175 }
176 }
177 if face_map.is_empty() {
178 return Ok((Built::new(shape.clone(), History::new()), 0));
179 }
180 let repaired = face_map.len();
181
182 let mut history = History::new();
184 for face in explore(model, shape, Filter::OfType(ShapeType::Face))? {
185 if face_map.contains_key(&face.node()) {
186 continue;
187 }
188 let uses_any = explore_unique(model, &face, ShapeType::Edge)?
189 .iter()
190 .any(|e| substitution.contains_key(&e.node()));
191 if uses_any {
192 let rebuilt = rebuild_with(model, &face, &substitution, tol)?;
193 face_map.insert(face.node(), rebuilt);
194 }
195 }
196 for face in explore_unique(model, shape, ShapeType::Face)? {
197 if let Some(rebuilt) = face_map.get(&face.node()) {
198 let oriented = if face.orientation() == ogeom_topo::Orientation::Reversed {
199 rebuilt.reversed()
200 } else {
201 rebuilt.clone()
202 };
203 history.modify(&face, oriented);
204 }
205 }
206 let mut shells = Vec::new();
207 for shell in explore_unique(model, shape, ShapeType::Shell)? {
208 let faces: Vec<Shape> = explore(model, &shell, Filter::OfType(ShapeType::Face))?
209 .into_iter()
210 .map(|f| {
211 face_map.get(&f.node()).map_or(f.clone(), |n| {
212 if f.orientation() == ogeom_topo::Orientation::Reversed {
213 n.reversed()
214 } else {
215 n.clone()
216 }
217 })
218 })
219 .collect();
220 let rebuilt = make_shell(model, &faces)?.shape;
221 if !is_shell_closed(model, &rebuilt)? {
222 ogeom_bail!(
223 Construction,
224 "seaming left the shell open; the shape resists this repair"
225 );
226 }
227 history.modify(&shell, rebuilt.clone());
228 shells.push(rebuilt);
229 }
230 let solid = make_solid(model, &shells)?.shape;
231 history.modify(shape, solid.clone());
232 Ok((Built::new(solid, history), repaired))
233}
234
235fn seamless_periodic(
238 model: &Model,
239 face: &Shape,
240) -> OgeomResult<Option<(ogeom_topo::SurfaceId, SurfaceGeometry)>> {
241 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
242 return Ok(None);
243 };
244 let Some(surface) = model.geometry().surface(data.surface) else {
245 return Ok(None);
246 };
247 if !surface.is_periodic_u()
248 || surface.is_periodic_v()
249 || !matches!(
250 surface,
251 SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_) | SurfaceGeometry::Sphere(_)
252 )
253 {
254 return Ok(None);
255 }
256 for wire in model.ordered_children_of(face)? {
257 let edges = model.ordered_children_of(&wire)?;
258 for edge in &edges {
259 if edges.iter().filter(|e| e.node() == edge.node()).count() > 1 {
260 return Ok(None);
261 }
262 let seam = model
263 .node(edge)
264 .and_then(|n| n.data().as_edge())
265 .and_then(|d| d.pcurve_for(data.surface, edge.location()))
266 .is_some_and(|r| matches!(r, EdgeRepr::Seam { .. }));
267 if seam {
268 return Ok(None);
269 }
270 }
271 }
272 Ok(Some((data.surface, surface.clone())))
273}
274
275struct Walk {
278 samples: Vec<Vec<Point2>>,
280 wraps: bool,
282 turn: f64,
284 span: f64,
285 surface: ogeom_topo::SurfaceId,
287 shifts: Vec<f64>,
289}
290
291impl Walk {
292 fn start(&self, i: usize) -> Point2 {
294 self.samples[i][0]
295 }
296
297 fn passes_once_at(&self, i: usize) -> bool {
301 let from = self.start(i).x;
302 let n = self.samples.len();
303 let margin = 1e-6 * self.span;
304 for k in 0..n {
305 let occurrence = (i + k) % n;
306 let shift = if i + k >= n { self.turn } else { 0.0 };
307 for (j, p) in self.samples[occurrence].iter().enumerate() {
308 if k == 0 && j == 0 {
309 continue;
310 }
311 if k == n - 1 && j == self.samples[occurrence].len() - 1 {
312 continue;
313 }
314 let along = (p.x + shift - from) / self.turn;
315 if along * self.span.abs() <= margin || (1.0 - along) * self.span <= margin {
316 return false;
317 }
318 }
319 }
320 true
321 }
322}
323
324fn walk(
325 model: &Model,
326 surface_id: ogeom_topo::SurfaceId,
327 surface: &SurfaceGeometry,
328 ring: &[Shape],
329 tol: Tolerances,
330) -> OgeomResult<Option<Walk>> {
331 const SAMPLES: u32 = 16;
332 let ((ua, ub), _) = surface.domain();
333 let span = ub - ua;
334 let mut samples: Vec<Vec<Point2>> = Vec::with_capacity(ring.len());
335 let mut shifts = Vec::with_capacity(ring.len());
336 let mut at: Option<Point2> = None;
337 for occurrence in ring {
338 let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
339 return Ok(None);
340 };
341 let Some(EdgeRepr::PCurve { curve, range, .. }) =
342 data.pcurve_for(surface_id, occurrence.location())
343 else {
344 return Ok(None);
345 };
346 let Some(pcurve) = model.geometry().pcurve(*curve) else {
347 return Ok(None);
348 };
349 let reversed = occurrence.orientation() == ogeom_topo::Orientation::Reversed;
350 let mut piece = Vec::with_capacity(SAMPLES as usize + 1);
351 for k in 0..=SAMPLES {
352 let f = f64::from(k) / f64::from(SAMPLES);
353 let t = if reversed {
354 range.1 - (range.1 - range.0) * f
355 } else {
356 range.0 + (range.1 - range.0) * f
357 };
358 piece.push(pcurve.point_at(t, tol)?);
359 }
360 let shift = at.map_or(0.0, |at| ((at.x - piece[0].x) / span).round() * span);
361 for p in &mut piece {
362 p.x += shift;
363 }
364 shifts.push(shift);
365 at = piece.last().copied();
366 samples.push(piece);
367 }
368 let (Some(first), Some(last)) = (samples.first(), at) else {
369 return Ok(None);
370 };
371 let turn = last.x - first[0].x;
372 let wraps = (turn.abs() - span).abs() <= 1e-6 * span;
373 Ok(Some(Walk {
374 samples,
375 wraps,
376 turn,
377 span,
378 surface: surface_id,
379 shifts,
380 }))
381}
382
383fn lone_circle(model: &Model, ring: &[Shape]) -> Option<(Curve, (f64, f64))> {
385 let [edge] = ring else {
386 return None;
387 };
388 let data = model.node(edge).and_then(|n| n.data().as_edge())?;
389 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
390 return None;
391 };
392 let curve = model.geometry().curve(*curve)?;
393 matches!(curve, Curve::Circle(_)).then(|| (curve.clone(), *range))
394}
395
396fn band(
400 model: &mut Model,
401 surface: &SurfaceGeometry,
402 rings: [&[Shape]; 2],
403 walks: [&Walk; 2],
404 (substitution, settled): (
405 &mut FastMap<TShapeId, Vec<Shape>>,
406 &ogeom_core::FastSet<TShapeId>,
407 ),
408 tol: Tolerances,
409) -> OgeomResult<Option<Shape>> {
410 let span = walks[0].span;
411 if rings
413 .iter()
414 .flat_map(|r| r.iter())
415 .any(|e| settled.contains(&e.node()))
416 {
417 return Ok(None);
418 }
419 let mut columns: Vec<f64> = walks
422 .iter()
423 .flat_map(|w| (0..w.samples.len()).map(|i| w.start(i).x.rem_euclid(span)))
424 .collect();
425 columns.sort_by(f64::total_cmp);
426 columns.dedup_by(|a, b| (*a - *b).abs() <= 1e-9 * span);
427 let mut gaps: Vec<(f64, f64)> = (0..columns.len())
428 .map(|k| {
429 let (a, b) = (
430 columns[k],
431 columns.get(k + 1).copied().unwrap_or(columns[0] + span),
432 );
433 (b - a, f64::midpoint(a, b))
434 })
435 .collect();
436 gaps.sort_by(|x, y| y.0.total_cmp(&x.0));
437 for (_, column) in gaps {
438 let plans = [
439 plan(model, rings[0], walks[0], column, tol)?,
440 plan(model, rings[1], walks[1], column, tol)?,
441 ];
442 let [Some(first), Some(second)] = plans else {
443 continue;
444 };
445 let mut built = Vec::with_capacity(2);
446 let mut added = Vec::new();
447 for (ring, planned) in rings.iter().zip([first, second]) {
448 let (ring, replaced) =
449 apply(model, surface, walks[0].surface, ring, planned, column, tol)?;
450 built.push(ring);
451 added.extend(replaced);
452 }
453 match make_band_of_rings(model, surface, &built[0], &built[1], tol) {
454 Ok(face) => {
455 substitution.extend(added);
456 return Ok(Some(face));
457 }
458 Err(_) => continue,
459 }
460 }
461 Ok(None)
462}
463
464enum Plan {
466 Reanchor,
468 Split { k: usize, t: f64 },
470}
471
472fn plan(
475 model: &Model,
476 ring: &[Shape],
477 walk: &Walk,
478 column: f64,
479 tol: Tolerances,
480) -> OgeomResult<Option<Plan>> {
481 if lone_circle(model, ring).is_some() {
482 return Ok(Some(Plan::Reanchor));
483 }
484 let span = walk.span;
485 let level = |p: Point2| ((p.x - column) / span).floor();
486 let margin = 1e-6;
487 let mut crossing = None;
488 let mut crossings = 0.0;
489 for (k, piece) in walk.samples.iter().enumerate() {
490 for pair in piece.windows(2) {
491 let near = |p: Point2| {
492 let g = (p.x - column) / span;
493 (g - g.round()).abs() <= margin
494 };
495 if near(pair[0]) || near(pair[1]) {
496 return Ok(None);
497 }
498 let steps = (level(pair[1]) - level(pair[0])).abs();
499 if steps > 0.0 {
500 crossings += steps;
501 crossing = Some((k, pair[0], pair[1]));
502 }
503 }
504 }
505 let Some((k, _, _)) = crossing else {
506 return Ok(None);
507 };
508 if crossings != 1.0 {
509 return Ok(None);
510 }
511 let Some((pcurve, range, shift)) = stored_piece(model, walk, ring, k)? else {
513 return Ok(None);
514 };
515 let target = {
516 let at = pcurve.point_at(range.0, tol)?.x + shift;
517 column + ((at - column) / span).round() * span
518 };
519 let u = |t: f64| -> OgeomResult<f64> { Ok(pcurve.point_at(t, tol)?.x + shift) };
520 let (mut lo, mut hi) = range;
521 let (mut f_lo, f_hi) = (u(lo)? - target, u(hi)? - target);
522 if f_lo.signum() == f_hi.signum() {
523 return Ok(None);
524 }
525 for _ in 0..80 {
526 let mid = f64::midpoint(lo, hi);
527 let f_mid = u(mid)? - target;
528 if f_mid.signum() == f_lo.signum() {
529 lo = mid;
530 f_lo = f_mid;
531 } else {
532 hi = mid;
533 }
534 }
535 Ok(Some(Plan::Split {
536 k,
537 t: f64::midpoint(lo, hi),
538 }))
539}
540
541type StoredPiece = (ogeom_geom::PlanarCurve, (f64, f64), f64);
543
544fn stored_piece(
547 model: &Model,
548 walk: &Walk,
549 ring: &[Shape],
550 k: usize,
551) -> OgeomResult<Option<StoredPiece>> {
552 let occurrence = &ring[k];
553 let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
554 return Ok(None);
555 };
556 let surface = walk.surface;
557 let Some(EdgeRepr::PCurve { curve, range, .. }) =
558 data.pcurve_for(surface, occurrence.location())
559 else {
560 return Ok(None);
561 };
562 let Some(pcurve) = model.geometry().pcurve(*curve) else {
563 return Ok(None);
564 };
565 Ok(Some((pcurve.clone(), *range, walk.shifts[k])))
566}
567
568#[allow(clippy::type_complexity)]
572fn apply(
573 model: &mut Model,
574 surface: &SurfaceGeometry,
575 surface_id: ogeom_topo::SurfaceId,
576 ring: &[Shape],
577 planned: Plan,
578 column: f64,
579 tol: Tolerances,
580) -> OgeomResult<(Vec<Shape>, Vec<(TShapeId, Vec<Shape>)>)> {
581 let edge_curve = |model: &Model, edge: &Shape| -> OgeomResult<(Curve, (f64, f64))> {
582 let Some(EdgeRepr::Curve3d { curve, range, .. }) = model
583 .node(edge)
584 .and_then(|n| n.data().as_edge())
585 .and_then(|d| d.curve3d())
586 else {
587 ogeom_bail!(Construction, "a ring edge has no curve");
588 };
589 let Some(geometry) = model.geometry().curve(*curve) else {
590 ogeom_bail!(Dangling, "curve is not in this model");
591 };
592 Ok((geometry.clone(), *range))
593 };
594 let reversed = |e: &Shape| e.orientation() == ogeom_topo::Orientation::Reversed;
595 match planned {
596 Plan::Reanchor => {
597 let old = &ring[0];
598 let (curve, range) = edge_curve(model, old)?;
599 let row = {
600 let Some((a, _)) = ogeom_algo::edge_vertices(model, old)? else {
601 ogeom_bail!(Construction, "a ring has no vertex");
602 };
603 let Some(data) = model.node(&a).and_then(|n| n.data().as_vertex()) else {
604 ogeom_bail!(Dangling, "vertex is not in this model");
605 };
606 ogeom_algo::measure::project_on_surface(surface, data.point, 32, tol)?
607 .parameters
608 .1
609 };
610 let target = surface.point_at(column, row, tol)?;
611 let Some(at) = circle_parameter(&curve, target) else {
612 ogeom_bail!(Construction, "an anchor point fell off its circle");
613 };
614 let period = range.1 - range.0;
615 let vertex = make_vertex(model, target).shape;
616 let edge =
617 make_edge_between(model, curve, (at, at + period), &vertex, &vertex, tol)?.shape;
618 let placed = if reversed(old) {
619 edge.reversed()
620 } else {
621 edge.clone()
622 };
623 Ok((vec![placed], vec![(old.node(), vec![edge])]))
624 }
625 Plan::Split { k, t } => {
626 let old = &ring[k];
627 let (curve, range) = edge_curve(model, old)?;
628 let Some(EdgeRepr::PCurve { range: prange, .. }) = model
631 .node(old)
632 .and_then(|n| n.data().as_edge())
633 .and_then(|d| d.pcurve_for(surface_id, old.location()))
634 .cloned()
635 else {
636 ogeom_bail!(Construction, "a ring edge has no pcurve");
637 };
638 let at = range.0 + (t - prange.0) / (prange.1 - prange.0) * (range.1 - range.0);
639 let bounds = model.children_of(old)?;
640 let (Some(first), Some(last)) = (bounds.first(), bounds.last()) else {
641 ogeom_bail!(Construction, "a ring edge has no vertices");
642 };
643 let point = curve.point_at(at, tol)?;
644 let vertex = make_vertex(model, point).shape;
645 let head =
646 make_edge_between(model, curve.clone(), (range.0, at), first, &vertex, tol)?.shape;
647 let tail = make_edge_between(model, curve, (at, range.1), &vertex, last, tol)?.shape;
648 let halves = if reversed(old) {
651 [tail.reversed(), head.reversed()]
652 } else {
653 [head.clone(), tail.clone()]
654 };
655 let mut out: Vec<Shape> = Vec::with_capacity(ring.len() + 1);
656 out.push(halves[1].clone());
657 out.extend(ring[k + 1..].iter().cloned());
658 out.extend(ring[..k].iter().cloned());
659 out.push(halves[0].clone());
660 Ok((out, vec![(old.node(), vec![head, tail])]))
661 }
662 }
663}
664
665fn cap(
669 model: &mut Model,
670 face: &Shape,
671 surface: &SurfaceGeometry,
672 ring: &[Shape],
673 tol: Tolerances,
674) -> OgeomResult<Option<Shape>> {
675 let SurfaceGeometry::Sphere(_) = surface else {
676 return Ok(None);
677 };
678 let half = core::f64::consts::FRAC_PI_2;
679 let mesh = ogeom_mesh::triangulate(model, face, ogeom_mesh::Deflection::default(), tol)?;
683 let reach = |pole: Point| {
684 mesh.positions
685 .iter()
686 .map(|p| p.distance(pole))
687 .fold(f64::INFINITY, f64::min)
688 };
689 let poles = [
690 surface.point_at(0.0, half, tol)?,
691 surface.point_at(0.0, -half, tol)?,
692 ];
693 let pole = if mesh.positions.is_empty() {
694 None
695 } else if reach(poles[0]) < reach(poles[1]) {
696 Some(poles[0])
697 } else {
698 Some(poles[1])
699 };
700 let Some(pole) = pole else {
701 return Ok(None);
702 };
703 let apex = make_vertex(model, pole).shape;
704 let mut data = EdgeData::new();
705 data.degenerate = true;
706 let degenerate = model.add_edge(data, &[apex.clone(), apex])?;
707 Ok(Some(make_band_of_rings(
708 model,
709 surface,
710 ring,
711 &[degenerate],
712 tol,
713 )?))
714}
715
716fn pole_gap(
719 model: &mut Model,
720 surface_id: ogeom_topo::SurfaceId,
721 surface: &SurfaceGeometry,
722 ring: &[Shape],
723 walk: &Walk,
724 tol: Tolerances,
725) -> OgeomResult<Option<Shape>> {
726 let SurfaceGeometry::Sphere(_) = surface else {
727 return Ok(None);
728 };
729 let half = core::f64::consts::FRAC_PI_2;
730 let n = ring.len();
731 let mut edges = Vec::with_capacity(n + 1);
732 let mut inserted = false;
733 for (k, occurrence) in ring.iter().enumerate() {
734 edges.push(occurrence.clone());
735 let end = *walk.samples[k].last().unwrap_or(&walk.samples[k][0]);
736 let next = walk.samples[(k + 1) % n][0];
737 let gap = next.x - end.x;
741 if (end.y.abs() - half).abs() > 1e-9 || gap.abs() <= 1e-9 * walk.span {
742 continue;
743 }
744 let Some((_, vertex)) = ogeom_algo::edge_vertices(model, occurrence)? else {
745 return Ok(None);
746 };
747 let mut data = EdgeData::new();
748 data.degenerate = true;
749 let degenerate = model.add_edge(data, &[vertex.clone(), vertex])?;
750 let stored_end = stored_end(model, surface_id, occurrence, tol)?;
752 let Some(stored_end) = stored_end else {
753 return Ok(None);
754 };
755 let pcurve: ogeom_geom::PlanarCurve = ogeom_geom::Line2d::over(
756 Axis2::new(
757 stored_end,
758 Direction2::new(Vector2::new(gap.signum(), 0.0), tol)?,
759 ),
760 0.0,
761 gap.abs(),
762 )?
763 .into();
764 ogeom_algo::attach_pcurve(
765 model,
766 °enerate,
767 pcurve,
768 surface_id,
769 Location::identity(),
770 (0.0, gap.abs()),
771 )?;
772 edges.push(degenerate);
773 inserted = true;
774 }
775 if !inserted {
776 return Ok(None);
777 }
778 let mut area = 0.0;
782 let stored: Vec<Point2> = walk
783 .samples
784 .iter()
785 .zip(&walk.shifts)
786 .flat_map(|(piece, shift)| piece.iter().map(move |p| Point2::new(p.x - shift, p.y)))
787 .collect();
788 for (k, p) in stored.iter().enumerate() {
789 let q = stored[(k + 1) % stored.len()];
790 area += p.x * q.y - q.x * p.y;
791 }
792 let wire = make_wire(model, &edges, tol)?.shape;
793 let face = make_face_on(model, surface_id, &[wire], tol)?.shape;
794 Ok(Some(if area < 0.0 { face.reversed() } else { face }))
795}
796
797type Halved = (Shape, Vec<(TShapeId, Vec<Shape>)>);
800
801fn meridian_poles(
815 model: &mut Model,
816 face: &Shape,
817 (surface_id, surface): (ogeom_topo::SurfaceId, &SurfaceGeometry),
818 ring: &[Shape],
819 settled: &ogeom_core::FastSet<TShapeId>,
820 resolved: bool,
821 tol: Tolerances,
822) -> OgeomResult<Option<Halved>> {
823 let SurfaceGeometry::Sphere(sphere) = surface else {
824 return Ok(None);
825 };
826 if !face.location().is_identity() {
827 return Ok(None);
828 }
829 let sphere = sphere.sphere();
830 let half = core::f64::consts::FRAC_PI_2;
831 let turn = core::f64::consts::TAU;
832 let poles = [
833 surface.point_at(0.0, half, tol)?,
834 surface.point_at(0.0, -half, tol)?,
835 ];
836 let has_pcurve = |model: &Model, e: &Shape| {
837 model
838 .node(e)
839 .and_then(|n| n.data().as_edge())
840 .and_then(|d| d.pcurve_for(surface_id, e.location()))
841 .is_some()
842 };
843 let ring_vertices: Vec<Shape> = ring
844 .iter()
845 .filter_map(|e| ogeom_algo::edge_vertices(model, e).ok().flatten())
846 .flat_map(|(a, b)| [a, b])
847 .collect();
848 let vertex_at = |model: &Model, p: Point| {
849 ring_vertices.iter().find(|v| {
850 model
851 .node(v)
852 .and_then(|n| n.data().as_vertex())
853 .is_some_and(|d| d.point.is_within(p, d.tolerance.get().max(tol.confusion())))
854 })
855 };
856
857 let mut pieces: Vec<Shape> = Vec::with_capacity(ring.len() + 2);
859 let mut added: Vec<(TShapeId, Vec<Shape>)> = Vec::new();
860 let mut fresh: Vec<Shape> = Vec::new();
861 for occurrence in ring {
862 let Some(data) = model.node(occurrence).and_then(|n| n.data().as_edge()) else {
863 return Ok(None);
864 };
865 if data.degenerate {
866 pieces.push(occurrence.clone());
867 continue;
868 }
869 let Some(EdgeRepr::Curve3d {
872 curve,
873 range,
874 location,
875 }) = data.curve3d()
876 else {
877 return Ok(None);
878 };
879 if !location.is_identity() || !occurrence.location().is_identity() {
880 return Ok(None);
881 }
882 let (curve_id, range) = (*curve, *range);
883 let Some(Curve::Circle(circle)) = model.geometry().curve(curve_id).cloned() else {
884 if !has_pcurve(model, occurrence) {
885 return Ok(None);
886 }
887 pieces.push(occurrence.clone());
888 continue;
889 };
890 let c = circle.circle();
891 let great = c.centre().distance(sphere.centre()) <= tol.confusion()
892 && (c.radius() - sphere.radius()).abs() <= tol.confusion();
893 let mut cuts: Vec<(f64, Point)> = Vec::new();
896 if great {
897 let margin = 1e-9 * (range.1 - range.0).abs().max(1.0);
898 for pole in poles {
899 let Some(base) = circle_parameter(&Curve::Circle(circle), pole) else {
900 continue;
901 };
902 for t0 in [base, -base] {
903 let first = t0 + ((range.0 - t0) / turn).ceil() * turn;
904 let mut t = first;
905 while t < range.1 - margin {
906 if t > range.0 + margin
907 && circle.point_at(t, tol)?.is_within(pole, tol.confusion())
908 && !cuts.iter().any(|(s, _)| (s - t).abs() <= margin)
909 {
910 cuts.push((t, pole));
911 }
912 t += turn;
913 }
914 }
915 }
916 }
917 if cuts.is_empty() {
918 if !has_pcurve(model, occurrence) {
919 fresh.push(occurrence.clone());
920 }
921 pieces.push(occurrence.clone());
922 continue;
923 }
924 if settled.contains(&occurrence.node()) {
925 return Ok(None);
926 }
927 cuts.sort_by(|a, b| a.0.total_cmp(&b.0));
928 let bounds = model.children_of(occurrence)?;
929 let (Some(first), Some(last)) = (bounds.first().cloned(), bounds.last().cloned()) else {
930 return Ok(None);
931 };
932 let mut split = Vec::with_capacity(cuts.len() + 1);
933 let (mut from, mut at) = (first, range.0);
934 for (t, pole) in cuts {
935 let vertex = match vertex_at(model, pole) {
936 Some(v) => v.clone(),
937 None => make_vertex(model, pole).shape,
938 };
939 let edge =
940 make_edge_between(model, Curve::Circle(circle), (at, t), &from, &vertex, tol)?
941 .shape;
942 split.push(edge);
943 (from, at) = (vertex, t);
944 }
945 let edge = make_edge_between(
946 model,
947 Curve::Circle(circle),
948 (at, range.1),
949 &from,
950 &last,
951 tol,
952 )?
953 .shape;
954 split.push(edge);
955 fresh.extend(split.iter().cloned());
956 if occurrence.orientation() == ogeom_topo::Orientation::Reversed {
957 pieces.extend(split.iter().rev().map(Shape::reversed));
958 } else {
959 pieces.extend(split.iter().cloned());
960 }
961 added.push((occurrence.node(), split));
962 }
963 if fresh.is_empty() || (!resolved && added.is_empty()) {
964 return Ok(None);
965 }
966
967 let old_mesh = ogeom_mesh::triangulate(model, face, ogeom_mesh::Deflection::default(), tol)?;
969 let mut middle = ogeom_math::Vector::new(0.0, 0.0, 0.0);
970 for p in &old_mesh.positions {
971 if let Ok(d) = (*p - sphere.centre()).normalized(tol) {
972 middle += d;
973 }
974 }
975 let local = sphere.frame().to_local(sphere.centre() + middle);
976 if local.x.hypot(local.y) <= 1e-6 * middle.magnitude().max(1.0) {
977 return Ok(None);
978 }
979 let centre_u = local.y.atan2(local.x);
980 for edge in &fresh {
981 let Some(EdgeRepr::Curve3d { curve, range, .. }) = model
982 .node(edge)
983 .and_then(|n| n.data().as_edge())
984 .and_then(|d| d.curve3d())
985 .cloned()
986 else {
987 return Ok(None);
988 };
989 let Some(geometry) = model.geometry().curve(curve).cloned() else {
990 return Ok(None);
991 };
992 let Some(pcurve) = ogeom_intersect::exact_pcurve_over(&geometry, range, surface, tol)
993 else {
994 return Ok(None);
995 };
996 let pcurve = match &pcurve {
999 ogeom_geom::PlanarCurve::Line(l) if l.axis().direction.vector().x.abs() <= 1e-12 => {
1000 let at = l.axis().location;
1001 let shift = ((centre_u - at.x) / turn).round() * turn;
1002 ogeom_geom::Line2d::over(
1003 Axis2::new(Point2::new(at.x + shift, at.y), l.axis().direction),
1004 l.domain().0,
1005 l.domain().1,
1006 )?
1007 .into()
1008 }
1009 _ => pcurve,
1010 };
1011 ogeom_algo::attach_pcurve(model, edge, pcurve, surface_id, Location::identity(), range)?;
1012 }
1013
1014 let mut ends: Vec<(Point2, Point2)> = Vec::with_capacity(pieces.len());
1017 let mut samples: Vec<Point2> = Vec::new();
1018 for piece in &pieces {
1019 let Some(EdgeRepr::PCurve { curve, range, .. }) = model
1020 .node(piece)
1021 .and_then(|n| n.data().as_edge())
1022 .and_then(|d| d.pcurve_for(surface_id, piece.location()))
1023 .cloned()
1024 else {
1025 return Ok(None);
1026 };
1027 let Some(pcurve) = model.geometry().pcurve(curve).cloned() else {
1028 return Ok(None);
1029 };
1030 let reversed = piece.orientation() == ogeom_topo::Orientation::Reversed;
1031 const SAMPLES: u32 = 16;
1032 let mut run = Vec::with_capacity(SAMPLES as usize + 1);
1033 for k in 0..=SAMPLES {
1034 let f = f64::from(k) / f64::from(SAMPLES);
1035 let t = if reversed {
1036 range.1 - (range.1 - range.0) * f
1037 } else {
1038 range.0 + (range.1 - range.0) * f
1039 };
1040 run.push(pcurve.point_at(t, tol)?);
1041 }
1042 ends.push((run[0], run[run.len() - 1]));
1043 samples.extend(run);
1044 }
1045 let n = pieces.len();
1046 let joint = tol.parametric().max(1e-9);
1047 let mut edges = Vec::with_capacity(n + 2);
1048 for k in 0..n {
1049 edges.push(pieces[k].clone());
1050 let (end, next) = (ends[k].1, ends[(k + 1) % n].0);
1051 if end.distance(next) <= joint {
1052 continue;
1053 }
1054 if (end.y - next.y).abs() > joint || (end.y.abs() - half).abs() > joint {
1055 return Ok(None);
1056 }
1057 let Some((_, vertex)) = ogeom_algo::edge_vertices(model, &pieces[k])? else {
1058 return Ok(None);
1059 };
1060 let gap = next.x - end.x;
1061 let mut data = EdgeData::new();
1062 data.degenerate = true;
1063 let degenerate = model.add_edge(data, &[vertex.clone(), vertex])?;
1064 let pcurve: ogeom_geom::PlanarCurve = ogeom_geom::Line2d::over(
1065 Axis2::new(end, Direction2::new(Vector2::new(gap.signum(), 0.0), tol)?),
1066 0.0,
1067 gap.abs(),
1068 )?
1069 .into();
1070 ogeom_algo::attach_pcurve(
1071 model,
1072 °enerate,
1073 pcurve,
1074 surface_id,
1075 Location::identity(),
1076 (0.0, gap.abs()),
1077 )?;
1078 edges.push(degenerate);
1079 }
1080 let mut area = 0.0;
1083 for (k, p) in samples.iter().enumerate() {
1084 let q = samples[(k + 1) % samples.len()];
1085 area += p.x * q.y - q.x * p.y;
1086 }
1087 let wire = make_wire(model, &edges, tol)?.shape;
1088 let rebuilt = make_face_on(model, surface_id, &[wire], tol)?.shape;
1089 let rebuilt = if area < 0.0 {
1090 rebuilt.reversed()
1091 } else {
1092 rebuilt
1093 };
1094 let new_mesh =
1095 ogeom_mesh::triangulate(model, &rebuilt, ogeom_mesh::Deflection::default(), tol)?;
1096 let (before, after) = (old_mesh.area(), new_mesh.area());
1097 if (before - after).abs() > 1e-2 * before.max(after) {
1098 return Ok(None);
1099 }
1100 Ok(Some((rebuilt, added)))
1101}
1102
1103fn stored_end(
1105 model: &Model,
1106 surface_id: ogeom_topo::SurfaceId,
1107 occurrence: &Shape,
1108 tol: Tolerances,
1109) -> OgeomResult<Option<Point2>> {
1110 let Some(EdgeRepr::PCurve { curve, range, .. }) = model
1111 .node(occurrence)
1112 .and_then(|n| n.data().as_edge())
1113 .and_then(|d| d.pcurve_for(surface_id, occurrence.location()))
1114 else {
1115 return Ok(None);
1116 };
1117 let Some(pcurve) = model.geometry().pcurve(*curve) else {
1118 return Ok(None);
1119 };
1120 let t = if occurrence.orientation() == ogeom_topo::Orientation::Reversed {
1121 range.0
1122 } else {
1123 range.1
1124 };
1125 Ok(Some(pcurve.point_at(t, tol)?))
1126}
1127
1128fn rebuild_with(
1132 model: &mut Model,
1133 face: &Shape,
1134 substitution: &FastMap<TShapeId, Vec<Shape>>,
1135 tol: Tolerances,
1136) -> OgeomResult<Shape> {
1137 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
1138 ogeom_bail!(Dangling, "face is not in this model");
1139 };
1140 let surface_id = data.surface;
1141 let Some(surface) = model.geometry().surface(surface_id).cloned() else {
1142 ogeom_bail!(Dangling, "face refers to a surface not in this model");
1143 };
1144 let mut wires = Vec::new();
1145 let mut replaced = Vec::new();
1146 for wire in model.ordered_children_of(&face.oriented(ogeom_topo::Orientation::Forward))? {
1149 let mut edges = Vec::new();
1150 for edge in model.ordered_children_of(&wire)? {
1151 match substitution.get(&edge.node()) {
1152 Some(new_edges) => {
1153 replaced.extend(new_edges.iter().cloned());
1154 if edge.orientation() == ogeom_topo::Orientation::Reversed {
1155 edges.extend(new_edges.iter().rev().map(Shape::reversed));
1156 } else {
1157 edges.extend(new_edges.iter().cloned());
1158 }
1159 }
1160 None => edges.push(edge),
1161 }
1162 }
1163 wires.push(make_wire(model, &edges, tol)?.shape);
1164 }
1165 attach_face_pcurves(model, &surface, surface_id, &replaced, tol)?;
1166 Ok(make_face_on(model, surface_id, &wires, tol)?.shape)
1167}