1use ogeom_core::{FastMap, FastSet};
29
30use ogeom_algo::{Built, edge_vertices, project_on_surface, project_on_surface_from};
31use ogeom_core::{OgeomResult, Tolerance, Tolerances, ogeom_bail, ogeom_err};
32use ogeom_geom::{
33 Curve, Curve2d as _, Curve3d as _, PlanarCurve, Surface as _, SurfaceGeometry,
34 Transformable as _,
35};
36use ogeom_math::{Direction, Point, Point2, Transform2, Vector2};
37use ogeom_topo::{
38 EdgeData, EdgeRepr, Location, Model, NodeData, Shape, ShapeType, SurfaceId, TShapeId,
39 VertexData, explore_unique,
40};
41
42use crate::Reshape;
43use crate::divide::{
44 Bounds, Occurrence, bounded_natural, chart_bounds, edge_data, face_data, forward, inside,
45 placed_baked, rings, signed_area, split_edge, vertex_point,
46};
47
48#[derive(Debug, Clone, Copy, PartialEq)]
50pub enum Projection {
51 OnFace,
53 AlongNormals,
55 Along(Direction),
58}
59
60pub fn split_face(
85 model: &mut Model,
86 shape: &Shape,
87 face: &Shape,
88 edges: &[Shape],
89 projection: Projection,
90 tol: Tolerances,
91) -> OgeomResult<Built> {
92 if model.kind_of(face)? != ShapeType::Face {
93 ogeom_bail!(Construction, "only a face can be split along curves");
94 }
95 if !explore_unique(model, shape, ShapeType::Face)?
96 .iter()
97 .any(|f| f.node() == face.node())
98 {
99 ogeom_bail!(Construction, "the face to split is not a face of the shape");
100 }
101 let mut sources: Vec<Shape> = Vec::new();
102 for given in edges {
103 for edge in explore_unique(model, given, ShapeType::Edge)? {
104 if !edge_data(model, &edge)?.degenerate {
105 sources.push(edge);
106 }
107 }
108 }
109 if sources.is_empty() {
110 ogeom_bail!(Construction, "a face is split along at least one edge");
111 }
112
113 let root = if model.kind_of(shape)? == ShapeType::Face {
115 model.add_shell(std::slice::from_ref(shape))?
116 } else {
117 shape.clone()
118 };
119 let start = placed_baked(model, &root, tol)?;
120 let mut current = Built::new(start.shape.clone(), start.history);
121 let Some(mut target) = current.history.trace(face).first().cloned() else {
122 ogeom_bail!(Construction, "the face to split is lost in its placement");
123 };
124 if face_data(model, &target)?.natural_restriction {
125 let Some(bounded) = bounded_natural(model, &target, tol)? else {
126 ogeom_bail!(
127 Construction,
128 "a face covering an unbounded surface has no boundary to cut to"
129 );
130 };
131 let mut reshape = Reshape::new();
132 reshape.replace(&forward(&target), bounded.clone());
133 let next = reshape.apply(model, ¤t.shape)?;
134 current = Built::new(next.shape, current.history.then(&next.history));
135 target = bounded;
136 }
137 let data = face_data(model, &target)?;
138 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
139 ogeom_bail!(Dangling, "surface is not in this model");
140 };
141 ogeom_algo::build::trimmed_where_bare(model, &target, tol)?;
142 let window = chart_bounds(&rings(model, &forward(&target), data.surface, tol)?, tol);
143
144 let mut segments: Vec<Segment> = Vec::new();
145 for source in &sources {
146 let found = segments_of(model, source, &surface, data.surface, projection, tol)?;
147 if found.is_empty() {
148 ogeom_bail!(Construction, "the projection of an edge misses the face");
149 }
150 for segment in found {
151 segments.extend(unfold(segment, &surface, window, tol)?);
152 }
153 }
154 let mut cutters = chain(segments, &surface, tol)?;
155
156 let mut targets: Vec<Shape> = vec![target];
157 let mut pending: Vec<usize> = (0..cutters.len()).collect();
158 loop {
159 let mut progressed = false;
160 let mut held: Vec<usize> = Vec::new();
161 for &k in &pending {
162 let dangling = loop {
163 let mut found = None;
164 let mut dangling = false;
165 for face in &targets {
166 match next_stretch(model, face, &cutters[k], &surface, tol)? {
167 Found::Nothing => {}
168 Found::Dangling => dangling = true,
169 Found::Rebase(t) => {
170 found = Some(Next::Rebase(t));
171 break;
172 }
173 Found::Cut(stretch) => {
174 found = Some(Next::Cut(face.clone(), Box::new(stretch)));
175 break;
176 }
177 }
178 }
179 let (face, stretch) = match found {
180 None => break dangling,
181 Some(Next::Rebase(t)) => {
182 cutters[k] = cutters[k].rebased(t, tol)?;
183 continue;
184 }
185 Some(Next::Cut(face, stretch)) => (face, *stretch),
186 };
187 let made = cut(model, &face, &cutters[k], &stretch, tol)?;
188 let mut step = made.reshape.apply(model, ¤t.shape)?;
189 for (source, edge) in &made.edges {
190 step.history.generate(source, edge.clone());
191 }
192 let mut next_targets = Vec::new();
193 for t in &targets {
194 next_targets.extend(step.history.trace(t).iter().cloned());
195 }
196 targets = next_targets;
197 for cutter in &mut cutters {
198 for (old, pieces) in &made.splits {
199 if cutter.own.remove(old) {
200 cutter.own.extend(pieces.iter().map(Shape::node));
201 }
202 }
203 }
204 cutters[k]
205 .own
206 .extend(made.edges.iter().map(|(_, e)| e.node()));
207 cutters[k].consumed.push(stretch.span);
208 current = Built::new(step.shape, current.history.then(&step.history));
209 progressed = true;
210 };
211 if dangling {
212 held.push(k);
213 } else if cutters[k].consumed.is_empty() {
214 ogeom_bail!(
215 Construction,
216 "a curve to split along does not cross the face"
217 );
218 }
219 }
220 if held.is_empty() {
221 break;
222 }
223 if !progressed {
224 ogeom_bail!(
225 Construction,
226 "a curve to split along ends inside the face; it must run from \
227 boundary to boundary"
228 );
229 }
230 pending = held;
231 }
232 let current = join_across_seams(model, current, &targets, data.surface, tol)?;
233 let held: ogeom_core::FastSet<_> = explore_unique(model, shape, ShapeType::Edge)?
239 .iter()
240 .map(Shape::node)
241 .collect();
242 let made: Vec<Shape> = explore_unique(model, ¤t.shape, ShapeType::Edge)?
243 .into_iter()
244 .filter(|e| !held.contains(&e.node()))
245 .collect();
246 ogeom_algo::state_pcurve_gaps_of(model, &made, tol)?;
247 Ok(current)
248}
249
250enum Next {
252 Rebase(f64),
253 Cut(Shape, Box<Planned>),
254}
255
256#[derive(Debug, Clone)]
261struct Segment {
262 source: Shape,
264 curve: Curve,
265 pcurve: PlanarCurve,
266 range: (f64, f64),
268 backwards: bool,
270 tolerance: f64,
272}
273
274impl Segment {
275 fn local(&self, f: f64) -> f64 {
277 let (a, b) = if self.backwards {
278 (self.range.1, self.range.0)
279 } else {
280 self.range
281 };
282 (b - a).mul_add(f, a)
283 }
284
285 fn chart(&self, f: f64, tol: Tolerances) -> OgeomResult<Point2> {
286 self.pcurve.point_at(self.local(f), tol)
287 }
288
289 fn point(&self, f: f64, tol: Tolerances) -> OgeomResult<Point> {
290 self.curve.point_at(self.local(f), tol)
291 }
292
293 fn part(&self, f0: f64, f1: f64) -> Self {
295 let (a, b) = (self.local(f0), self.local(f1));
296 Self {
297 range: (a.min(b), a.max(b)),
298 ..self.clone()
299 }
300 }
301
302 fn reversed(mut self) -> Self {
303 self.backwards = !self.backwards;
304 self
305 }
306
307 fn shifted(mut self, by: Vector2, tol: Tolerances) -> OgeomResult<Self> {
309 if by.x != 0.0 || by.y != 0.0 {
310 self.pcurve = self.pcurve.transformed(&Transform2::translation(by), tol)?;
311 }
312 Ok(self)
313 }
314}
315
316struct Cutter {
319 path: Vec<Segment>,
320 closed: bool,
321 own: FastSet<TShapeId>,
324 consumed: Vec<(f64, f64)>,
326}
327
328impl Cutter {
329 #[expect(clippy::cast_precision_loss, reason = "a segment count")]
330 fn range(&self) -> (f64, f64) {
331 (0.0, self.path.len() as f64)
332 }
333
334 fn locate(&self, t: f64) -> (&Segment, f64) {
336 #[expect(
337 clippy::cast_possible_truncation,
338 clippy::cast_sign_loss,
339 reason = "a parameter clamped to the segment count"
340 )]
341 let k = (t.floor().max(0.0) as usize).min(self.path.len() - 1);
342 #[expect(clippy::cast_precision_loss, reason = "a segment index")]
343 let f = t - k as f64;
344 (&self.path[k], f)
345 }
346
347 fn chart(&self, t: f64, tol: Tolerances) -> OgeomResult<Point2> {
348 let (s, f) = self.locate(t);
349 s.chart(f, tol)
350 }
351
352 fn point(&self, t: f64, tol: Tolerances) -> OgeomResult<Point> {
353 let (s, f) = self.locate(t);
354 s.point(f, tol)
355 }
356
357 fn chart_d1(&self, t: f64, tol: Tolerances) -> OgeomResult<Vector2> {
359 let (s, f) = self.locate(t);
360 let rate = if s.backwards {
361 s.range.0 - s.range.1
362 } else {
363 s.range.1 - s.range.0
364 };
365 Ok(s.pcurve.d1_at(s.local(f), tol)? * rate)
366 }
367
368 fn tolerance(&self) -> f64 {
369 self.path.iter().map(|s| s.tolerance).fold(0.0, f64::max)
370 }
371
372 fn resolution(&self, tol: Tolerances) -> f64 {
373 (self.range().1 - self.range().0) * 1e-9 + tol.parametric()
374 }
375
376 fn is_consumed(&self, a: f64, b: f64) -> bool {
377 let mid = 0.5 * (a + b);
378 self.consumed.iter().any(|(lo, hi)| mid > *lo && mid < *hi)
379 }
380
381 fn rebased(&self, t: f64, tol: Tolerances) -> OgeomResult<Self> {
383 #[expect(
384 clippy::cast_possible_truncation,
385 clippy::cast_sign_loss,
386 reason = "a parameter clamped to the segment count"
387 )]
388 let k = (t.floor().max(0.0) as usize).min(self.path.len() - 1);
389 #[expect(clippy::cast_precision_loss, reason = "a segment index")]
390 let f = t - k as f64;
391 let eps = self.resolution(tol);
392 let mut path = Vec::with_capacity(self.path.len() + 1);
393 if f < 1.0 - eps {
394 path.push(self.path[k].part(f, 1.0));
395 }
396 path.extend(self.path[k + 1..].iter().cloned());
397 path.extend(self.path[..k].iter().cloned());
398 if f > eps {
399 path.push(self.path[k].part(0.0, f));
400 }
401 Ok(Self {
402 path,
403 closed: false,
404 own: FastSet::default(),
405 consumed: Vec::new(),
406 })
407 }
408}
409
410fn segments_of(
412 model: &Model,
413 edge: &Shape,
414 surface: &SurfaceGeometry,
415 surface_id: SurfaceId,
416 projection: Projection,
417 tol: Tolerances,
418) -> OgeomResult<Vec<Segment>> {
419 let data = edge_data(model, edge)?;
420 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
421 ogeom_bail!(Construction, "an edge to split along has no curve in space");
422 };
423 let Some(geometry) = model.geometry().curve(curve).cloned() else {
424 ogeom_bail!(Dangling, "curve is not in this model");
425 };
426 let unplaced = edge.location().is_identity();
427 let geometry = if unplaced {
428 geometry
429 } else {
430 geometry.transformed(&edge.transform(model.datums())?, tol)?
431 };
432 let mut out = Vec::new();
433 match projection {
434 Projection::OnFace => {
435 let cap = data.tolerance.get().max(tol.confusion());
436 let mut worst: f64 = 0.0;
437 for k in 0..=32 {
438 let t = (range.1 - range.0).mul_add(f64::from(k) / 32.0, range.0);
439 let p = geometry.point_at(t, tol)?;
440 worst = worst.max(project_on_surface(surface, p, 24, tol)?.distance);
441 }
442 if worst > cap {
443 ogeom_bail!(
444 Construction,
445 "an edge to split along does not lie on the face: it stands \
446 {worst:.3e} off the surface; project it instead"
447 );
448 }
449 let own = if unplaced && data.same_parameter() {
450 match data.pcurve_for(surface_id, edge.location()) {
451 Some(EdgeRepr::PCurve {
452 curve, range: r, ..
453 }) if *r == range => model.geometry().pcurve(*curve).cloned(),
454 _ => None,
455 }
456 } else {
457 None
458 };
459 let exact =
460 own.or_else(|| ogeom_intersect::exact_pcurve_over(&geometry, range, surface, tol));
461 if exact.is_none()
462 && let Some(pieces) = through_poles(&geometry, range, surface, cap, tol)?
463 {
464 for (span, pcurve, fit) in pieces {
465 out.push(segment(
466 edge,
467 geometry.clone(),
468 pcurve,
469 span,
470 fit,
471 surface,
472 tol,
473 )?);
474 }
475 return Ok(out);
476 }
477 let (pcurve, fit) = match exact {
478 Some(p) => (p, 0.0),
479 None => {
480 let (p, error, _, off, _) =
481 ogeom_algo::pcurve_fit::fit_projected_pcurve_capped(
482 &geometry, range, surface, cap, tol,
483 )?;
484 (p, error.max(off))
485 }
486 };
487 out.push(segment(edge, geometry, pcurve, range, fit, surface, tol)?);
488 }
489 Projection::AlongNormals | Projection::Along(_) => {
490 for (span, seed) in landed_runs(&geometry, range, surface, projection, tol)? {
491 out.push(projected(
492 edge, &geometry, span, seed, surface, projection, tol,
493 )?);
494 }
495 }
496 }
497 Ok(out)
498}
499
500type Piece = ((f64, f64), PlanarCurve, f64);
503
504fn through_poles(
523 curve: &Curve,
524 range: (f64, f64),
525 surface: &SurfaceGeometry,
526 cap: f64,
527 tol: Tolerances,
528) -> OgeomResult<Option<Vec<Piece>>> {
529 const SAMPLES: u32 = 256;
530 let SurfaceGeometry::Sphere(sphere) = surface else {
531 return Ok(None);
532 };
533 let sphere = sphere.sphere();
534 let frame = sphere.frame();
535 let (x, y, z) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
536 let half = core::f64::consts::FRAC_PI_2;
537 let poles = [
538 (sphere.centre() + z * sphere.radius(), half),
539 (sphere.centre() - z * sphere.radius(), -half),
540 ];
541 let off = |t: f64| -> OgeomResult<(f64, usize)> {
542 let p = curve.point_at(t, tol)?;
543 let (n, s) = (p.distance(poles[0].0), p.distance(poles[1].0));
544 Ok(if n <= s { (n, 0) } else { (s, 1) })
545 };
546 let at = |k: u32| (range.1 - range.0).mul_add(f64::from(k) / f64::from(SAMPLES), range.0);
547 let distances = (0..=SAMPLES)
548 .map(|k| off(at(k)).map(|d| d.0))
549 .collect::<OgeomResult<Vec<_>>>()?;
550 let resolution = f64::EPSILON * range.0.abs().max(range.1.abs()).max(range.1 - range.0);
551 let ratio = (5.0_f64.sqrt() - 1.0) / 2.0;
552 let mut hits: Vec<(f64, usize)> = Vec::new();
553 for k in 0..=SAMPLES {
554 let i = usize::try_from(k).unwrap_or(0);
555 let left = if k == 0 {
556 f64::INFINITY
557 } else {
558 distances[i - 1]
559 };
560 let right = if k == SAMPLES {
561 f64::INFINITY
562 } else {
563 distances[i + 1]
564 };
565 if distances[i] > left || distances[i] > right {
566 continue;
567 }
568 let (mut a, mut b) = (at(k.saturating_sub(1)), at((k + 1).min(SAMPLES)));
569 while b - a > resolution {
570 let (c, d) = ((b - a).mul_add(-ratio, b), (b - a).mul_add(ratio, a));
571 if off(c)?.0 <= off(d)?.0 {
572 b = d;
573 } else {
574 a = c;
575 }
576 }
577 let mut t = f64::midpoint(a, b);
578 let (d, pole) = off(t)?;
579 let apart = resolution * f64::from(SAMPLES);
582 if t - range.0 <= apart {
583 t = range.0;
584 } else if range.1 - t <= apart {
585 t = range.1;
586 }
587 if d <= cap && hits.last().is_none_or(|h| t - h.0 > apart) {
588 hits.push((t, pole));
589 }
590 }
591 if hits.is_empty() {
592 return Ok(None);
593 }
594 let period = core::f64::consts::TAU;
595 let longitude = |v: ogeom_math::Vector| v.dot(y).atan2(v.dot(x));
596 let chart = |p: Point| {
597 let l = p - sphere.centre();
598 let (a, b, c) = (l.dot(x), l.dot(y), l.dot(z));
599 Point2::new(b.atan2(a), c.atan2(a.hypot(b)))
600 };
601 let mut cuts: Vec<f64> = vec![range.0];
602 cuts.extend(hits.iter().map(|h| h.0));
603 cuts.push(range.1);
604 let mut pieces = Vec::new();
605 for pair in cuts.windows(2) {
606 let (lo, hi) = (pair[0], pair[1]);
607 if hi <= lo {
608 continue;
609 }
610 let mut ends: Vec<(f64, Point2)> = Vec::new();
613 for &(t, pole) in &hits {
614 if t == lo {
615 ends.push((
616 t,
617 Point2::new(longitude(curve.d1_at(t, tol)?), poles[pole].1),
618 ));
619 } else if t == hi {
620 ends.push((
621 t,
622 Point2::new(longitude(-curve.d1_at(t, tol)?), poles[pole].1),
623 ));
624 }
625 }
626 let raw = |t: f64| -> OgeomResult<Point2> {
627 match ends.iter().find(|e| e.0 == t) {
628 Some(e) => Ok(e.1),
629 None => Ok(chart(curve.point_at(t, tol)?)),
630 }
631 };
632 let near = |uv: Point2, guide: Point2| {
633 Point2::new(
634 ((guide.x - uv.x) / period).round().mul_add(period, uv.x),
635 uv.y,
636 )
637 };
638 let ts: Vec<f64> = (0..=SAMPLES)
642 .map(|k| (hi - lo).mul_add(f64::from(k) / f64::from(SAMPLES), lo))
643 .collect();
644 let mut table: Vec<Point2> = Vec::with_capacity(ts.len());
645 for &t in &ts {
646 let uv = raw(t)?;
647 table.push(match table.last() {
648 Some(previous) => near(uv, *previous),
649 None => uv,
650 });
651 }
652 let continued = |t: f64| -> OgeomResult<Point2> {
653 let k = ts.partition_point(|s| *s <= t).clamp(1, ts.len() - 1);
654 let f = ((t - ts[k - 1]) / (ts[k] - ts[k - 1])).clamp(0.0, 1.0);
655 let guide = table[k - 1] + (table[k] - table[k - 1]) * f;
656 Ok(near(raw(t)?, guide))
657 };
658 let (fitted, image) = ogeom_geom::fit::fit_trace_sampled(
659 |t| Ok((curve.point_at(t, tol)?, continued(t)?)),
660 |uv| surface.point_at(uv.x, uv.y, tol),
661 &ts,
662 3,
663 cap,
664 tol,
665 )?;
666 if !fitted.met {
667 ogeom_bail!(
668 Construction,
669 "a curve to split along runs through the sphere's pole, and its trace \
670 there misses it by {:.3e}, over the {cap:.3e} it may",
671 fitted.error
672 );
673 }
674 pieces.push(((lo, hi), image.into(), fitted.error));
675 }
676 Ok(Some(pieces))
677}
678
679fn segment(
682 edge: &Shape,
683 curve: Curve,
684 pcurve: PlanarCurve,
685 range: (f64, f64),
686 fit: f64,
687 surface: &SurfaceGeometry,
688 tol: Tolerances,
689) -> OgeomResult<Segment> {
690 let mut gap = fit;
691 for k in 0..=64 {
692 let t = (range.1 - range.0).mul_add(f64::from(k) / 64.0, range.0);
693 let uv = pcurve.point_at(t, tol)?;
694 let on = surface.point_at(uv.x, uv.y, tol)?;
695 gap = gap.max(on.distance(curve.point_at(t, tol)?));
696 }
697 Ok(Segment {
698 source: edge.clone(),
699 curve,
700 pcurve,
701 range,
702 backwards: false,
703 tolerance: gap,
704 })
705}
706
707fn periods(surface: &SurfaceGeometry, tol: Tolerances) -> (f64, f64) {
713 let ((u0, u1), (v0, v1)) = surface.domain();
714 let (wraps_u, wraps_v) = wraps(surface, tol);
715 (
716 if wraps_u { u1 - u0 } else { 0.0 },
717 if wraps_v { v1 - v0 } else { 0.0 },
718 )
719}
720
721fn wraps(surface: &SurfaceGeometry, tol: Tolerances) -> (bool, bool) {
724 (
725 surface.is_periodic_u() || surface.is_closed_u(tol),
726 surface.is_periodic_v() || surface.is_closed_v(tol),
727 )
728}
729
730fn unfold(
737 segment: Segment,
738 surface: &SurfaceGeometry,
739 window: Bounds,
740 tol: Tolerances,
741) -> OgeomResult<Vec<Segment>> {
742 let (pu, pv) = periods(surface, tol);
743 let mut parts = vec![segment];
744 for (axis, period, (lo, hi)) in [(0, pu, window.0), (1, pv, window.1)] {
745 if period <= 0.0 || !lo.is_finite() || !hi.is_finite() {
746 continue;
747 }
748 let low = lo - 0.5 * (period - (hi - lo)).max(0.0);
749 let copy = |p: Point2| -> f64 {
751 let x = if axis == 0 { p.x } else { p.y };
752 ((x - low) / period + 1e-9).floor()
753 };
754 let mut next = Vec::new();
755 for part in parts {
756 const N: usize = 128;
757 let mut cuts = vec![0.0];
758 let mut last = copy(part.chart(0.0, tol)?);
759 for i in 1..=N {
760 #[expect(clippy::cast_precision_loss, reason = "a sample index")]
761 let f = i as f64 / N as f64;
762 let here = copy(part.chart(f, tol)?);
763 if here != last {
764 #[expect(clippy::cast_precision_loss, reason = "a sample index")]
765 let (mut a, mut b) = ((i - 1) as f64 / N as f64, f);
766 for _ in 0..60 {
767 let mid = 0.5 * (a + b);
768 if copy(part.chart(mid, tol)?) == last {
769 a = mid;
770 } else {
771 b = mid;
772 }
773 }
774 cuts.push(0.5 * (a + b));
775 last = here;
776 }
777 }
778 cuts.push(1.0);
779 let mut kept = vec![0.0];
784 for &c in &cuts[1..] {
785 let from = kept[kept.len() - 1];
786 let sliver = c - from <= 1e-6
787 && part.point(from, tol)?.distance(part.point(c, tol)?) <= tol.confusion();
788 if !sliver {
789 kept.push(c);
790 } else if c >= 1.0 {
791 if kept.len() > 1 {
792 let last = kept.len() - 1;
793 kept[last] = 1.0;
794 } else {
795 kept.push(1.0);
796 }
797 }
798 }
799 for w in kept.windows(2) {
800 if w[1] - w[0] <= 1e-9 {
801 continue;
802 }
803 let piece = part.part(w[0], w[1]);
804 let turns = copy(piece.chart(0.5, tol)?);
805 let by = if axis == 0 {
806 Vector2::new(-turns * period, 0.0)
807 } else {
808 Vector2::new(0.0, -turns * period)
809 };
810 next.push(piece.shifted(by, tol)?);
811 }
812 }
813 parts = next;
814 }
815 Ok(parts)
816}
817
818fn chain(
821 mut segments: Vec<Segment>,
822 surface: &SurfaceGeometry,
823 tol: Tolerances,
824) -> OgeomResult<Vec<Cutter>> {
825 let (pu, pv) = periods(surface, tol);
826 let meet = |a: &Segment, fa: f64, b: &Segment, fb: f64| -> OgeomResult<bool> {
827 let reach = a.tolerance + b.tolerance + tol.confusion();
828 if a.point(fa, tol)?.distance(b.point(fb, tol)?) > reach {
829 return Ok(false);
830 }
831 let (p, q) = (a.chart(fa, tol)?, b.chart(fb, tol)?);
832 Ok((pu <= 0.0 || (p.x - q.x).abs() < 0.25 * pu)
833 && (pv <= 0.0 || (p.y - q.y).abs() < 0.25 * pv))
834 };
835 let mut out = Vec::new();
836 while let Some(first) = segments.pop() {
837 let mut path = std::collections::VecDeque::from([first]);
838 loop {
839 let mut grown = false;
840 let mut k = 0;
841 while k < segments.len() {
842 let (head, tail) = (&path[0], &path[path.len() - 1]);
843 let s = &segments[k];
844 if meet(tail, 1.0, s, 0.0)? {
845 path.push_back(segments.swap_remove(k));
846 } else if meet(tail, 1.0, s, 1.0)? {
847 path.push_back(segments.swap_remove(k).reversed());
848 } else if meet(head, 0.0, s, 1.0)? {
849 path.push_front(segments.swap_remove(k));
850 } else if meet(head, 0.0, s, 0.0)? {
851 path.push_front(segments.swap_remove(k).reversed());
852 } else {
853 k += 1;
854 continue;
855 }
856 grown = true;
857 }
858 if !grown {
859 break;
860 }
861 }
862 let path: Vec<Segment> = path.into_iter().collect();
863 let closed = meet(&path[path.len() - 1], 1.0, &path[0], 0.0)?;
864 out.push(Cutter {
865 path,
866 closed,
867 own: FastSet::default(),
868 consumed: Vec::new(),
869 });
870 }
871 Ok(out)
872}
873
874fn land(
882 surface: &SurfaceGeometry,
883 p: Point,
884 seed: Option<Point2>,
885 projection: Projection,
886 tol: Tolerances,
887) -> OgeomResult<Option<(Point, Point2)>> {
888 match projection {
889 Projection::OnFace | Projection::AlongNormals => {
890 let found = match seed
891 .and_then(|s| project_on_surface_from(surface, p, (s.x, s.y), tol).ok())
892 {
893 Some(found) => found,
894 None => project_on_surface(surface, p, 24, tol)?,
895 };
896 let (u, v) = found.parameters;
897 let gap = p - found.point;
898 let d = gap.magnitude();
899 if d > tol.confusion() {
900 let (su, sv) = surface.d1_at(u, v, tol)?;
901 for t in [su, sv] {
902 let m = t.magnitude();
903 if m > 0.0 && gap.dot(t).abs() > 1e-7 * m * d {
904 return Ok(None);
905 }
906 }
907 }
908 Ok(Some((found.point, Point2::new(u, v))))
909 }
910 Projection::Along(direction) => {
911 let start = match seed {
912 Some(s) => (s.x, s.y),
913 None => project_on_surface(surface, p, 24, tol)?.parameters,
914 };
915 Ok(ray_hit(surface, p, direction.vector(), start, tol)?
916 .map(|(at, u, v)| (at, Point2::new(u, v))))
917 }
918 }
919}
920
921fn ray_hit(
924 surface: &SurfaceGeometry,
925 p: Point,
926 d: ogeom_math::Vector,
927 start: (f64, f64),
928 tol: Tolerances,
929) -> OgeomResult<Option<(Point, f64, f64)>> {
930 let ((u0, u1), (v0, v1)) = surface.domain();
931 let (wraps_u, wraps_v) = wraps(surface, tol);
932 let hold = |x: f64, lo: f64, hi: f64, periodic: bool| {
933 if periodic && hi > lo {
934 lo + (x - lo).rem_euclid(hi - lo)
935 } else {
936 x.clamp(lo, hi)
937 }
938 };
939 let (mut u, mut v) = start;
940 let mut s = (surface.point_at(u, v, tol)? - p).dot(d);
941 for _ in 0..64 {
942 let (at, su, sv) = surface.point_d1_at(u, v, tol)?;
943 let f = at - (p + d * s);
944 if f.magnitude() <= tol.confusion() * 1e-3 {
945 return Ok(Some((at, u, v)));
946 }
947 let j = nalgebra::Matrix3::new(su.x, sv.x, -d.x, su.y, sv.y, -d.y, su.z, sv.z, -d.z);
948 let Some(step) = j.lu().solve(&nalgebra::Vector3::new(-f.x, -f.y, -f.z)) else {
949 return Ok(None);
950 };
951 u = hold(u + step[0], u0, u1, wraps_u);
952 v = hold(v + step[1], v0, v1, wraps_v);
953 s += step[2];
954 }
955 let at = surface.point_at(u, v, tol)?;
956 if at.distance(p + d * s) <= tol.confusion() {
957 return Ok(Some((at, u, v)));
958 }
959 Ok(None)
960}
961
962fn landed_runs(
966 curve: &Curve,
967 range: (f64, f64),
968 surface: &SurfaceGeometry,
969 projection: Projection,
970 tol: Tolerances,
971) -> OgeomResult<Vec<((f64, f64), Point2)>> {
972 const STATIONS: usize = 128;
973 let mut stations: Vec<(f64, Option<Point2>)> = Vec::with_capacity(STATIONS + 1);
974 let mut seed = None;
975 for k in 0..=STATIONS {
976 #[expect(
977 clippy::cast_precision_loss,
978 reason = "a station index, far below the mantissa"
979 )]
980 let t = (range.1 - range.0).mul_add(k as f64 / STATIONS as f64, range.0);
981 let landed = land(surface, curve.point_at(t, tol)?, seed, projection, tol)?;
982 seed = landed.map(|(_, uv)| uv);
983 stations.push((t, seed));
984 }
985 let edge_of = |inside: (f64, Point2), outside: f64| -> OgeomResult<(f64, Point2)> {
988 let (mut good, mut seed) = inside;
989 let mut bad = outside;
990 for _ in 0..60 {
991 let mid = 0.5 * (good + bad);
992 match land(
993 surface,
994 curve.point_at(mid, tol)?,
995 Some(seed),
996 projection,
997 tol,
998 )? {
999 Some((_, uv)) => (good, seed) = (mid, uv),
1000 None => bad = mid,
1001 }
1002 }
1003 Ok((good, seed))
1004 };
1005 let mut runs = Vec::new();
1006 let mut k = 0;
1007 while k < stations.len() {
1008 let Some(uv) = stations[k].1 else {
1009 k += 1;
1010 continue;
1011 };
1012 let first = k;
1013 while k + 1 < stations.len() && stations[k + 1].1.is_some() {
1014 k += 1;
1015 }
1016 let (lo, seed) = if first == 0 {
1017 (range.0, uv)
1018 } else {
1019 edge_of((stations[first].0, uv), stations[first - 1].0)?
1020 };
1021 let hi = match (stations[k].1, stations.get(k + 1)) {
1022 (Some(last), Some(next)) => edge_of((stations[k].0, last), next.0)?.0,
1023 _ => range.1,
1024 };
1025 if hi - lo > (range.1 - range.0) * 1e-9 {
1026 runs.push(((lo, hi), seed));
1027 }
1028 k += 1;
1029 }
1030 Ok(runs)
1031}
1032
1033fn projected(
1036 edge: &Shape,
1037 curve: &Curve,
1038 span: (f64, f64),
1039 start: Point2,
1040 surface: &SurfaceGeometry,
1041 projection: Projection,
1042 tol: Tolerances,
1043) -> OgeomResult<Segment> {
1044 const SAMPLES: usize = 96;
1045 let mut points = Vec::with_capacity(SAMPLES + 1);
1046 let mut chart: Vec<Point2> = Vec::with_capacity(SAMPLES + 1);
1047 let mut seed = Some(start);
1048 for k in 0..=SAMPLES {
1049 #[expect(
1050 clippy::cast_precision_loss,
1051 reason = "a sample index, far below the mantissa"
1052 )]
1053 let t = (span.1 - span.0).mul_add(k as f64 / SAMPLES as f64, span.0);
1054 let inward = (span.1 - span.0) * if k == 0 { 1e-12 } else { -1e-12 };
1057 let tries = if k == 0 || k == SAMPLES { 16 } else { 0 };
1058 let mut landed = None;
1059 for step in 0..=tries {
1060 let at = curve.point_at(f64::from(step).mul_add(inward, t), tol)?;
1061 landed = land(surface, at, seed, projection, tol)?;
1062 if landed.is_some() {
1063 break;
1064 }
1065 }
1066 let Some((at, uv)) = landed else {
1067 ogeom_bail!(
1068 Construction,
1069 "the projection of an edge breaks off between two landings"
1070 );
1071 };
1072 points.push(at);
1073 chart.push(uv);
1074 seed = Some(uv);
1075 }
1076 unwrap(&mut chart, surface, tol);
1077 let (fitted, trace, _) =
1078 ogeom_geom::fit::fit_points_joint(&points, &chart, &chart, 3, tol.confusion(), tol)?;
1079 let error = fitted.error;
1080 let fitted: Curve = Curve::BSpline(fitted.curve);
1081 let range = fitted.domain();
1082 segment(
1083 edge,
1084 fitted,
1085 PlanarCurve::BSpline(trace),
1086 range,
1087 error,
1088 surface,
1089 tol,
1090 )
1091}
1092
1093fn unwrap(chart: &mut [Point2], surface: &SurfaceGeometry, tol: Tolerances) {
1096 let (pu, pv) = periods(surface, tol);
1097 for k in 1..chart.len() {
1098 let previous = chart[k - 1];
1099 let here = &mut chart[k];
1100 if pu > 0.0 {
1101 here.x -= ((here.x - previous.x) / pu).round() * pu;
1102 }
1103 if pv > 0.0 {
1104 here.y -= ((here.y - previous.y) / pv).round() * pv;
1105 }
1106 }
1107}
1108
1109#[derive(Debug, Clone)]
1113struct Meeting {
1114 t: f64,
1116 at: Point2,
1118 on: On,
1119}
1120
1121#[derive(Debug, Clone)]
1122enum On {
1123 Vertex(Shape),
1125 Inside(Shape, f64),
1127}
1128
1129#[derive(Debug, Clone)]
1131enum Stretch {
1132 Open(Meeting, Meeting),
1134 Loop,
1136}
1137
1138struct Planned {
1140 stretch: Stretch,
1141 span: (f64, f64),
1142}
1143
1144enum Found {
1145 Nothing,
1146 Dangling,
1148 Rebase(f64),
1151 Cut(Planned),
1152}
1153
1154struct Chart {
1156 rings: Vec<Vec<Occurrence>>,
1157 outlines: Vec<Vec<Point2>>,
1158 eps: f64,
1160}
1161
1162fn chart_of(model: &Model, face: &Shape, tol: Tolerances) -> OgeomResult<Chart> {
1163 let face_fwd = forward(face);
1164 let data = face_data(model, &face_fwd)?;
1165 let rings = rings(model, &face_fwd, data.surface, tol)?;
1166 let bounds = chart_bounds(&rings, tol);
1167 let span = (bounds.0.1 - bounds.0.0).max(bounds.1.1 - bounds.1.0);
1168 let outlines = rings
1169 .iter()
1170 .map(|ring| -> OgeomResult<Vec<Point2>> {
1171 let mut out = Vec::new();
1172 for o in ring {
1173 let mut line = o.polyline(64, tol)?;
1174 line.pop();
1175 out.extend(line);
1176 }
1177 Ok(out)
1178 })
1179 .collect::<OgeomResult<_>>()?;
1180 Ok(Chart {
1181 rings,
1182 outlines,
1183 eps: span * 1e-9 + tol.parametric(),
1184 })
1185}
1186
1187fn next_stretch(
1189 model: &Model,
1190 face: &Shape,
1191 cutter: &Cutter,
1192 surface: &SurfaceGeometry,
1193 tol: Tolerances,
1194) -> OgeomResult<Found> {
1195 let chart = chart_of(model, face, tol)?;
1196 let meetings = meetings(model, &chart, cutter, surface, tol)?;
1197 let t_eps = cutter.resolution(tol);
1198 let (lo, hi) = cutter.range();
1199
1200 let within = |a: f64, b: f64| -> OgeomResult<bool> {
1203 let mid = 0.5 * (a + b);
1204 let at = cutter.chart(mid, tol)?;
1205 let p = cutter.point(mid, tol)?;
1206 for o in chart.rings.iter().flatten() {
1207 if cutter.own.contains(&o.edge.node()) {
1208 continue;
1209 }
1210 let (_, near) = nearest_on(o, at, tol)?;
1211 let reach = boundary_reach(model, o, cutter, tol)?;
1212 if surface.point_at(near.x, near.y, tol)?.distance(p) <= reach {
1213 ogeom_bail!(
1214 Construction,
1215 "a curve to split along runs along the face's boundary"
1216 );
1217 }
1218 }
1219 Ok(inside(&chart.outlines, at))
1220 };
1221
1222 if meetings.is_empty() {
1223 if cutter.is_consumed(lo, hi) || !within(lo, hi)? {
1224 return Ok(Found::Nothing);
1225 }
1226 if cutter.closed {
1227 return Ok(Found::Cut(Planned {
1228 stretch: Stretch::Loop,
1229 span: (lo, hi),
1230 }));
1231 }
1232 return Ok(Found::Dangling);
1233 }
1234 if cutter.closed {
1235 return Ok(Found::Rebase(meetings[0].t));
1236 }
1237 let negligible = |a: f64, b: f64| -> OgeomResult<bool> {
1243 if b - a <= t_eps {
1244 return Ok(true);
1245 }
1246 let reach = cutter.tolerance() + tol.confusion() * 10.0;
1247 let start = cutter.point(a, tol)?;
1248 Ok(start.distance(cutter.point(b, tol)?) <= reach
1249 && start.distance(cutter.point(f64::midpoint(a, b), tol)?) <= reach)
1250 };
1251 let (first, last) = (&meetings[0], &meetings[meetings.len() - 1]);
1252 for (a, b) in [(lo, first.t), (last.t, hi)] {
1253 if !negligible(a, b)? && !cutter.is_consumed(a, b) && within(a, b)? {
1254 return Ok(Found::Dangling);
1255 }
1256 }
1257 for pair in meetings.windows(2) {
1258 let (a, b) = (&pair[0], &pair[1]);
1259 if negligible(a.t, b.t)? || cutter.is_consumed(a.t, b.t) {
1260 continue;
1261 }
1262 if within(a.t, b.t)? {
1263 return Ok(Found::Cut(Planned {
1264 stretch: Stretch::Open(a.clone(), b.clone()),
1265 span: (a.t, b.t),
1266 }));
1267 }
1268 }
1269 Ok(Found::Nothing)
1270}
1271
1272fn boundary_reach(
1274 model: &Model,
1275 o: &Occurrence,
1276 cutter: &Cutter,
1277 tol: Tolerances,
1278) -> OgeomResult<f64> {
1279 Ok(edge_data(model, &o.edge)?.tolerance.get() + cutter.tolerance() + tol.confusion())
1280}
1281
1282fn nearest_on(o: &Occurrence, p: Point2, tol: Tolerances) -> OgeomResult<(f64, Point2)> {
1284 const N: usize = 64;
1285 let line = o.polyline(N, tol)?;
1286 let mut best = (0, f64::INFINITY);
1287 for (i, q) in line.iter().enumerate() {
1288 let d = q.distance(p);
1289 if d < best.1 {
1290 best = (i, d);
1291 }
1292 }
1293 #[expect(clippy::cast_precision_loss, reason = "a sample index")]
1294 let step = 1.0 / N as f64;
1295 #[expect(clippy::cast_precision_loss, reason = "a sample index")]
1296 let centre = best.0 as f64 * step;
1297 let (mut a, mut b) = ((centre - step).max(0.0), (centre + step).min(1.0));
1298 let g = 0.5 * (5.0_f64.sqrt() - 1.0);
1299 let far = |s: f64| -> OgeomResult<f64> { Ok(o.at(s, tol)?.distance(p)) };
1300 for _ in 0..80 {
1301 let (x, y) = (b - g * (b - a), a + g * (b - a));
1302 if far(x)? <= far(y)? {
1303 b = y;
1304 } else {
1305 a = x;
1306 }
1307 }
1308 let s = 0.5 * (a + b);
1309 Ok((s, o.at(s, tol)?))
1310}
1311
1312fn meetings(
1315 model: &Model,
1316 chart: &Chart,
1317 cutter: &Cutter,
1318 surface: &SurfaceGeometry,
1319 tol: Tolerances,
1320) -> OgeomResult<Vec<Meeting>> {
1321 const ALONG: usize = 128;
1322 const ACROSS: usize = 64;
1323 let (lo, hi) = cutter.range();
1324 let samples = ALONG * cutter.path.len();
1325 let track: Vec<(f64, Point2)> = (0..=samples)
1326 .map(|i| {
1327 #[expect(clippy::cast_precision_loss, reason = "a sample index")]
1328 let t = (hi - lo).mul_add(i as f64 / samples as f64, lo);
1329 Ok((t, cutter.chart(t, tol)?))
1330 })
1331 .collect::<OgeomResult<_>>()?;
1332 let mut found: Vec<Meeting> = Vec::new();
1333 for o in chart.rings.iter().flatten() {
1334 if cutter.own.contains(&o.edge.node()) {
1335 continue;
1336 }
1337 let line = o.polyline(ACROSS, tol)?;
1338 for j in 1..line.len() {
1339 for i in 1..track.len() {
1340 let Some((x, y)) =
1341 segments_cross((track[i - 1].1, track[i].1), (line[j - 1], line[j]))
1342 else {
1343 continue;
1344 };
1345 let t = (track[i].0 - track[i - 1].0).mul_add(x, track[i - 1].0);
1346 #[expect(clippy::cast_precision_loss, reason = "a sample index")]
1347 let s = ((j - 1) as f64 + y) / ACROSS as f64;
1348 if let Some((t, s)) = polish(cutter, o, t, s, chart.eps, tol)? {
1349 found.push(meeting(model, o, cutter, t, s, tol)?);
1350 }
1351 }
1352 }
1353 if !cutter.closed {
1355 let reach = boundary_reach(model, o, cutter, tol)?;
1356 for t in [lo, hi] {
1357 let at = cutter.chart(t, tol)?;
1358 let (s, near) = nearest_on(o, at, tol)?;
1359 let p = cutter.point(t, tol)?;
1360 if surface.point_at(near.x, near.y, tol)?.distance(p) <= reach {
1361 found.push(meeting(model, o, cutter, t, s, tol)?);
1362 }
1363 }
1364 }
1365 }
1366 found.sort_by(|a, b| a.t.total_cmp(&b.t));
1367 let t_eps = cutter.resolution(tol);
1370 let mut out: Vec<Meeting> = Vec::new();
1371 for m in found {
1372 match out.last_mut() {
1373 Some(held) if (m.t - held.t).abs() <= t_eps * 1e3 => {
1374 if matches!(m.on, On::Vertex(_)) {
1375 *held = m;
1376 }
1377 }
1378 _ => out.push(m),
1379 }
1380 }
1381 on_pole_rows(model, chart, &mut out, tol)?;
1382 Ok(out)
1383}
1384
1385fn on_pole_rows(
1396 model: &Model,
1397 chart: &Chart,
1398 out: &mut [Meeting],
1399 tol: Tolerances,
1400) -> OgeomResult<()> {
1401 for i in 0..out.len() {
1402 for j in i + 1..out.len() {
1403 let (On::Vertex(a), On::Vertex(b)) = (&out[i].on, &out[j].on) else {
1404 continue;
1405 };
1406 if a.node() != b.node() || out[i].at.distance(out[j].at) <= chart.eps {
1407 continue;
1408 }
1409 let v = a.node();
1410 for o in chart.rings.iter().flatten() {
1411 let edge = forward(&o.edge);
1412 if !edge_data(model, &edge)?.degenerate {
1413 continue;
1414 }
1415 let at_pole = edge_vertices(model, &edge)?
1416 .is_some_and(|(first, last)| first.node() == v || last.node() == v);
1417 if !at_pole {
1418 continue;
1419 }
1420 let mut reread = Vec::with_capacity(2);
1421 for k in [i, j] {
1422 let (s, near) = nearest_on(o, out[k].at, tol)?;
1423 if near.distance(out[k].at) > chart.eps {
1424 break;
1425 }
1426 let forward_s = if o.reversed() { 1.0 - s } else { s };
1427 reread.push((k, (forward_s * 1e12).round() / 1e12));
1428 }
1429 if reread.len() == 2 {
1430 for (k, s) in reread {
1431 out[k].on = On::Inside(edge.clone(), s);
1432 }
1433 break;
1434 }
1435 }
1436 }
1437 }
1438 Ok(())
1439}
1440
1441fn polish(
1443 cutter: &Cutter,
1444 o: &Occurrence,
1445 mut t: f64,
1446 mut s: f64,
1447 eps: f64,
1448 tol: Tolerances,
1449) -> OgeomResult<Option<(f64, f64)>> {
1450 let (lo, hi) = cutter.range();
1451 let h = 1e-7;
1452 for _ in 0..40 {
1453 let f = cutter.chart(t, tol)? - o.at(s, tol)?;
1454 if f.magnitude() <= eps * 1e-3 {
1455 break;
1456 }
1457 let dc = cutter.chart_d1(t, tol)?;
1458 let (s0, s1) = ((s - h).max(0.0), (s + h).min(1.0));
1459 let dq = (o.at(s1, tol)? - o.at(s0, tol)?) / (s1 - s0);
1460 let det = dc.x.mul_add(-dq.y, dq.x * dc.y);
1462 if det.abs() <= f64::MIN_POSITIVE {
1463 return Ok(None);
1464 }
1465 let dt = (-f.x).mul_add(-dq.y, dq.x * (-f.y)) / det;
1466 let ds = dc.x.mul_add(-f.y, f.x * dc.y) / det;
1467 t = (t + dt).clamp(lo, hi);
1468 s = (s + ds).clamp(0.0, 1.0);
1469 }
1470 let f = cutter.chart(t, tol)? - o.at(s, tol)?;
1471 Ok((f.magnitude() <= eps).then_some((t, s)))
1472}
1473
1474fn meeting(
1477 model: &Model,
1478 o: &Occurrence,
1479 cutter: &Cutter,
1480 t: f64,
1481 s: f64,
1482 tol: Tolerances,
1483) -> OgeomResult<Meeting> {
1484 let at = cutter.chart(t, tol)?;
1485 let edge = forward(&o.edge);
1486 let forward_s = if o.reversed() { 1.0 - s } else { s };
1487 let forward_s = (forward_s * 1e12).round() / 1e12;
1489 let p = cutter.point(t, tol)?;
1490 if let Some((first, last)) = edge_vertices(model, &edge)? {
1491 for v in [first, last] {
1492 let held = model
1493 .node(&v)
1494 .and_then(|n| n.data().as_vertex())
1495 .map_or(0.0, |d| d.tolerance.get());
1496 if vertex_point(model, &v)?.distance(p) <= held + cutter.tolerance() + tol.confusion() {
1497 return Ok(Meeting {
1498 t,
1499 at,
1500 on: On::Vertex(v),
1501 });
1502 }
1503 }
1504 }
1505 Ok(Meeting {
1506 t,
1507 at,
1508 on: On::Inside(edge, forward_s),
1509 })
1510}
1511
1512fn segments_cross(a: (Point2, Point2), b: (Point2, Point2)) -> Option<(f64, f64)> {
1515 let da = a.1 - a.0;
1516 let db = b.1 - b.0;
1517 let cross = da.x.mul_add(db.y, -(da.y * db.x));
1518 if cross.abs() <= f64::MIN_POSITIVE {
1519 return None;
1520 }
1521 let w = b.0 - a.0;
1522 let x = w.x.mul_add(db.y, -(w.y * db.x)) / cross;
1523 let y = w.x.mul_add(da.y, -(w.y * da.x)) / cross;
1524 let slack = 1e-9;
1525 ((-slack..=1.0 + slack).contains(&x) && (-slack..=1.0 + slack).contains(&y))
1526 .then_some((x.clamp(0.0, 1.0), y.clamp(0.0, 1.0)))
1527}
1528
1529fn join_across_seams(
1545 model: &mut Model,
1546 current: Built,
1547 pieces: &[Shape],
1548 surface: SurfaceId,
1549 tol: Tolerances,
1550) -> OgeomResult<Built> {
1551 let repeats = model
1555 .geometry()
1556 .surface(surface)
1557 .map_or([false; 2], |s| [s.is_periodic_u(), s.is_periodic_v()]);
1558 let seam = |model: &Model, edge: &Shape| -> OgeomResult<bool> {
1559 let data = edge_data(model, edge)?;
1560 let Some(EdgeRepr::Seam {
1561 forward,
1562 reversed,
1563 range,
1564 ..
1565 }) = data.pcurve_for(surface, edge.location())
1566 else {
1567 return Ok(false);
1568 };
1569 let (Some(a), Some(b)) = (
1570 model.geometry().pcurve(*forward),
1571 model.geometry().pcurve(*reversed),
1572 ) else {
1573 return Ok(false);
1574 };
1575 let middle = f64::midpoint(range.0, range.1);
1576 let apart = b.point_at(middle, tol)? - a.point_at(middle, tol)?;
1577 Ok(if apart.x.abs() >= apart.y.abs() {
1578 repeats[0]
1579 } else {
1580 repeats[1]
1581 })
1582 };
1583 let mut rings: Vec<Vec<Vec<Shape>>> = Vec::with_capacity(pieces.len());
1585 for piece in pieces {
1586 let mut own = Vec::new();
1587 for wire in model.children_of(&forward(piece))? {
1588 own.push(model.ordered_children_of(&wire)?);
1589 }
1590 rings.push(own);
1591 }
1592 let mut bounding: FastMap<TShapeId, usize> = FastMap::default();
1593 for face in explore_unique(model, ¤t.shape, ShapeType::Face)? {
1594 for edge in explore_unique(model, &face, ShapeType::Edge)? {
1595 *bounding.entry(edge.node()).or_default() += 1;
1596 }
1597 }
1598 let mut uses: FastMap<TShapeId, Vec<(usize, usize, &Shape)>> = FastMap::default();
1599 let mut order: Vec<TShapeId> = Vec::new();
1600 for (i, own) in rings.iter().enumerate() {
1601 for (r, ring) in own.iter().enumerate() {
1602 for edge in ring {
1603 let at = uses.entry(edge.node()).or_default();
1604 if at.is_empty() {
1605 order.push(edge.node());
1606 }
1607 at.push((i, r, edge));
1608 }
1609 }
1610 }
1611 let mut parting: FastMap<TShapeId, (usize, usize)> = FastMap::default();
1614 for node in &order {
1615 let [(i, 0, a), (j, 0, b)] = uses[node].as_slice() else {
1616 continue;
1617 };
1618 if i != j
1619 && pieces[*i].orientation() == pieces[*j].orientation()
1620 && a.orientation() != b.orientation()
1621 && bounding.get(node) == Some(&2)
1622 && seam(model, a)?
1623 {
1624 parting.insert(*node, (*i, *j));
1625 }
1626 }
1627 if parting.is_empty() {
1628 return Ok(current);
1629 }
1630
1631 let mut group: Vec<usize> = (0..pieces.len()).collect();
1632 fn root(group: &mut [usize], mut i: usize) -> usize {
1633 while group[i] != i {
1634 group[i] = group[group[i]];
1635 i = group[i];
1636 }
1637 i
1638 }
1639 for node in &order {
1640 if let Some(&(i, j)) = parting.get(node) {
1641 let (ri, rj) = (root(&mut group, i), root(&mut group, j));
1642 group[ri] = rj;
1643 }
1644 }
1645 let mut reshape = crate::Reshape::new();
1646 for keeper in 0..pieces.len() {
1647 let members: Vec<usize> = (0..pieces.len())
1648 .filter(|&k| root(&mut group, k) == root(&mut group, keeper))
1649 .collect();
1650 if members.len() < 2 || members[0] != keeper {
1651 continue;
1652 }
1653 let mut ring = rings[keeper][0].clone();
1656 let mut joined: FastSet<usize> = FastSet::default();
1657 joined.insert(keeper);
1658 let mut k = 0;
1659 while k < ring.len() {
1660 let other = parting
1661 .get(&ring[k].node())
1662 .and_then(|&(i, j)| [i, j].into_iter().find(|m| !joined.contains(m)));
1663 let Some(other) = other else {
1664 k += 1;
1665 continue;
1666 };
1667 let theirs = &rings[other][0];
1668 let Some(at) = theirs.iter().position(|e| e.node() == ring[k].node()) else {
1669 break;
1670 };
1671 let spliced: Vec<Shape> = (1..theirs.len())
1672 .map(|s| theirs[(at + s) % theirs.len()].clone())
1673 .collect();
1674 ring.splice(k..=k, spliced);
1675 joined.insert(other);
1676 }
1677 let mut kept: Vec<Shape> = Vec::with_capacity(ring.len());
1680 for edge in ring {
1681 match kept.last() {
1682 Some(last)
1683 if last.node() == edge.node()
1684 && last.orientation() != edge.orientation()
1685 && parting.contains_key(&edge.node()) =>
1686 {
1687 kept.pop();
1688 }
1689 _ => kept.push(edge),
1690 }
1691 }
1692 while kept.len() > 1 {
1693 let (first, last) = (&kept[0], &kept[kept.len() - 1]);
1694 if first.node() == last.node()
1695 && first.orientation() != last.orientation()
1696 && parting.contains_key(&first.node())
1697 {
1698 kept.pop();
1699 kept.remove(0);
1700 } else {
1701 break;
1702 }
1703 }
1704 let mut walked: FastMap<TShapeId, usize> = FastMap::default();
1705 for edge in &kept {
1706 *walked.entry(edge.node()).or_default() += 1;
1707 }
1708 let mut sound = joined.len() == members.len() && !kept.is_empty();
1709 for edge in &kept {
1710 if !sound {
1711 break;
1712 }
1713 let times = walked[&edge.node()];
1714 sound = !parting.contains_key(&edge.node())
1715 && (times == 1 || (times == 2 && seam(model, edge)?));
1716 }
1717 if !sound {
1718 continue;
1719 }
1720 let mut wires = vec![model.add_wire(&kept)?];
1721 for &m in &members {
1722 wires.extend(model.children_of(&forward(&pieces[m]))?.into_iter().skip(1));
1723 }
1724 let Some(NodeData::Face(data)) = model.node(&pieces[keeper]).map(|n| n.data().clone())
1725 else {
1726 ogeom_bail!(Dangling, "a piece of the face is not in this model");
1727 };
1728 let mut data = *data;
1729 data.triangulation = None;
1730 let face = model.add_face(data, &wires)?;
1731 reshape.replace(&forward(&pieces[keeper]), face);
1732 for &m in &members[1..] {
1733 reshape.remove(&forward(&pieces[m]));
1734 }
1735 }
1736 if reshape.is_empty() {
1737 return Ok(current);
1738 }
1739 let step = reshape.apply(model, ¤t.shape)?;
1740 Ok(Built::new(step.shape, current.history.then(&step.history)))
1741}
1742
1743struct Made {
1747 reshape: Reshape,
1748 edges: Vec<(Shape, Shape)>,
1750 splits: Vec<(TShapeId, Vec<Shape>)>,
1752}
1753
1754type Parts = Vec<(Shape, f64, f64)>;
1756
1757#[derive(Debug, Clone)]
1759struct Item {
1760 edge: Shape,
1761 from: TShapeId,
1762 polyline: Vec<Point2>,
1763}
1764
1765#[allow(clippy::too_many_lines)]
1767fn cut(
1768 model: &mut Model,
1769 face: &Shape,
1770 cutter: &Cutter,
1771 planned: &Planned,
1772 tol: Tolerances,
1773) -> OgeomResult<Made> {
1774 let face_fwd = forward(face);
1775 let data = face_data(model, &face_fwd)?;
1776 let chart = chart_of(model, face, tol)?;
1777 let mut reshape = Reshape::new();
1778 let mut splits: Vec<(TShapeId, Vec<Shape>)> = Vec::new();
1779
1780 let ends: Vec<Meeting> = match &planned.stretch {
1782 Stretch::Open(a, b) => vec![a.clone(), b.clone()],
1783 Stretch::Loop => Vec::new(),
1784 };
1785 let mut fractions: Vec<(Shape, Vec<f64>)> = Vec::new();
1786 for m in &ends {
1787 if let On::Inside(edge, s) = &m.on {
1788 match fractions.iter_mut().find(|(e, _)| e.node() == edge.node()) {
1789 Some((_, list)) => list.push(*s),
1790 None => fractions.push((edge.clone(), vec![*s])),
1791 }
1792 }
1793 }
1794 let mut pieces_of: Vec<(TShapeId, Parts)> = Vec::new();
1795 let mut vertex_at: Vec<((TShapeId, u64), Shape)> = Vec::new();
1796 for (edge, mut given) in fractions {
1797 given.sort_by(f64::total_cmp);
1798 let mut list: Vec<f64> = Vec::with_capacity(given.len());
1801 let mut joined: Vec<(f64, usize)> = Vec::with_capacity(given.len());
1802 for &s in &given {
1803 if list.last().is_none_or(|last| (s - last).abs() > 1e-12) {
1804 list.push(s);
1805 }
1806 joined.push((s, list.len() - 1));
1807 }
1808 let (pieces, vertices) = split_edge(model, &edge, &list, Some(data.surface), tol)?;
1809 for (s, k) in joined {
1810 vertex_at.push(((edge.node(), s.to_bits()), vertices[k].clone()));
1811 }
1812 let mut bounds = vec![0.0];
1813 bounds.extend(&list);
1814 bounds.push(1.0);
1815 pieces_of.push((
1816 edge.node(),
1817 pieces
1818 .iter()
1819 .zip(bounds.windows(2))
1820 .map(|(p, w)| (p.clone(), w[0], w[1]))
1821 .collect(),
1822 ));
1823 reshape.split(&edge, pieces.clone());
1824 splits.push((edge.node(), pieces));
1825 }
1826 let vertex_of = |m: &Meeting| -> OgeomResult<Shape> {
1827 match &m.on {
1828 On::Vertex(v) => Ok(v.clone()),
1829 On::Inside(edge, s) => vertex_at
1830 .iter()
1831 .find(|(key, _)| *key == (edge.node(), s.to_bits()))
1832 .map(|(_, v)| v.clone())
1833 .ok_or_else(|| ogeom_err!(Construction, "a cut vertex is lost")),
1834 }
1835 };
1836
1837 let mut items: Vec<Vec<Item>> = Vec::new();
1839 for ring in &chart.rings {
1840 let mut out = Vec::new();
1841 for o in ring {
1842 let edge = forward(&o.edge);
1843 let parts = pieces_of
1844 .iter()
1845 .find(|(node, _)| *node == edge.node())
1846 .map_or_else(|| vec![(edge.clone(), 0.0, 1.0)], |(_, p)| p.clone());
1847 let ordered: Parts = if o.reversed() {
1848 parts
1849 .into_iter()
1850 .rev()
1851 .map(|(p, a, b)| (p.reversed(), 1.0 - b, 1.0 - a))
1852 .collect()
1853 } else {
1854 parts
1855 };
1856 for (piece, a, b) in ordered {
1857 let polyline: Vec<Point2> = (0..=16)
1858 .map(|i| o.at((b - a).mul_add(f64::from(i) / 16.0, a), tol))
1859 .collect::<OgeomResult<_>>()?;
1860 let Some((from, _)) = edge_vertices(model, &piece)? else {
1861 ogeom_bail!(Construction, "a boundary piece has no vertices");
1862 };
1863 out.push(Item {
1864 edge: piece,
1865 from: from.node(),
1866 polyline,
1867 });
1868 }
1869 }
1870 items.push(out);
1871 }
1872 let areas: Vec<f64> = items
1873 .iter()
1874 .map(|ring| signed_area(&outline(ring)))
1875 .collect();
1876 let outer = (0..areas.len())
1877 .max_by(|a, b| areas[*a].abs().total_cmp(&areas[*b].abs()))
1878 .unwrap_or(0);
1879 let outer_sign = areas.get(outer).copied().unwrap_or(1.0).signum();
1880
1881 let (va, vb) = match &planned.stretch {
1883 Stretch::Open(a, b) => (vertex_of(a)?, vertex_of(b)?),
1884 Stretch::Loop => {
1885 let p = cutter.point(planned.span.0, tol)?;
1886 let v = model.add_vertex(VertexData::new(p));
1887 (v.clone(), v)
1888 }
1889 };
1890 let (along, edges) = cut_edges(model, cutter, planned.span, &va, &vb, data.surface, tol)?;
1891 let back: Vec<Item> = along
1892 .iter()
1893 .rev()
1894 .map(|item| -> OgeomResult<Item> {
1895 let Some((from, _)) = edge_vertices(model, &item.edge.reversed())? else {
1896 ogeom_bail!(Construction, "a cut edge has no vertices");
1897 };
1898 Ok(Item {
1899 edge: item.edge.reversed(),
1900 from: from.node(),
1901 polyline: item.polyline.iter().rev().copied().collect(),
1902 })
1903 })
1904 .collect::<OgeomResult<_>>()?;
1905
1906 let mut pieces: Vec<(Vec<Item>, Vec<Vec<Item>>)> = Vec::new();
1908 let probe = |ring: &[Item]| ring[0].polyline[ring[0].polyline.len() / 2];
1909 match &planned.stretch {
1910 Stretch::Open(a, b) => {
1911 let (ra, ia) = junction(&items, va.node(), a.at)?;
1912 let (rb, ib) = junction(&items, vb.node(), b.at)?;
1913 if ra != rb {
1914 ogeom_bail!(
1915 Construction,
1916 "a curve running from one boundary loop of the face to \
1917 another does not divide it"
1918 );
1919 }
1920 let ring = &items[ra];
1921 let n = ring.len();
1922 let arc = |from: usize, to: usize, whole: bool| -> Vec<Item> {
1925 let mut out = Vec::new();
1926 let mut k = from;
1927 while out.len() < n && (k != to || (whole && out.is_empty())) {
1928 out.push(ring[k].clone());
1929 k = (k + 1) % n;
1930 }
1931 out
1932 };
1933 let mut first = arc(ia, ib, false);
1934 first.extend(back.iter().cloned());
1935 let mut second = arc(ib, ia, true);
1936 second.extend(along.iter().cloned());
1937 let (s1, s2) = (
1938 signed_area(&outline(&first)) * outer_sign,
1939 signed_area(&outline(&second)) * outer_sign,
1940 );
1941 if ra == outer {
1942 if s1 <= 0.0 || s2 <= 0.0 {
1943 ogeom_bail!(Construction, "the cut does not divide the face in two");
1944 }
1945 let (mut h1, mut h2) = (Vec::new(), Vec::new());
1946 let line1 = outline(&first);
1947 for (k, hole) in items.iter().enumerate() {
1948 if k == ra {
1949 continue;
1950 }
1951 if inside(std::slice::from_ref(&line1), probe(hole)) {
1952 h1.push(hole.clone());
1953 } else {
1954 h2.push(hole.clone());
1955 }
1956 }
1957 pieces.push((first, h1));
1958 pieces.push((second, h2));
1959 } else {
1960 let (fresh, hole) = match (s1 > 0.0, s2 > 0.0) {
1963 (true, false) => (first, second),
1964 (false, true) => (second, first),
1965 _ => ogeom_bail!(Construction, "the cut does not divide the face in two"),
1966 };
1967 let line = outline(&fresh);
1968 let (mut held, mut rest) = (Vec::new(), vec![hole]);
1969 for (k, ring) in items.iter().enumerate() {
1970 if k == ra || k == outer {
1971 continue;
1972 }
1973 if inside(std::slice::from_ref(&line), probe(ring)) {
1974 held.push(ring.clone());
1975 } else {
1976 rest.push(ring.clone());
1977 }
1978 }
1979 pieces.push((items[outer].clone(), rest));
1980 pieces.push((fresh, held));
1981 }
1982 }
1983 Stretch::Loop => {
1984 let (disk, hole) = if signed_area(&outline(&along)) * outer_sign > 0.0 {
1985 (along, back)
1986 } else {
1987 (back, along)
1988 };
1989 let line = outline(&disk);
1990 let (mut held, mut rest) = (Vec::new(), vec![hole]);
1991 for (k, ring) in items.iter().enumerate() {
1992 if k == outer {
1993 continue;
1994 }
1995 if inside(std::slice::from_ref(&line), probe(ring)) {
1996 held.push(ring.clone());
1997 } else {
1998 rest.push(ring.clone());
1999 }
2000 }
2001 pieces.push((items[outer].clone(), rest));
2002 pieces.push((disk, held));
2003 }
2004 }
2005
2006 let mut faces = Vec::with_capacity(pieces.len());
2007 for (outer_ring, holes) in pieces {
2008 let mut wires = vec![model.add_wire(&edges_of(&outer_ring))?];
2009 for hole in holes {
2010 wires.push(model.add_wire(&edges_of(&hole))?);
2011 }
2012 let mut fresh = data.clone();
2013 fresh.triangulation = None;
2014 fresh.natural_restriction = false;
2015 faces.push(model.add_face(fresh, &wires)?);
2016 }
2017 reshape.split(&face_fwd, faces);
2018 Ok(Made {
2019 reshape,
2020 edges,
2021 splits,
2022 })
2023}
2024
2025fn edges_of(ring: &[Item]) -> Vec<Shape> {
2026 ring.iter().map(|i| i.edge.clone()).collect()
2027}
2028
2029fn outline(ring: &[Item]) -> Vec<Point2> {
2030 ring.iter()
2031 .flat_map(|i| i.polyline.iter().copied())
2032 .collect()
2033}
2034
2035fn junction(items: &[Vec<Item>], v: TShapeId, at: Point2) -> OgeomResult<(usize, usize)> {
2038 let mut best: Option<(usize, usize, f64)> = None;
2039 for (r, ring) in items.iter().enumerate() {
2040 for (k, item) in ring.iter().enumerate() {
2041 if item.from != v {
2042 continue;
2043 }
2044 let d = item.polyline[0].distance(at);
2045 if best.is_none_or(|(.., held)| d < held) {
2046 best = Some((r, k, d));
2047 }
2048 }
2049 }
2050 best.map(|(r, k, _)| (r, k)).ok_or_else(|| {
2051 ogeom_err!(
2052 Construction,
2053 "a cut ends at a vertex the face does not have"
2054 )
2055 })
2056}
2057
2058type Walked = (Vec<Item>, Vec<(Shape, Shape)>);
2061
2062fn cut_edges(
2065 model: &mut Model,
2066 cutter: &Cutter,
2067 span: (f64, f64),
2068 from: &Shape,
2069 to: &Shape,
2070 surface: SurfaceId,
2071 tol: Tolerances,
2072) -> OgeomResult<Walked> {
2073 let eps = cutter.resolution(tol);
2074 let mut stops = vec![span.0];
2075 let mut joint = span.0.floor() + 1.0;
2076 while joint < span.1 - eps {
2077 if joint > span.0 + eps {
2078 stops.push(joint);
2079 }
2080 joint += 1.0;
2081 }
2082 stops.push(span.1);
2083 let mut vertices = vec![from.clone()];
2085 for &t in &stops[1..stops.len() - 1] {
2086 let before = cutter.locate(t - 0.5).0.point(1.0, tol)?;
2087 let after = cutter.point(t, tol)?;
2088 let at = Point::from_vector((before.to_vector() + after.to_vector()) * 0.5);
2089 vertices.push(model.add_vertex(VertexData::new(at)));
2090 }
2091 vertices.push(to.clone());
2092
2093 let mut items = Vec::with_capacity(stops.len() - 1);
2094 let mut edges = Vec::with_capacity(stops.len() - 1);
2095 for (k, w) in stops.windows(2).enumerate() {
2096 let (segment, f0) = cutter.locate(0.5 * (w[0] + w[1]));
2097 let base = 0.5f64.mul_add(-(w[1] - w[0]), f0);
2098 let (f0, f1) = (base, base + (w[1] - w[0]));
2099 let (t0, t1) = (segment.local(f0), segment.local(f1));
2100 let range = (t0.min(t1), t0.max(t1));
2101 let (start, end) = if t0 <= t1 {
2102 (&vertices[k], &vertices[k + 1])
2103 } else {
2104 (&vertices[k + 1], &vertices[k])
2105 };
2106 let curve_id = model.geometry_mut().add_curve(segment.curve.clone());
2107 let mut data = EdgeData::on_curve(curve_id, Location::identity(), range);
2108 let trace = model.geometry_mut().add_pcurve(segment.pcurve.clone());
2109 data.add(EdgeRepr::PCurve {
2110 curve: trace,
2111 surface,
2112 location: Location::identity(),
2113 range,
2114 });
2115 data.assert_same_parameter(true);
2116 let reach = vertex_point(model, start)?
2119 .distance(segment.curve.point_at(range.0, tol)?)
2120 .max(vertex_point(model, end)?.distance(segment.curve.point_at(range.1, tol)?));
2121 let edge = model.add_edge(data, &[start.clone(), end.clone()])?;
2122 model.widen(
2123 &edge,
2124 Tolerance::new(reach.max(segment.tolerance) + tol.confusion())?,
2125 )?;
2126 let walked = if t0 <= t1 {
2127 edge.clone()
2128 } else {
2129 edge.reversed()
2130 };
2131 let polyline: Vec<Point2> = (0..=32)
2132 .map(|i| segment.chart((f1 - f0).mul_add(f64::from(i) / 32.0, f0), tol))
2133 .collect::<OgeomResult<_>>()?;
2134 items.push(Item {
2135 edge: walked,
2136 from: vertices[k].node(),
2137 polyline,
2138 });
2139 edges.push((segment.source.clone(), edge));
2140 }
2141 Ok((items, edges))
2142}