1use ogeom_algo::{Built, History, make_sphere};
22use ogeom_core::{OgeomError, OgeomResult, Tolerances};
23use ogeom_geom::{Curve2d as _, Curve3d as _, Surface as _};
24use ogeom_geom::{PlanarCurve, SurfaceGeometry, Transformable};
25use ogeom_math::{Direction, Frame, Point, Point2, Vector};
26use ogeom_topo::{EdgeRepr, Model, NodeData, Orientation, Shape, ShapeType, explore_unique};
27
28pub const TOUCHING: f64 = 1e-2;
33
34struct Ball {
37 solid: Shape,
38 face: Shape,
39 centre: Point,
40 radius: f64,
41 axis: Vector,
42 x: Vector,
43}
44
45const CHARTS_TRIED: usize = 6;
48
49pub(crate) fn with_poles_clear(
64 model: &mut Model,
65 a: &Shape,
66 b: &Shape,
67 tol: Tolerances,
68 mut run: impl FnMut(&mut Model, &Shape, &Shape) -> OgeomResult<Built>,
69) -> OgeomResult<Built> {
70 #[cfg(feature = "test-hooks")]
71 if crate::work::is_unturned() {
72 return run(model, a, b);
73 }
74 let mut last: Option<OgeomError> = None;
75 for rank in 0..CHARTS_TRIED {
76 let Some((ta, tb, before)) = turned(model, a, b, rank, tol)? else {
77 break;
78 };
79 #[cfg(feature = "test-hooks")]
80 let refused = crate::work::is_turn_refused(rank);
81 #[cfg(not(feature = "test-hooks"))]
82 let refused = false;
83 let result = if refused {
84 Err(ogeom_core::ogeom_err!(
85 NotDone,
86 "the turned run is taken as refused"
87 ))
88 } else {
89 run(model, &ta, &tb)
90 };
91 match result {
92 Ok(built) => return Ok(Built::new(built.shape, before.then(&built.history))),
93 Err(e @ (OgeomError::Cancelled | OgeomError::Dangling(_))) => return Err(e),
94 Err(e) => last = Some(e),
95 }
96 }
97 match last {
98 Some(e) => Err(ogeom_core::ogeom_err!(
99 NotDone,
100 "a ball's pole stands on or beside the other operand, and the boolean on every \
101 chart of the ball whose poles stand clear of it is refused (the last: {e}); on \
102 its own chart a section beside a pole is not resolved soundly; see \
103 docs/CAPABILITIES.md, bool.booleans"
104 )),
105 None => run(model, a, b),
106 }
107}
108
109fn turned(
113 model: &mut Model,
114 a: &Shape,
115 b: &Shape,
116 rank: usize,
117 tol: Tolerances,
118) -> OgeomResult<Option<(Shape, Shape, History)>> {
119 let mut before = History::new();
120 let mut any = false;
121 let (mut ta, mut tb) = (a.clone(), b.clone());
122 for (operand, other) in [(&mut ta, b), (&mut tb, a)] {
123 if let Some((made, history)) = operand_turned(model, operand, other, rank, tol)? {
124 before = before.then(&history);
125 *operand = made;
126 any = true;
127 }
128 }
129 Ok(any.then_some((ta, tb, before)))
130}
131
132fn operand_turned(
137 model: &mut Model,
138 operand: &Shape,
139 other: &Shape,
140 rank: usize,
141 tol: Tolerances,
142) -> OgeomResult<Option<(Shape, History)>> {
143 if model.kind_of(operand)? == ShapeType::Compound {
144 let mut before = History::new();
145 let mut any = false;
146 let mut members = Vec::new();
147 for member in model.children_of(operand)? {
148 if let Some((made, history)) = operand_turned(model, &member, other, rank, tol)? {
149 before = before.then(&history);
150 members.push(made);
151 any = true;
152 } else {
153 members.push(member);
154 }
155 }
156 if !any {
157 return Ok(None);
158 }
159 let made = model.add_compound(&members)?;
160 before.modify(operand, made.clone());
161 return Ok(Some((made, before)));
162 }
163 let Some(ball) = whole_ball(model, operand, tol)? else {
164 return faces_turned(model, operand, other, rank, tol);
165 };
166 let walls = walls_of(model, other, tol)?;
167 let clearance = |p: Point| clearance_of(&walls, p, tol);
168 let poles = |axis: Vector| {
169 clearance(ball.centre + axis * ball.radius).min(clearance(ball.centre - axis * ball.radius))
170 };
171 if poles(ball.axis) > ball.radius * TOUCHING {
172 return Ok(None);
173 }
174 let candidates = candidate_axes(ball.x, ball.axis);
180 let x = ball.x;
181 let y = ball.axis.cross(ball.x);
182 let mut ranked: Vec<(f64, Vector)> = Vec::new();
183 for candidate in candidates {
184 let Ok(axis) = candidate.normalized(tol) else {
185 continue;
186 };
187 let score = poles(axis);
188 if score > ball.radius * TOUCHING {
189 ranked.push((score, axis));
190 }
191 }
192 ranked.sort_by(|a, b| b.0.total_cmp(&a.0));
193 let Some(&(_, axis)) = ranked.get(rank) else {
194 return Ok(None);
195 };
196 let across = if axis.cross(x).magnitude() > 0.5 {
199 x
200 } else {
201 y
202 };
203 let Ok(u) = axis.cross(across).normalized(tol) else {
204 return Ok(None);
205 };
206 let w = axis.cross(u);
207 let seam = [u, -u, w, -w]
208 .into_iter()
209 .map(|d| (clearance(ball.centre + d * ball.radius), d))
210 .fold(None, |kept: Option<(f64, Vector)>, (c, d)| match kept {
211 Some((k, _)) if k >= c => kept,
212 _ => Some((c, d)),
213 })
214 .map_or(u, |(_, d)| d);
215 let frame = Frame::new(
216 ball.centre,
217 Direction::new(axis, tol)?,
218 Direction::new(seam, tol)?,
219 tol,
220 )?;
221 let made = make_sphere(model, frame, ball.radius, tol)?.shape;
222 let Some(face) = explore_unique(model, &made, ShapeType::Face)?
223 .first()
224 .cloned()
225 else {
226 return Ok(None);
227 };
228 let mut before = History::new();
229 before.modify(&ball.solid, made.clone());
230 for shell in explore_unique(model, &ball.solid, ShapeType::Shell)? {
231 for new_shell in explore_unique(model, &made, ShapeType::Shell)? {
232 before.modify(&shell, new_shell);
233 }
234 }
235 before.modify(&ball.face, face);
236 for kind in [ShapeType::Edge, ShapeType::Vertex] {
237 for sub in explore_unique(model, &ball.solid, kind)? {
238 before.delete(&sub);
239 }
240 }
241 Ok(Some((made, before)))
242}
243
244fn whole_ball(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Option<Ball>> {
249 if model.kind_of(shape)? != ShapeType::Solid {
250 return Ok(None);
251 }
252 let faces = explore_unique(model, shape, ShapeType::Face)?;
253 let [face] = faces.as_slice() else {
254 return Ok(None);
255 };
256 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
257 return Ok(None);
258 };
259 if !data.location.is_identity() {
260 return Ok(None);
261 }
262 let placement = face.transform(model.datums())?;
263 let Some(SurfaceGeometry::Sphere(stored)) = model.geometry().surface(data.surface) else {
264 return Ok(None);
265 };
266 let mut seams = Vec::new();
269 for wire in model.children_of(face)? {
270 for edge in model.children_of(&wire)? {
271 let Some(NodeData::Edge(e)) = model.node(&edge).map(|n| n.data()) else {
272 return Ok(None);
273 };
274 if matches!(e.curve3d(), Some(EdgeRepr::Curve3d { .. })) {
275 seams.push(edge.node());
276 }
277 }
278 }
279 if seams.len() != 2 || seams[0] != seams[1] {
280 return Ok(None);
281 }
282 let sphere = stored.sphere();
283 let frame = sphere.frame();
284 let (u, v) = (0.5, 0.25);
289 let at = stored.point_at(u, v, tol)?;
290 let normal = stored.normal_at(u, v, tol)?.vector();
291 let reversed = face.orientation() == ogeom_topo::Orientation::Reversed;
292 let outward = (at - sphere.centre()).dot(normal) > 0.0;
293 if outward == reversed {
294 return Ok(None);
295 }
296 Ok(Some(Ball {
297 solid: shape.clone(),
298 face: face.clone(),
299 centre: placement.apply(sphere.centre()),
300 radius: sphere.radius() * placement.scale_factor().abs(),
301 axis: placement.apply_vector(frame.z().vector()).normalized(tol)?,
302 x: placement.apply_vector(frame.x().vector()).normalized(tol)?,
303 }))
304}
305
306fn walls_of(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Vec<SurfaceGeometry>> {
308 let mut out = Vec::new();
309 for face in explore_unique(model, shape, ShapeType::Face)? {
310 let Some(NodeData::Face(data)) = model.node(&face).map(|n| n.data()) else {
311 continue;
312 };
313 let Some(surface) = model.geometry().surface(data.surface) else {
314 continue;
315 };
316 out.push(surface.transformed(&face.transform(model.datums())?, tol)?);
317 }
318 Ok(out)
319}
320
321fn clearance_of(walls: &[SurfaceGeometry], p: Point, tol: Tolerances) -> f64 {
327 walls
328 .iter()
329 .filter_map(|wall| match wall {
330 SurfaceGeometry::Plane(plane) => {
331 Some(plane.plane().distance_to(p)).filter(|d| *d > tol.confusion())
332 }
333 _ => ogeom_algo::project_on_surface(wall, p, 8, tol)
334 .ok()
335 .map(|found| found.distance),
336 })
337 .fold(f64::INFINITY, f64::min)
338}
339
340fn candidate_axes(x: Vector, z: Vector) -> Vec<Vector> {
343 let y = z.cross(x);
344 let mut candidates = vec![x, y];
345 for (p, q) in [(x, y), (y, z), (z, x)] {
346 candidates.push(p + q);
347 candidates.push(p - q);
348 }
349 for (s, t) in [(1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)] {
350 candidates.push(x * s + y * t + z);
351 }
352 candidates
353}
354
355const CLEAR_OF_TRIM: f64 = 0.1;
360
361const EDGE_SAMPLES: usize = 128;
363
364const FIT_SAMPLES: usize = 512;
368const FIT_IN_CHART: f64 = 1e-11;
369
370const OFF_SEAM: f64 = 1e-6;
373
374const BAND_SEAMS: usize = 8;
378const BAND_PROBES: usize = 8;
379
380type Record = (Shape, Option<Shape>);
383
384type Split = (Shape, Vec<Shape>);
387
388type Started = (Vec<Shape>, Shape, Vec<Split>);
391
392fn faces_turned(
417 model: &mut Model,
418 solid: &Shape,
419 other: &Shape,
420 rank: usize,
421 tol: Tolerances,
422) -> OgeomResult<Option<(Shape, History)>> {
423 if model.kind_of(solid)? != ShapeType::Solid {
424 return Ok(None);
425 }
426 let bare = solid
427 .located(ogeom_topo::Location::identity())
428 .oriented(Orientation::Forward);
429 let mut pinned_any = false;
430 let mut left_handed = false;
431 for face in explore_unique(model, &bare, ShapeType::Face)? {
432 if let Some(pinned) = pinned_sphere(model, &face)? {
433 pinned_any = true;
434 left_handed |= pinned.sphere.frame().handedness() != ogeom_math::Handedness::Right;
435 }
436 }
437 if !pinned_any {
438 return Ok(None);
439 }
440 let placement = solid.transform(model.datums())?;
441 let rigid =
442 (placement.scale_factor().abs() - 1.0).abs() <= 1e-9 && placement.preserves_handedness();
443 let (target, baked) = if rigid && !left_handed {
444 (solid.clone(), None)
445 } else {
446 let baked = ogeom_algo::baked_shape(model, solid, tol)?;
447 (baked.shape, Some(baked.history))
448 };
449 let Some((made, records)) = rigid_faces_turned(model, &target, other, rank, tol)? else {
450 return Ok(None);
451 };
452 let mut history = History::new();
453 for (old, new) in records {
454 match new {
455 Some(new) => history.modify(&old, new),
456 None => history.delete(&old),
457 }
458 }
459 Ok(Some(match baked {
460 Some(baked) => (made, baked.then(&history)),
461 None => (made, history),
462 }))
463}
464
465fn rigid_faces_turned(
468 model: &mut Model,
469 solid: &Shape,
470 other: &Shape,
471 rank: usize,
472 tol: Tolerances,
473) -> OgeomResult<Option<(Shape, Vec<Record>)>> {
474 let placement = solid.location().clone();
477 let bare = solid
478 .located(ogeom_topo::Location::identity())
479 .oriented(Orientation::Forward);
480 let to_local = solid.transform(model.datums())?.inverse()?;
481 let mut walls: Option<Vec<SurfaceGeometry>> = None;
482 let mut records = Vec::new();
483 let mut splits: Vec<(Split, ogeom_topo::TShapeId)> = Vec::new();
486 let shells = model.children_of(&bare)?;
487 let mut turned_in: Vec<Vec<Option<Shape>>> = Vec::with_capacity(shells.len());
488 for shell in &shells {
489 let faces = model.children_of(shell)?;
490 let mut turned_here = Vec::with_capacity(faces.len());
491 for face in &faces {
492 let mut turned = None;
493 if let Some(pinned) = pinned_sphere(model, face)?
494 && pinned.sphere.frame().handedness() == ogeom_math::Handedness::Right
495 {
496 let walls = match &walls {
497 Some(w) => w,
498 None => walls.insert(
499 walls_of(model, other, tol)?
500 .iter()
501 .map(|w| w.transformed(&to_local, tol))
502 .collect::<OgeomResult<_>>()?,
503 ),
504 };
505 if let Some((new, cut)) = face_turned(model, face, &pinned, walls, rank, tol)? {
506 records.push((face.clone(), Some(new.clone())));
507 for gone in &pinned.dropped {
508 records.push((gone.clone(), None));
509 }
510 for (old, pieces) in &cut {
511 for piece in pieces {
512 records.push((old.clone(), Some(piece.clone())));
513 }
514 }
515 splits.extend(cut.into_iter().map(|s| (s, face.node())));
516 turned = Some(new);
517 }
518 }
519 turned_here.push(turned);
520 }
521 turned_in.push(turned_here);
522 }
523 for (shell, turned_here) in shells.iter().zip(&turned_in) {
526 for (face, turned) in model.children_of(shell)?.iter().zip(turned_here) {
527 if turned.is_none() {
528 continue;
529 }
530 for edge in explore_unique(model, face, ShapeType::Edge)? {
531 if splits
532 .iter()
533 .any(|((old, _), by)| old.node() == edge.node() && *by != face.node())
534 {
535 return Ok(None);
536 }
537 }
538 }
539 }
540 let cut: Vec<Split> = splits.into_iter().map(|(s, _)| s).collect();
541 let mut changed = false;
542 let mut rebuilt_shells = Vec::with_capacity(shells.len());
543 for (shell, turned_here) in shells.into_iter().zip(turned_in) {
544 let faces = model.children_of(&shell)?;
545 let mut kept = Vec::with_capacity(faces.len());
546 let mut touched = false;
547 for (face, turned) in faces.into_iter().zip(turned_here) {
548 if let Some(new) = turned {
549 kept.push(new);
550 touched = true;
551 } else if let Some(new) = with_pieces(model, &face, &cut)? {
552 records.push((face, Some(new.clone())));
553 kept.push(new);
554 touched = true;
555 } else {
556 kept.push(face);
557 }
558 }
559 if touched {
560 let new_shell = model.add_shell(&kept)?;
561 records.push((shell, Some(new_shell.clone())));
562 rebuilt_shells.push(new_shell);
563 changed = true;
564 } else {
565 rebuilt_shells.push(shell);
566 }
567 }
568 if !changed {
569 return Ok(None);
570 }
571 let shells = rebuilt_shells;
572 let made = model.add_solid(&shells)?;
573 if !ogeom_algo::check(model, &made, tol)?.is_valid() {
574 return Ok(None);
575 }
576 let placed = made.moved(&placement).composed(solid.orientation());
577 let mut records: Vec<Record> = records
578 .into_iter()
579 .map(|(old, new)| (old.moved(&placement), new.map(|n| n.moved(&placement))))
580 .collect();
581 records.push((solid.clone(), Some(placed.clone())));
582 Ok(Some((placed, records)))
583}
584
585fn with_pieces(model: &mut Model, face: &Shape, splits: &[Split]) -> OgeomResult<Option<Shape>> {
589 if splits.is_empty() {
590 return Ok(None);
591 }
592 let Some(node) = model.node(face) else {
593 ogeom_core::ogeom_bail!(Dangling, "face is not in this model");
594 };
595 let NodeData::Face(data) = node.data() else {
596 return Ok(None);
597 };
598 let mut data = (**data).clone();
599 data.triangulation = None;
600 let wires = node.children().to_vec();
601 let mut any = false;
602 let mut made = Vec::with_capacity(wires.len());
603 for wire in wires {
604 let Some(held) = model.node(&wire) else {
605 ogeom_core::ogeom_bail!(Dangling, "wire is not in this model");
606 };
607 let mut edges = Vec::with_capacity(held.children().len() + 1);
608 for edge in held.children() {
609 let Some((_, pieces)) = splits.iter().find(|(old, _)| old.node() == edge.node()) else {
610 edges.push(edge.clone());
611 continue;
612 };
613 if !edge.location().is_identity() {
614 return Ok(None);
615 }
616 any = true;
617 if edge.orientation() == Orientation::Reversed {
618 edges.extend(pieces.iter().rev().map(Shape::reversed));
619 } else {
620 edges.extend(pieces.iter().cloned());
621 }
622 }
623 let rebuilt = model.add_wire(&edges)?;
624 made.push(
625 rebuilt
626 .located(wire.location().clone())
627 .oriented(wire.orientation()),
628 );
629 }
630 if !any {
631 return Ok(None);
632 }
633 let new = model.add_face(data, &made)?;
634 Ok(Some(
635 new.located(face.location().clone())
636 .oriented(face.orientation()),
637 ))
638}
639
640struct Pinned {
642 sphere: ogeom_math::Sphere,
643 poles: Vec<Point>,
645 rings: Vec<Vec<Shape>>,
648 dropped: Vec<Shape>,
650}
651
652fn pinned_sphere(model: &Model, face: &Shape) -> OgeomResult<Option<Pinned>> {
656 pinned_sphere_with(model, face, true)
657}
658
659fn pinned_sphere_with(
662 model: &Model,
663 face: &Shape,
664 need_poles: bool,
665) -> OgeomResult<Option<Pinned>> {
666 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
667 return Ok(None);
668 };
669 if !face.location().is_identity() || !data.location.is_identity() {
670 return Ok(None);
671 }
672 let Some(SurfaceGeometry::Sphere(stored)) = model.geometry().surface(data.surface) else {
673 return Ok(None);
674 };
675 let sphere = stored.sphere();
676 let forward = face.oriented(Orientation::Forward);
678 let mut uses: ogeom_core::FastMap<ogeom_topo::TShapeId, usize> = ogeom_core::FastMap::default();
679 let mut edges = Vec::new();
680 for wire in model.children_of(&forward)? {
681 for edge in model.children_of(&wire)? {
682 *uses.entry(edge.node()).or_default() += 1;
683 edges.push(edge);
684 }
685 }
686 let mut poles = Vec::new();
687 let mut dropped: Vec<Shape> = Vec::new();
688 let mut kept: Vec<(Shape, Shape, Shape)> = Vec::new();
689 for edge in edges {
690 let Some(NodeData::Edge(e)) = model.node(&edge).map(|n| n.data()) else {
691 return Ok(None);
692 };
693 if !edge.location().is_identity() {
694 return Ok(None);
695 }
696 let Some((start, end)) = ogeom_algo::edge_vertices(model, &edge)? else {
697 return Ok(None);
698 };
699 if e.degenerate {
700 let Some(NodeData::Vertex(v)) = model.node(&start).map(|n| n.data()) else {
701 return Ok(None);
702 };
703 if !start.location().is_identity() {
704 return Ok(None);
705 }
706 poles.push(v.point);
707 if !dropped.iter().any(|d| d.node() == edge.node()) {
708 dropped.push(edge.clone());
709 dropped.push(start);
710 }
711 continue;
712 }
713 match uses.get(&edge.node()) {
714 Some(1) => {}
715 Some(2) => {
717 if !dropped.iter().any(|d| d.node() == edge.node()) {
718 dropped.push(edge.clone());
719 }
720 continue;
721 }
722 _ => return Ok(None),
723 }
724 if !matches!(e.curve3d(), Some(EdgeRepr::Curve3d { location, .. }) if location.is_identity())
725 {
726 return Ok(None);
727 }
728 kept.push((edge, start, end));
729 }
730 if (need_poles && poles.is_empty()) || kept.is_empty() {
731 return Ok(None);
732 }
733 dropped.retain(|d| {
735 !kept
736 .iter()
737 .any(|(_, s, e)| s.node() == d.node() || e.node() == d.node())
738 });
739 let mut rings = Vec::new();
740 let mut used = vec![false; kept.len()];
741 while let Some(first) = used.iter().position(|u| !u) {
742 used[first] = true;
743 let origin = kept[first].1.node();
744 let mut ring = vec![kept[first].0.clone()];
745 let mut at = kept[first].2.node();
746 while at != origin {
747 let next: Vec<usize> = (0..kept.len())
748 .filter(|&i| !used[i] && kept[i].1.node() == at)
749 .collect();
750 let [i] = next.as_slice() else {
751 return Ok(None);
752 };
753 used[*i] = true;
754 ring.push(kept[*i].0.clone());
755 at = kept[*i].2.node();
756 }
757 rings.push(ring);
758 }
759 Ok(Some(Pinned {
760 sphere,
761 poles,
762 rings,
763 dropped,
764 }))
765}
766
767fn face_turned(
773 model: &mut Model,
774 face: &Shape,
775 pinned: &Pinned,
776 walls: &[SurfaceGeometry],
777 rank: usize,
778 tol: Tolerances,
779) -> OgeomResult<Option<(Shape, Vec<Split>)>> {
780 let (flats, curved): (Vec<SurfaceGeometry>, Vec<SurfaceGeometry>) = walls
784 .iter()
785 .cloned()
786 .partition(|w| matches!(w, SurfaceGeometry::Plane(_)));
787 let clearance = |p: Point| clearance_of(&curved, p, tol);
788 let (centre, radius) = (pinned.sphere.centre(), pinned.sphere.radius());
789 let beside = |p: Point| clearance_of(walls, p, tol) <= radius * TOUCHING;
790 if !pinned.poles.iter().any(|&p| beside(p)) {
791 return Ok(None);
792 }
793 let mut trim: Vec<Point> = Vec::new();
794 for ring in &pinned.rings {
795 trim.extend(ring_points(model, ring, tol)?);
796 }
797 let near_trim = |p: Point| -> f64 {
798 trim.iter()
799 .map(|q| q.distance(p))
800 .fold(f64::INFINITY, f64::min)
801 };
802 let frame = pinned.sphere.frame();
803 let mut scored: Vec<(f64, Vector)> = Vec::new();
804 for candidate in candidate_axes(frame.x().vector(), frame.z().vector()) {
805 let Ok(axis) = candidate.normalized(tol) else {
806 continue;
807 };
808 let ends = [centre + axis * radius, centre - axis * radius];
809 if ends.iter().any(|&p| near_trim(p) <= radius * CLEAR_OF_TRIM) {
810 continue;
811 }
812 if ends
813 .iter()
814 .any(|&p| clearance_of(&flats, p, tol) <= radius * TOUCHING)
815 {
816 continue;
817 }
818 let score = clearance(ends[0]).min(clearance(ends[1]));
819 if score > radius * TOUCHING {
820 scored.push((score, axis));
821 }
822 }
823 scored.sort_by(|a, b| b.0.total_cmp(&a.0));
824 let mut held = 0;
826 for (_, axis) in scored {
827 if let Some(made) = restated(model, face, pinned, axis, &clearance, None, tol)? {
828 if held == rank {
829 return Ok(Some(made));
830 }
831 held += 1;
832 }
833 }
834 Ok(None)
835}
836
837fn edge_samples(
840 model: &Model,
841 edge: &Shape,
842 count: usize,
843 tol: Tolerances,
844) -> OgeomResult<Vec<(f64, Point)>> {
845 let Some(NodeData::Edge(e)) = model.node(edge).map(|n| n.data()) else {
846 return Ok(Vec::new());
847 };
848 let Some(EdgeRepr::Curve3d { curve, range, .. }) = e.curve3d() else {
849 return Ok(Vec::new());
850 };
851 let Some(curve) = model.geometry().curve(*curve) else {
852 return Ok(Vec::new());
853 };
854 let mut out = Vec::with_capacity(count + 1);
855 for k in 0..=count {
856 #[allow(clippy::cast_precision_loss)]
857 let s = k as f64 / count as f64;
858 let s = if edge.orientation() == Orientation::Reversed {
859 1.0 - s
860 } else {
861 s
862 };
863 let t = range.0 + (range.1 - range.0) * s;
864 out.push((t, curve.point_at(t, tol)?));
865 }
866 Ok(out)
867}
868
869fn ring_points(model: &Model, ring: &[Shape], tol: Tolerances) -> OgeomResult<Vec<Point>> {
871 let mut out = Vec::new();
872 for edge in ring {
873 out.extend(
874 edge_samples(model, edge, EDGE_SAMPLES, tol)?
875 .into_iter()
876 .map(|(_, p)| p),
877 );
878 }
879 Ok(out)
880}
881
882fn chart_of(frame: &Frame, p: Point) -> (f64, f64) {
884 let local = frame.to_local(p);
885 (
886 local.y.atan2(local.x),
887 local.z.atan2(local.x.hypot(local.y)),
888 )
889}
890
891fn unwrapped(u: f64, near: f64) -> f64 {
893 let turn = core::f64::consts::TAU;
894 u - turn * ((u - near) / turn).round()
895}
896
897fn longitudes(frame: &Frame, ring: &[Point]) -> Vec<f64> {
900 let mut out: Vec<f64> = Vec::with_capacity(ring.len());
901 for &p in ring {
902 let u = chart_of(frame, p).0;
903 out.push(out.last().map_or(u, |&before| unwrapped(u, before)));
904 }
905 out
906}
907
908fn winding(us: &[f64]) -> i64 {
910 let (Some(first), Some(last)) = (us.first(), us.last()) else {
911 return 0;
912 };
913 #[allow(clippy::cast_possible_truncation)]
914 let turns = ((last - first) / core::f64::consts::TAU).round() as i64;
915 turns
916}
917
918fn span(us: &[f64]) -> (f64, f64) {
920 us.iter()
921 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &u| {
922 (lo.min(u), hi.max(u))
923 })
924}
925
926fn reaches(lo: f64, hi: f64, u: f64) -> bool {
929 let turn = core::f64::consts::TAU;
930 let first = u + turn * ((lo - OFF_SEAM - u) / turn).ceil();
931 first <= hi + OFF_SEAM
932}
933
934fn crossed_once(us: &[f64], u: f64) -> Option<usize> {
938 let turn = core::f64::consts::TAU;
939 let mut count = 0.0;
940 let mut found = None;
941 for (i, w) in us.windows(2).enumerate() {
942 let (lo, hi) = if w[0] <= w[1] {
943 (w[0], w[1])
944 } else {
945 (w[1], w[0])
946 };
947 let passed = (((hi - u) / turn).floor() - ((lo - u) / turn).floor()).max(0.0);
949 if passed > 0.0 {
950 count += passed;
951 found = Some(i);
952 }
953 }
954 if count == 1.0 { found } else { None }
955}
956
957fn face_surface(model: &Model, face: &Shape) -> Option<ogeom_topo::SurfaceId> {
959 match model.node(face).map(|n| n.data()) {
960 Some(NodeData::Face(data)) => Some(data.surface),
961 _ => None,
962 }
963}
964
965fn started_on_seam(
970 model: &mut Model,
971 ring: &[Shape],
972 chart: &Frame,
973 old_surface: ogeom_topo::SurfaceId,
974 tol: Tolerances,
975) -> OgeomResult<Option<Started>> {
976 use core::f64::consts::FRAC_PI_2;
977 let mut samples: Vec<(usize, f64, f64)> = Vec::new();
978 for (k, edge) in ring.iter().enumerate() {
979 for (t, p) in edge_samples(model, edge, EDGE_SAMPLES, tol)? {
980 samples.push((k, t, chart_of(chart, p).0));
981 }
982 }
983 let crossings: Vec<(usize, f64, f64, f64)> = samples
986 .windows(2)
987 .filter(|w| {
988 w[0].0 == w[1].0
989 && (w[0].2 < 0.0) != (w[1].2 < 0.0)
990 && w[0].2.abs() < FRAC_PI_2
991 && w[1].2.abs() < FRAC_PI_2
992 })
993 .map(|w| (w[0].0, w[0].1, w[1].1, w[0].2))
994 .collect();
995 let [(k, lo, hi, u_lo)] = crossings[..] else {
996 return Ok(None);
997 };
998 let (mut lo, mut hi) = (lo, hi);
999 let Some(NodeData::Edge(data)) = model.node(&ring[k]).map(|n| n.data()) else {
1000 return Ok(None);
1001 };
1002 let Some(EdgeRepr::Curve3d { curve, .. }) = data.curve3d() else {
1003 return Ok(None);
1004 };
1005 let Some(curve) = model.geometry().curve(*curve).cloned() else {
1006 return Ok(None);
1007 };
1008 let below = u_lo < 0.0;
1009 for _ in 0..200 {
1010 let mid = f64::midpoint(lo, hi);
1011 if mid == lo || mid == hi {
1012 break;
1013 }
1014 if (chart_of(chart, curve.point_at(mid, tol)?).0 < 0.0) == below {
1015 lo = mid;
1016 } else {
1017 hi = mid;
1018 }
1019 }
1020 let t = f64::midpoint(lo, hi);
1021 let at = curve.point_at(t, tol)?;
1022 let Some((start, end)) = ogeom_algo::edge_vertices(model, &ring[k])? else {
1023 return Ok(None);
1024 };
1025 for (vertex, from) in [(&start, k), (&end, k + 1)] {
1026 let Some(NodeData::Vertex(v)) = model.node(vertex).map(|n| n.data()) else {
1027 return Ok(None);
1028 };
1029 if v.point.distance(at) <= v.tolerance.get().max(tol.confusion()) {
1030 let mut started = ring.to_vec();
1031 started.rotate_left(from % ring.len());
1032 return Ok(Some((started, vertex.clone(), Vec::new())));
1033 }
1034 }
1035 let edge = ring[k].oriented(Orientation::Forward);
1036 let Some((pieces, vertex)) = cut_edge(model, &edge, t, old_surface, tol)? else {
1037 return Ok(None);
1038 };
1039 let [head, tail] = pieces.as_slice() else {
1040 return Ok(None);
1041 };
1042 let run = if ring[k].orientation() == Orientation::Reversed {
1043 [tail.reversed(), head.reversed()]
1044 } else {
1045 [head.clone(), tail.clone()]
1046 };
1047 let mut started: Vec<Shape> = ring[..k].to_vec();
1048 started.extend(run);
1049 started.extend(ring[k + 1..].iter().cloned());
1050 started.rotate_left(k + 1);
1051 Ok(Some((started, vertex, vec![(edge, pieces)])))
1052}
1053
1054fn cut_edge(
1060 model: &mut Model,
1061 edge: &Shape,
1062 t: f64,
1063 skip: ogeom_topo::SurfaceId,
1064 tol: Tolerances,
1065) -> OgeomResult<Option<(Vec<Shape>, Shape)>> {
1066 let Some(NodeData::Edge(data)) = model.node(edge).map(|n| n.data()) else {
1067 return Ok(None);
1068 };
1069 let data = data.clone();
1070 if data.degenerate {
1071 return Ok(None);
1072 }
1073 let Some(EdgeRepr::Curve3d {
1074 curve,
1075 location,
1076 range,
1077 }) = data.curve3d()
1078 else {
1079 return Ok(None);
1080 };
1081 if !location.is_identity() || !(range.0 < t && t < range.1) {
1082 return Ok(None);
1083 }
1084 let range = *range;
1085 let Some(geometry) = model.geometry().curve(*curve).cloned() else {
1086 return Ok(None);
1087 };
1088 let Some((start, end)) = ogeom_algo::edge_vertices(model, edge)? else {
1089 return Ok(None);
1090 };
1091 let at = geometry.point_at(t, tol)?;
1092 let reach = data.tolerance.get().max(tol.confusion());
1093 let mut cuts = Vec::new();
1094 for repr in &data.representations {
1095 match repr {
1096 EdgeRepr::PCurve {
1097 curve,
1098 surface,
1099 location,
1100 range,
1101 } => {
1102 if *surface == skip {
1103 continue;
1104 }
1105 if !location.is_identity() {
1106 return Ok(None);
1107 }
1108 let (Some(pcurve), Some(on)) = (
1109 model.geometry().pcurve(*curve),
1110 model.geometry().surface(*surface),
1111 ) else {
1112 return Ok(None);
1113 };
1114 let Some(s) = pcurve_parameter_at(on, pcurve, *range, at, reach, tol)? else {
1115 return Ok(None);
1116 };
1117 if !(range.0 < s && s < range.1) {
1118 return Ok(None);
1119 }
1120 cuts.push((pcurve.clone(), *surface, (range.0, s), (s, range.1)));
1121 }
1122 EdgeRepr::Seam { surface, .. } if *surface != skip => return Ok(None),
1123 _ => {}
1124 }
1125 }
1126 let vertex = model.add_vertex(ogeom_topo::VertexData::new(at));
1127 let head =
1128 ogeom_algo::make_edge_between(model, geometry.clone(), (range.0, t), &start, &vertex, tol)?
1129 .shape;
1130 let tail =
1131 ogeom_algo::make_edge_between(model, geometry, (t, range.1), &vertex, &end, tol)?.shape;
1132 for (pcurve, surface, first, second) in cuts {
1133 let identity = ogeom_topo::Location::identity();
1134 ogeom_algo::attach_pcurve(
1135 model,
1136 &head,
1137 pcurve.clone(),
1138 surface,
1139 identity.clone(),
1140 first,
1141 )?;
1142 ogeom_algo::attach_pcurve(model, &tail, pcurve, surface, identity, second)?;
1143 }
1144 model.widen(&head, data.tolerance)?;
1145 model.widen(&tail, data.tolerance)?;
1146 Ok(Some((vec![head, tail], vertex)))
1147}
1148
1149fn pcurve_parameter_at(
1153 surface: &SurfaceGeometry,
1154 pcurve: &PlanarCurve,
1155 range: (f64, f64),
1156 p: Point,
1157 reach: f64,
1158 tol: Tolerances,
1159) -> OgeomResult<Option<f64>> {
1160 let gap = |s: f64| -> OgeomResult<f64> {
1161 let uv = pcurve.point_at(s, tol)?;
1162 Ok(surface.point_at(uv.x, uv.y, tol)?.distance(p))
1163 };
1164 #[allow(clippy::cast_precision_loss)]
1165 let step = |k: usize| range.0 + (range.1 - range.0) * k as f64 / FIT_SAMPLES as f64;
1166 let mut nearest = (f64::INFINITY, 0);
1167 for k in 0..=FIT_SAMPLES {
1168 let d = gap(step(k))?;
1169 if d < nearest.0 {
1170 nearest = (d, k);
1171 }
1172 }
1173 let (mut lo, mut hi) = (
1174 step(nearest.1.saturating_sub(1)),
1175 step((nearest.1 + 1).min(FIT_SAMPLES)),
1176 );
1177 let golden = (5.0_f64.sqrt() - 1.0) / 2.0;
1178 for _ in 0..100 {
1179 let (a, b) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1180 if gap(a)? < gap(b)? {
1181 hi = b;
1182 } else {
1183 lo = a;
1184 }
1185 }
1186 let s = f64::midpoint(lo, hi);
1187 Ok((gap(s)? <= reach).then_some(s))
1188}
1189
1190fn restated(
1195 model: &mut Model,
1196 face: &Shape,
1197 pinned: &Pinned,
1198 axis: Vector,
1199 clearance: &dyn Fn(Point) -> f64,
1200 known: Option<[bool; 2]>,
1201 tol: Tolerances,
1202) -> OgeomResult<Option<(Shape, Vec<Split>)>> {
1203 use core::f64::consts::{FRAC_PI_2, TAU};
1204 use ogeom_algo::Containment;
1205 let (centre, radius) = (pinned.sphere.centre(), pinned.sphere.radius());
1206 let deflection = ogeom_mesh::Deflection::default();
1207 let mut inside = [false; 2];
1208 if let Some(known) = known {
1209 inside = known;
1210 } else {
1211 for (k, sign) in [1.0, -1.0].into_iter().enumerate() {
1212 let pole = centre + axis * (radius * sign);
1213 match ogeom_algo::classify_on_face(model, face, pole, deflection, tol)? {
1214 Containment::In => inside[k] = true,
1215 Containment::Out => {}
1216 Containment::On => return Ok(None),
1217 }
1218 }
1219 }
1220 let axis = if inside == [false, true] { -axis } else { axis };
1222 let poles_in = inside.iter().filter(|&&i| i).count();
1223 let reference = pinned.sphere.frame();
1224 let Ok(across) = axis
1225 .cross(reference.x().vector())
1226 .normalized(tol)
1227 .or_else(|_| axis.cross(reference.y().vector()).normalized(tol))
1228 else {
1229 return Ok(None);
1230 };
1231 let provisional = Frame::new(
1232 centre,
1233 Direction::new(axis, tol)?,
1234 Direction::new(across, tol)?,
1235 tol,
1236 )?;
1237 let mut points: Vec<Vec<Point>> = Vec::with_capacity(pinned.rings.len());
1238 for ring in &pinned.rings {
1239 points.push(ring_points(model, ring, tol)?);
1240 }
1241 let turns: Vec<i64> = points
1242 .iter()
1243 .map(|r| winding(&longitudes(&provisional, r)))
1244 .collect();
1245 let around: Vec<usize> = (0..turns.len()).filter(|&i| turns[i] != 0).collect();
1246 let (seam_u, through) = if poles_in == 1 {
1250 let [outer] = around.as_slice() else {
1251 return Ok(None);
1252 };
1253 let outer = *outer;
1254 if turns[outer].abs() != 1 {
1255 return Ok(None);
1256 }
1257 let mut found = None;
1258 for k in 0..pinned.rings[outer].len() {
1259 let mut ring = pinned.rings[outer].clone();
1262 ring.rotate_left(k);
1263 let us = longitudes(&provisional, &ring_points(model, &ring, tol)?);
1264 let Some((&first, rest)) = us.split_first() else {
1265 return Ok(None);
1266 };
1267 let inner = &rest[..rest.len().saturating_sub(1)];
1268 let (lo, hi) = span(inner);
1269 let (below, above) = if turns[outer] > 0 {
1270 (first, first + TAU)
1271 } else {
1272 (first - TAU, first)
1273 };
1274 let alone = lo > below + OFF_SEAM && hi < above - OFF_SEAM;
1275 let clear = alone
1276 && points.iter().enumerate().all(|(i, r)| {
1277 if i == outer {
1278 return true;
1279 }
1280 let (lo, hi) = span(&longitudes(&provisional, r));
1281 !reaches(lo, hi, first)
1282 });
1283 if clear {
1284 found = Some((first, Some((outer, k))));
1285 break;
1286 }
1287 }
1288 let Some(found) = found else {
1289 return Ok(None);
1290 };
1291 found
1292 } else if poles_in == 0 && !around.is_empty() {
1293 let [first, second] = around.as_slice() else {
1297 return Ok(None);
1298 };
1299 if turns[*first].abs() != 1 || turns[*second] != -turns[*first] {
1300 return Ok(None);
1301 }
1302 let carried: Vec<Vec<f64>> = points.iter().map(|r| longitudes(&provisional, r)).collect();
1303 let mut best: Option<(f64, f64)> = None;
1304 for k in 0..BAND_SEAMS {
1305 #[allow(clippy::cast_precision_loss)]
1306 let u = TAU * k as f64 / BAND_SEAMS as f64;
1307 let mut latitudes = Vec::with_capacity(2);
1308 let mut fits = true;
1309 for (i, us) in carried.iter().enumerate() {
1310 if !around.contains(&i) {
1311 let (lo, hi) = span(us);
1312 fits &= !reaches(lo, hi, u);
1313 continue;
1314 }
1315 match crossed_once(us, u) {
1316 Some(at) => latitudes.push(chart_of(&provisional, points[i][at]).1),
1317 None => fits = false,
1318 }
1319 }
1320 let [a, b] = latitudes.as_slice() else {
1321 continue;
1322 };
1323 if !fits {
1324 continue;
1325 }
1326 let meridian = across * u.cos() + axis.cross(across) * u.sin();
1327 let score = (0..=BAND_PROBES)
1328 .map(|j| {
1329 #[allow(clippy::cast_precision_loss)]
1330 let v = a + (b - a) * j as f64 / BAND_PROBES as f64;
1331 clearance(centre + (meridian * v.cos() + axis * v.sin()) * radius)
1332 })
1333 .fold(f64::INFINITY, f64::min);
1334 if best.is_none_or(|(s, _)| score > s) {
1335 best = Some((score, u));
1336 }
1337 }
1338 let Some((_, u)) = best else {
1339 return Ok(None);
1340 };
1341 (u, None)
1342 } else {
1343 if !around.is_empty() {
1344 return Ok(None);
1345 }
1346 let mut spans: Vec<(f64, f64)> = points
1348 .iter()
1349 .map(|r| {
1350 let (lo, hi) = span(&longitudes(&provisional, r));
1351 let start = lo.rem_euclid(TAU);
1352 (start, start + (hi - lo))
1353 })
1354 .collect();
1355 spans.sort_by(|a, b| a.0.total_cmp(&b.0));
1356 let Some(&(first, _)) = spans.first() else {
1357 return Ok(None);
1358 };
1359 let mut best: Option<(f64, f64)> = None;
1360 let mut reach = f64::NEG_INFINITY;
1361 for &(lo, hi) in &spans {
1362 if lo > reach && reach > f64::NEG_INFINITY && best.is_none_or(|(w, _)| lo - reach > w) {
1363 best = Some((lo - reach, f64::midpoint(lo, reach)));
1364 }
1365 reach = reach.max(hi);
1366 }
1367 let wrap = first + TAU - reach;
1368 if best.is_none_or(|(w, _)| wrap > w) {
1369 best = Some((wrap, f64::midpoint(reach, first + TAU)));
1370 }
1371 let Some((width, middle)) = best else {
1372 return Ok(None);
1373 };
1374 if width <= CLEAR_OF_TRIM {
1375 return Ok(None);
1376 }
1377 (middle, None)
1378 };
1379 let seam_x = across * seam_u.cos() + axis.cross(across) * seam_u.sin();
1380 let chart = Frame::new(
1381 centre,
1382 Direction::new(axis, tol)?,
1383 Direction::new(seam_x, tol)?,
1384 tol,
1385 )?;
1386 let sphere = ogeom_math::Sphere::new(chart, radius, tol)?;
1387 let geometry: SurfaceGeometry = ogeom_geom::SphereSurface::new(sphere).into();
1388 let surface = model.geometry_mut().add_surface(geometry.clone());
1389
1390 let mut rings = pinned.rings.clone();
1393 let mut splits: Vec<Split> = Vec::new();
1394 let mut band_ends: Vec<(usize, Shape, f64)> = Vec::new();
1395 if poles_in == 0 && !around.is_empty() {
1396 let Some(old_surface) = face_surface(model, face) else {
1397 return Ok(None);
1398 };
1399 for &i in &around {
1400 let Some((ring, vertex, cut)) =
1401 started_on_seam(model, &rings[i], &chart, old_surface, tol)?
1402 else {
1403 return Ok(None);
1404 };
1405 let Some(NodeData::Vertex(v)) = model.node(&vertex).map(|n| n.data()) else {
1406 return Ok(None);
1407 };
1408 band_ends.push((i, vertex.clone(), chart_of(&chart, v.point).1));
1409 rings[i] = ring;
1410 splits.extend(cut);
1411 }
1412 }
1413
1414 let mut wires: Vec<(Vec<Shape>, f64)> = Vec::new();
1418 let mut seam_ends: Option<(f64, f64)> = None;
1419 for (i, ring) in rings.iter().enumerate() {
1420 let mut ring = ring.clone();
1421 if let Some((outer, k)) = through
1422 && outer == i
1423 {
1424 ring.rotate_left(k);
1425 }
1426 let mut samples: Vec<Vec<(f64, Point)>> = Vec::with_capacity(ring.len());
1427 for edge in &ring {
1428 samples.push(edge_samples(model, edge, FIT_SAMPLES, tol)?);
1429 }
1430 let flat: Vec<Point> = samples.iter().flatten().map(|(_, p)| *p).collect();
1431 let us = longitudes(&chart, &flat);
1432 let (Some(&first), Some(&last)) = (us.first(), us.last()) else {
1433 return Ok(None);
1434 };
1435 let turn = winding(&us);
1436 let shift = match turn {
1437 0 => -TAU * (span(&us).0 / TAU).floor(),
1438 1 => -first,
1439 -1 => TAU - first,
1440 _ => return Ok(None),
1441 };
1442 if turn != 0 {
1443 seam_ends = Some((first + shift, last + shift));
1444 }
1445 let mut area = 0.0;
1446 let mut at = 0;
1447 for (edge, along) in ring.iter().zip(&samples) {
1448 let mut params = Vec::with_capacity(along.len());
1449 let mut image_points = Vec::with_capacity(along.len());
1450 for (j, &(t, p)) in along.iter().enumerate() {
1451 params.push(t);
1452 image_points.push(Point2::new(us[at + j] + shift, chart_of(&chart, p).1));
1453 }
1454 at += along.len();
1455 for w in image_points.windows(2) {
1456 area += (w[0].x * w[1].y - w[1].x * w[0].y) / 2.0;
1457 }
1458 if edge.orientation() == Orientation::Reversed {
1459 params.reverse();
1460 image_points.reverse();
1461 }
1462 let (Some(&lo), Some(&hi)) = (params.first(), params.last()) else {
1463 return Ok(None);
1464 };
1465 let fitted =
1466 ogeom_geom::fit::fit_points_2d_at(¶ms, &image_points, 3, FIT_IN_CHART, tol)?;
1467 let image: PlanarCurve = fitted.curve.into();
1468 let Some(NodeData::Edge(e)) = model.node(edge).map(|n| n.data()) else {
1470 return Ok(None);
1471 };
1472 let Some(EdgeRepr::Curve3d { curve, .. }) = e.curve3d() else {
1473 return Ok(None);
1474 };
1475 let Some(curve) = model.geometry().curve(*curve) else {
1476 return Ok(None);
1477 };
1478 let budget = e.tolerance.get().max(tol.confusion()) * 0.1;
1479 for pair in params.windows(2) {
1480 let t = f64::midpoint(pair[0], pair[1]);
1481 let uv = image.point_at(t, tol)?;
1482 let Ok(on) = geometry.point_at(uv.x, uv.y, tol) else {
1483 return Ok(None);
1484 };
1485 if on.distance(curve.point_at(t, tol)?) > budget {
1486 return Ok(None);
1487 }
1488 }
1489 ogeom_algo::attach_pcurve(
1490 model,
1491 edge,
1492 image,
1493 surface,
1494 ogeom_topo::Location::identity(),
1495 (lo, hi),
1496 )?;
1497 }
1498 wires.push((ring, area));
1499 }
1500
1501 let (outer, holes): (Vec<Shape>, Vec<Vec<Shape>>) = match (poles_in, through) {
1504 (0, None) if !band_ends.is_empty() => {
1505 let [low, high] = band_ends.as_slice() else {
1506 return Ok(None);
1507 };
1508 let (low, high) = if low.2 <= high.2 {
1509 (low, high)
1510 } else {
1511 (high, low)
1512 };
1513 let sides = if turns[low.0] > 0 {
1515 (TAU, 0.0)
1516 } else {
1517 (0.0, TAU)
1518 };
1519 let seam = seam_between(
1520 model,
1521 surface,
1522 &chart,
1523 radius,
1524 (&low.1, low.2),
1525 (&high.1, high.2),
1526 sides,
1527 tol,
1528 )?;
1529 let mut edges = wires[low.0].0.clone();
1530 edges.push(seam.clone());
1531 edges.extend(wires[high.0].0.iter().cloned());
1532 edges.push(seam.reversed());
1533 let holes = wires
1534 .into_iter()
1535 .enumerate()
1536 .filter(|(i, _)| *i != low.0 && *i != high.0)
1537 .map(|(_, (w, _))| w)
1538 .collect();
1539 (edges, holes)
1540 }
1541 (1, Some((ring_index, _))) => {
1542 let Some((u_start, u_end)) = seam_ends else {
1543 return Ok(None);
1544 };
1545 let ring = wires[ring_index].0.clone();
1546 let Some((start, _)) = ogeom_algo::edge_vertices(model, &ring[0])? else {
1547 return Ok(None);
1548 };
1549 let Some(NodeData::Vertex(v)) = model.node(&start).map(|n| n.data()) else {
1550 return Ok(None);
1551 };
1552 let low = chart_of(&chart, v.point).1;
1553 let north = model.add_vertex(ogeom_topo::VertexData::new(centre + axis * radius));
1554 let seam = seam_between(
1555 model,
1556 surface,
1557 &chart,
1558 radius,
1559 (&start, low),
1560 (&north, FRAC_PI_2),
1561 (u_end, u_start),
1562 tol,
1563 )?;
1564 let top = pole_at(model, surface, &north, FRAC_PI_2, tol)?;
1565 let mut edges = ring;
1566 edges.push(seam.clone());
1567 edges.push(if u_end > u_start { top.reversed() } else { top });
1570 edges.push(seam.reversed());
1571 let holes = wires
1572 .into_iter()
1573 .enumerate()
1574 .filter(|(i, _)| *i != ring_index)
1575 .map(|(_, (w, _))| w)
1576 .collect();
1577 (edges, holes)
1578 }
1579 (2, None) => {
1580 let Some(hole_area) = wires.first().map(|(_, a)| *a) else {
1581 return Ok(None);
1582 };
1583 let ccw = hole_area < 0.0;
1586 let south = model.add_vertex(ogeom_topo::VertexData::new(centre - axis * radius));
1587 let north = model.add_vertex(ogeom_topo::VertexData::new(centre + axis * radius));
1588 let sides = if ccw { (TAU, 0.0) } else { (0.0, TAU) };
1589 let seam = seam_between(
1590 model,
1591 surface,
1592 &chart,
1593 radius,
1594 (&south, -FRAC_PI_2),
1595 (&north, FRAC_PI_2),
1596 sides,
1597 tol,
1598 )?;
1599 let top = pole_at(model, surface, &north, FRAC_PI_2, tol)?;
1600 let bottom = pole_at(model, surface, &south, -FRAC_PI_2, tol)?;
1601 let edges = if ccw {
1602 vec![bottom, seam.clone(), top.reversed(), seam.reversed()]
1603 } else {
1604 vec![seam.clone(), top, seam.reversed(), bottom.reversed()]
1605 };
1606 (edges, wires.into_iter().map(|(w, _)| w).collect())
1607 }
1608 (0, None) => {
1609 let Some(widest) =
1610 (0..wires.len()).max_by(|&a, &b| wires[a].1.abs().total_cmp(&wires[b].1.abs()))
1611 else {
1612 return Ok(None);
1613 };
1614 let mut outer = Vec::new();
1615 let mut holes = Vec::new();
1616 for (i, (w, _)) in wires.into_iter().enumerate() {
1617 if i == widest {
1618 outer = w;
1619 } else {
1620 holes.push(w);
1621 }
1622 }
1623 (outer, holes)
1624 }
1625 _ => return Ok(None),
1626 };
1627 let mut made_wires = vec![ogeom_algo::make_wire(model, &outer, tol)?.shape];
1628 for hole in &holes {
1629 made_wires.push(ogeom_algo::make_wire(model, hole, tol)?.shape);
1630 }
1631 let Some(NodeData::Face(old)) = model.node(face).map(|n| n.data()) else {
1632 return Ok(None);
1633 };
1634 let held = old.tolerance;
1635 let data = ogeom_topo::FaceData::new(surface, ogeom_topo::Location::identity());
1636 let made = model.add_face(data, &made_wires)?;
1637 model.widen(&made, held)?;
1638 Ok(Some((made.oriented(face.orientation()), splits)))
1639}
1640
1641fn pole_at(
1644 model: &mut Model,
1645 surface: ogeom_topo::SurfaceId,
1646 at: &Shape,
1647 v: f64,
1648 tol: Tolerances,
1649) -> OgeomResult<Shape> {
1650 let mut data = ogeom_topo::EdgeData::new();
1651 data.degenerate = true;
1652 let edge = model.add_edge(data, &[at.clone(), at.clone()])?;
1653 let (from, to) = (Point2::new(0.0, v), Point2::new(core::f64::consts::TAU, v));
1654 ogeom_algo::attach_pcurve(
1655 model,
1656 &edge,
1657 ogeom_geom::Line2d::segment(from, to, tol)?.into(),
1658 surface,
1659 ogeom_topo::Location::identity(),
1660 (0.0, from.distance(to)),
1661 )?;
1662 Ok(edge)
1663}
1664
1665#[allow(clippy::too_many_arguments)]
1670fn seam_between(
1671 model: &mut Model,
1672 surface: ogeom_topo::SurfaceId,
1673 chart: &Frame,
1674 radius: f64,
1675 low: (&Shape, f64),
1676 high: (&Shape, f64),
1677 sides: (f64, f64),
1678 tol: Tolerances,
1679) -> OgeomResult<Shape> {
1680 let ((from, bottom), (to, top)) = (low, high);
1681 let meridian = ogeom_math::Circle::new(
1684 Frame::new(chart.origin(), -chart.y(), chart.x(), tol)?,
1685 radius,
1686 tol,
1687 )?;
1688 let seam = ogeom_algo::make_edge_between(
1689 model,
1690 ogeom_geom::CircleCurve::new(meridian).into(),
1691 (bottom, top),
1692 from,
1693 to,
1694 tol,
1695 )?
1696 .shape;
1697 let side = |u: f64| -> OgeomResult<PlanarCurve> {
1698 Ok(ogeom_geom::Line2d::segment(Point2::new(u, bottom), Point2::new(u, top), tol)?.into())
1699 };
1700 ogeom_algo::attach_seam(
1701 model,
1702 &seam,
1703 side(sides.0)?,
1704 side(sides.1)?,
1705 surface,
1706 ogeom_topo::Location::identity(),
1707 (0.0, top - bottom),
1708 )?;
1709 Ok(seam)
1710}
1711
1712pub const POLE_MARGIN: f64 = 0.05;
1718
1719#[derive(Debug, Clone, PartialEq)]
1721pub enum Recharted {
1722 Untouched,
1725 Restated,
1728 Refused(String),
1731}
1732
1733pub fn rechart_clear_of_poles(
1746 model: &mut Model,
1747 face: &Shape,
1748 tol: Tolerances,
1749) -> OgeomResult<Recharted> {
1750 let bare = face
1751 .located(ogeom_topo::Location::identity())
1752 .oriented(Orientation::Forward);
1753 let Some(NodeData::Face(data)) = model.node(&bare).map(|n| n.data()) else {
1754 return Ok(Recharted::Untouched);
1755 };
1756 if !matches!(
1757 model.geometry().surface(data.surface),
1758 Some(SurfaceGeometry::Sphere(_))
1759 ) {
1760 return Ok(Recharted::Untouched);
1761 }
1762 let Some(pinned) = pinned_sphere_with(model, &bare, false)? else {
1763 return Ok(Recharted::Untouched);
1764 };
1765 if pinned.sphere.frame().handedness() != ogeom_math::Handedness::Right {
1766 return Ok(Recharted::Untouched);
1767 }
1768 let (centre, radius) = (pinned.sphere.centre(), pinned.sphere.radius());
1769 let mut trim: Vec<Point> = Vec::new();
1770 let mut corners: Vec<Point> = Vec::new();
1771 for ring in &pinned.rings {
1772 trim.extend(ring_points(model, ring, tol)?);
1773 for edge in ring {
1774 if let Some((start, _)) = ogeom_algo::edge_vertices(model, edge)?
1775 && let Some(NodeData::Vertex(v)) = model.node(&start).map(|n| n.data())
1776 {
1777 corners.push(v.point);
1778 }
1779 }
1780 }
1781 let angle = |p: Point, axis: Vector| -> f64 {
1783 let d = p - centre;
1784 let length = d.magnitude();
1785 if length <= 0.0 {
1786 return 0.0;
1787 }
1788 (d.dot(axis) / length).clamp(-1.0, 1.0).acos()
1789 };
1790 let nearest = |axis: Vector| -> f64 {
1791 trim.iter()
1792 .map(|&p| angle(p, axis).min(angle(p, -axis)))
1793 .fold(f64::INFINITY, f64::min)
1794 };
1795 let reach = tol.confusion().max(data.tolerance.get());
1798 let z = pinned.sphere.frame().z().vector();
1799 let mut beside = f64::INFINITY;
1800 for pole_axis in [z, -z] {
1801 let pole = centre + pole_axis * radius;
1802 let through = pinned.poles.iter().any(|p| p.distance(pole) <= reach)
1803 || corners.iter().any(|p| p.distance(pole) <= reach);
1804 if through {
1805 continue;
1806 }
1807 let near = trim
1808 .iter()
1809 .map(|&p| angle(p, pole_axis))
1810 .fold(f64::INFINITY, f64::min);
1811 beside = beside.min(near);
1812 }
1813 if beside > POLE_MARGIN {
1814 return Ok(Recharted::Untouched);
1815 }
1816 let frame = pinned.sphere.frame();
1817 let mut scored: Vec<(f64, Vector)> = Vec::new();
1818 for candidate in candidate_axes(frame.x().vector(), z) {
1819 let Ok(axis) = candidate.normalized(tol) else {
1820 continue;
1821 };
1822 let score = nearest(axis);
1823 if score > CLEAR_OF_TRIM {
1824 scored.push((score, axis));
1825 }
1826 }
1827 scored.sort_by(|a, b| b.0.total_cmp(&a.0));
1828 let near_trim = |p: Point| -> f64 {
1829 trim.iter()
1830 .map(|q| q.distance(p))
1831 .fold(f64::INFINITY, f64::min)
1832 };
1833 let mut ring_samples: Vec<Vec<Point>> = Vec::with_capacity(pinned.rings.len());
1834 for ring in &pinned.rings {
1835 ring_samples.push(ring_points(model, ring, tol)?);
1836 }
1837 for (_, axis) in scored {
1838 let Some(known) = poles_inside(centre, axis, frame.x().vector(), &ring_samples, tol) else {
1839 continue;
1840 };
1841 let Some((made, cut)) =
1842 restated(model, &bare, &pinned, axis, &near_trim, Some(known), tol)?
1843 else {
1844 continue;
1845 };
1846 if !cut.is_empty() {
1850 continue;
1851 }
1852 let Some(NodeData::Face(new)) = model.node(&made).map(|n| n.data()) else {
1853 continue;
1854 };
1855 let surface = new.surface;
1856 let wires = model.children_of(&made.oriented(Orientation::Forward))?;
1857 model.rechart_face(&bare, surface, &wires)?;
1858 return Ok(Recharted::Restated);
1859 }
1860 Ok(Recharted::Refused(format!(
1861 "a sphere face's trim passes {beside:.3e} rad from a pole of its chart, and no chart \
1862 of its sphere about another axis keeps the trim {CLEAR_OF_TRIM} rad clear of its \
1863 poles; see docs/CAPABILITIES.md, bool.booleans"
1864 )))
1865}
1866
1867fn poles_inside(
1881 centre: Point,
1882 axis: Vector,
1883 reference: Vector,
1884 rings: &[Vec<Point>],
1885 tol: Tolerances,
1886) -> Option<[bool; 2]> {
1887 let across = axis
1888 .cross(reference)
1889 .normalized(tol)
1890 .or_else(|_| {
1891 axis.cross(axis.cross(reference) + reference)
1892 .normalized(tol)
1893 })
1894 .ok()?;
1895 let frame = Frame::new(
1896 centre,
1897 Direction::new(axis, tol).ok()?,
1898 Direction::new(across, tol).ok()?,
1899 tol,
1900 )
1901 .ok()?;
1902 let mut inside = [true, true];
1903 for ring in rings {
1904 let us = longitudes(&frame, ring);
1905 match winding(&us) {
1906 1 => inside[1] = false,
1907 -1 => inside[0] = false,
1908 0 => {
1909 let vs: Vec<f64> = ring.iter().map(|&p| chart_of(&frame, p).1).collect();
1910 let n = us.len();
1911 let area: f64 = (0..n)
1912 .map(|i| {
1913 let j = (i + 1) % n;
1914 us[i] * vs[j] - us[j] * vs[i]
1915 })
1916 .sum();
1917 if area > 0.0 {
1918 inside = [false, false];
1919 }
1920 }
1921 _ => return None,
1922 }
1923 }
1924 Some(inside)
1925}