1use axiolid_brep::ExactBRep;
30use axiolid_core::{Frame2, Interval, Point2, Point3, Scalar, Tolerance, Vec2};
31use axiolid_curve::{Curve2, Curve3, Ellipse2, Line2, Sinusoid2};
32use axiolid_evaluate::curve::{derivative2, evaluate2, locate2, locate3, second_derivative2};
33use axiolid_evaluate::evaluate3;
34use axiolid_evaluate::surface::locate;
35use axiolid_measure::FaceDomain;
36use axiolid_surface::Surface;
37use axiolid_topology::{EdgeId, FaceId, Orientation};
38use core::f64::consts::{PI, TAU};
39
40use crate::section::SectionEdge;
41use crate::support::{periods, window};
42use crate::BooleanError;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum PieceSource {
47 Boundary(EdgeId),
49 Section(usize),
51 Collapsed,
55}
56
57#[derive(Debug, Clone, PartialEq)]
60pub struct Piece {
61 pub curve: Curve3,
63 pub span: Interval,
66 pub pcurve: Curve2,
68 pub pspan: Interval,
70 pub source: PieceSource,
72}
73
74#[derive(Debug, Clone, PartialEq)]
76pub struct Region {
77 pub outer: Vec<Piece>,
79 pub holes: Vec<Vec<Piece>>,
81 pub against: bool,
85}
86
87pub fn split_face(
101 brep: &ExactBRep,
102 face: FaceId,
103 sections: &[SectionEdge],
104 first: bool,
105 cuts: &[Point3],
106 tolerance: Tolerance,
107) -> Result<Vec<Region>, BooleanError> {
108 let topology = brep.topology();
109 let record = topology
110 .faces()
111 .get(face.index())
112 .ok_or(BooleanError::DanglingReference)?;
113 let surface = record
114 .surface
115 .and_then(|id| brep.surfaces().get(id.index()))
116 .ok_or(BooleanError::DanglingReference)?;
117 let domain = FaceDomain::new(brep, face, tolerance)
118 .map_err(BooleanError::Measure)?
119 .ok_or(BooleanError::UnsupportedTrim)?;
120 let (lo, hi) = domain.bounds();
121
122 let mut pieces: Vec<(Piece, bool)> = Vec::new();
125 let mut ends: Vec<Point3> = cuts.to_vec();
126 let mut seen: Vec<&SectionEdge> = Vec::new();
129 let mut traces = Traces::default();
130 for (index, section) in sections.iter().enumerate() {
131 ends.push(section.start);
132 ends.push(section.end);
133 let (along, other) = if first {
134 (section.along_a, §ion.other_a)
135 } else {
136 (section.along_b, §ion.other_b)
137 };
138 if along
139 || seen
140 .iter()
141 .any(|other| same_stretch(other, section, tolerance))
142 {
143 continue;
144 }
145 seen.push(section);
146 let piece = section_piece(
147 surface,
148 other,
149 section,
150 index,
151 lo,
152 hi,
153 &mut traces,
154 tolerance,
155 )?;
156 pieces.push((piece, true));
157 }
158
159 for bound in &record.bounds {
161 let wire = topology
162 .loops()
163 .get(bound.loop_id.index())
164 .ok_or(BooleanError::DanglingReference)?;
165 let mut uses: Vec<Piece> = Vec::with_capacity(wire.edges.len());
166 for (index, use_) in wire.edges.iter().enumerate() {
167 let edge = &topology.edges()[use_.edge.index()];
168 let curve = edge
169 .curve
170 .and_then(|id| brep.curves3().get(id.index()))
171 .ok_or(BooleanError::DanglingReference)?
172 .clone();
173 let span = brep
174 .edge_interval(use_.edge)
175 .ok_or(BooleanError::DanglingReference)?;
176 let span = match use_.orientation {
177 Orientation::Forward => span,
178 Orientation::Reversed => Interval::new(span.end, span.start),
179 };
180 let pcurve = use_
181 .pcurve
182 .and_then(|id| brep.curves2().get(id.index()))
183 .ok_or(BooleanError::DanglingReference)?
184 .clone();
185 let pspan = brep
186 .pcurve_interval(bound.loop_id, index)
187 .ok_or(BooleanError::DanglingReference)?;
188 uses.push(Piece {
189 curve,
190 span,
191 pcurve,
192 pspan,
193 source: PieceSource::Boundary(use_.edge),
194 });
195 }
196 if bound.orientation == Orientation::Reversed {
197 uses.reverse();
198 for piece in &mut uses {
199 piece.span = Interval::new(piece.span.end, piece.span.start);
200 piece.pspan = Interval::new(piece.pspan.end, piece.pspan.start);
201 }
202 }
203 let closed = close_poles(surface, uses, tolerance)?;
204 for piece in closed {
205 for part in split_use(surface, piece, &ends, tolerance)? {
206 pieces.push((part, false));
207 }
208 }
209 }
210
211 let mut pole_ends: Vec<Point2> = Vec::new();
214 for (piece, section) in &pieces {
215 if *section {
216 for t in [piece.pspan.start, piece.pspan.end] {
217 pole_ends.push(evaluate2(&piece.pcurve, t).map_err(|_| BooleanError::Evaluation)?);
218 }
219 }
220 }
221 let mut split_pieces = Vec::with_capacity(pieces.len());
222 for (piece, section) in pieces {
223 if piece.source != PieceSource::Collapsed {
224 split_pieces.push((piece, section));
225 continue;
226 }
227 let (a, b) = (
228 evaluate2(&piece.pcurve, piece.pspan.start).map_err(|_| BooleanError::Evaluation)?,
229 evaluate2(&piece.pcurve, piece.pspan.end).map_err(|_| BooleanError::Evaluation)?,
230 );
231 let slack = 1e-9 * (1.0 + a.x.abs().max(b.x.abs()));
232 let mut cuts: Vec<Scalar> = pole_ends
233 .iter()
234 .filter(|p| (p.y - a.y).abs() <= slack)
235 .filter(|p| p.x > a.x.min(b.x) + slack && p.x < a.x.max(b.x) - slack)
236 .map(|p| (p.x - a.x) / (b.x - a.x))
237 .collect();
238 cuts.sort_by(Scalar::total_cmp);
239 cuts.dedup_by(|x, y| (*x - *y).abs() <= 1e-12);
240 let mut from = piece.pspan.start;
241 for c in cuts.into_iter().chain(std::iter::once(piece.pspan.end)) {
242 split_pieces.push((
243 Piece {
244 pspan: Interval::new(from, c),
245 ..piece.clone()
246 },
247 false,
248 ));
249 from = c;
250 }
251 }
252 let mut pieces = split_pieces;
253
254 let mut swept = 0.0;
258 for (piece, section) in &pieces {
259 if !section {
260 swept += sweep(piece)?;
261 }
262 }
263 let against = swept < 0.0;
264 if against {
265 for (piece, section) in &mut pieces {
266 if !*section {
267 *piece = directed(piece, true);
268 }
269 }
270 }
271 let mut regions = trace(&pieces)?;
272 for region in &mut regions {
273 region.against = against;
274 }
275 Ok(regions)
276}
277
278fn sweep(piece: &Piece) -> Result<Scalar, BooleanError> {
281 let n = 64;
282 let mut total = 0.0;
283 let mut previous =
284 evaluate2(&piece.pcurve, piece.pspan.start).map_err(|_| BooleanError::Evaluation)?;
285 for i in 1..=n {
286 let p =
287 piece.pspan.start + (piece.pspan.end - piece.pspan.start) * i as Scalar / n as Scalar;
288 let q = evaluate2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?;
289 total += 0.5 * (previous.x * q.y - q.x * previous.y);
290 previous = q;
291 }
292 Ok(total)
293}
294
295fn close_poles(
300 surface: &Surface,
301 uses: Vec<Piece>,
302 tolerance: Tolerance,
303) -> Result<Vec<Piece>, BooleanError> {
304 let n = uses.len();
305 let mut out = Vec::with_capacity(n + 2);
306 for i in 0..n {
307 let next = &uses[(i + 1) % n];
308 let a =
309 evaluate2(&uses[i].pcurve, uses[i].pspan.end).map_err(|_| BooleanError::Evaluation)?;
310 let b = evaluate2(&next.pcurve, next.pspan.start).map_err(|_| BooleanError::Evaluation)?;
311 out.push(uses[i].clone());
312 let slack = 1e-7 * (1.0 + a.x.abs().max(a.y.abs()));
313 if (a - b).length() <= slack {
314 continue;
315 }
316 let pa = axiolid_evaluate::surface::evaluate(surface, a.x, a.y)
317 .map_err(|_| BooleanError::Evaluation)?;
318 let pb = axiolid_evaluate::surface::evaluate(surface, b.x, b.y)
319 .map_err(|_| BooleanError::Evaluation)?;
320 let (su, sv) = axiolid_evaluate::surface::partials(surface, a.x, a.y)
321 .map_err(|_| BooleanError::Evaluation)?;
322 let scale = 1.0 + sv.length();
323 let pole =
324 (pa - pb).length() <= tolerance.linear().max(1e-9) && su.length() <= 1e-9 * scale;
325 if !pole {
326 return Err(BooleanError::UnclosedSplit);
329 }
330 out.push(Piece {
331 curve: Curve3::Line(axiolid_curve::Line3 {
332 origin: pa,
333 direction: axiolid_core::Vec3::ZERO,
334 }),
335 span: Interval::new(0.0, 1.0),
336 pcurve: Curve2::Line(Line2 {
337 origin: a,
338 direction: b - a,
339 }),
340 pspan: Interval::new(0.0, 1.0),
341 source: PieceSource::Collapsed,
342 });
343 }
344 Ok(out)
345}
346
347fn same_stretch(a: &SectionEdge, b: &SectionEdge, tolerance: Tolerance) -> bool {
349 let eps = tolerance.linear().max(1e-9);
350 let near = |p: Point3, q: Point3| (p - q).length() <= eps;
351 let mid = |e: &SectionEdge| evaluate3(&e.curve, 0.5 * (e.span.start + e.span.end));
352 let ends = (near(a.start, b.start) && near(a.end, b.end))
353 || (near(a.start, b.end) && near(a.end, b.start));
354 ends && matches!((mid(a), mid(b)), (Ok(p), Ok(q)) if near(p, q))
355}
356
357#[derive(Default)]
360struct Traces {
361 done: Vec<(Surface, Option<Vec<axiolid_curve::ImplicitCurve2>>)>,
362}
363
364impl Traces {
365 fn of(
366 &mut self,
367 surface: &Surface,
368 other: &Surface,
369 lo: Point2,
370 hi: Point2,
371 ) -> Result<&[axiolid_curve::ImplicitCurve2], BooleanError> {
372 let index = match self.done.iter().position(|(s, _)| s == other) {
373 Some(index) => index,
374 None => {
375 let curves =
376 axiolid_nurbs::trace_section_pcurves(surface, other, window(surface, lo, hi))
377 .ok();
378 self.done.push((other.clone(), curves));
379 self.done.len() - 1
380 }
381 };
382 self.done[index]
383 .1
384 .as_deref()
385 .ok_or(BooleanError::UnsupportedSplit)
386 }
387}
388
389#[allow(clippy::too_many_arguments)]
395fn section_piece(
396 surface: &Surface,
397 other: &Surface,
398 section: &SectionEdge,
399 index: usize,
400 lo: Point2,
401 hi: Point2,
402 traces: &mut Traces,
403 tolerance: Tolerance,
404) -> Result<Piece, BooleanError> {
405 let closed_form = iso_curve(surface, §ion.curve, tolerance)
406 || matches!(
407 (surface, §ion.curve),
408 (
409 Surface::Plane(_),
410 Curve3::Line(_) | Curve3::Circle(_) | Curve3::Ellipse(_)
411 ) | (
412 Surface::Cylinder(_),
413 Curve3::Line(_) | Curve3::Circle(_) | Curve3::Ellipse(_)
414 )
415 );
416 if !closed_form {
417 return implicit_piece(surface, other, section, index, lo, hi, traces, tolerance);
418 }
419 let first = match place(surface, section.start, lo, hi, tolerance) {
421 Ok(p) => p,
422 Err(_) => {
423 let t = section.span.start + 0.01 * (section.span.end - section.span.start);
424 let p = evaluate3(§ion.curve, t).map_err(|_| BooleanError::Evaluation)?;
425 place(surface, p, lo, hi, tolerance)?
426 }
427 };
428 let slack = 1e-9 * (1.0 + lo.x.abs().max(hi.x.abs()));
429 let mut candidates = vec![first];
430 if periods(surface).0 {
431 for shift in [TAU, -TAU] {
432 let other_turn = Point2::new(first.x + shift, first.y);
433 if other_turn.x >= lo.x - slack && other_turn.x <= hi.x + slack {
434 candidates.push(other_turn);
435 }
436 }
437 }
438 let mut fallback = None;
439 for start_uv in candidates {
440 let piece = section_piece_from(surface, section, index, start_uv, tolerance)?;
441 let mid = evaluate2(&piece.pcurve, 0.5 * (piece.pspan.start + piece.pspan.end))
442 .map_err(|_| BooleanError::Evaluation)?;
443 if mid.x >= lo.x - slack && mid.x <= hi.x + slack {
444 return Ok(piece);
445 }
446 fallback.get_or_insert(piece);
447 }
448 fallback.ok_or(BooleanError::Evaluation)
449}
450
451fn lifted_piece(
455 surface: &Surface,
456 section: &SectionEdge,
457 index: usize,
458 lo: Point2,
459 hi: Point2,
460 tolerance: Tolerance,
461) -> Result<Piece, BooleanError> {
462 let carrier = match surface {
463 Surface::Plane(p) => axiolid_curve::Carrier::Plane(p.frame),
464 Surface::Cylinder(c) => axiolid_curve::Carrier::Ruled(axiolid_curve::RuledCarrier {
465 frame: c.frame,
466 x_radius: c.radius,
467 y_radius: c.radius,
468 slope: 0.0,
469 }),
470 Surface::EllipticalCylinder(c) => {
471 axiolid_curve::Carrier::Ruled(axiolid_curve::RuledCarrier {
472 frame: c.frame,
473 x_radius: c.semi_axis_x,
474 y_radius: c.semi_axis_y,
475 slope: 0.0,
476 })
477 }
478 Surface::Cone(c) => axiolid_curve::Carrier::Ruled(axiolid_curve::RuledCarrier {
479 frame: c.frame,
480 x_radius: c.radius,
481 y_radius: c.radius,
482 slope: c.semi_angle.tan(),
483 }),
484 Surface::Sphere(s) => axiolid_curve::Carrier::Sphere {
485 frame: s.frame,
486 radius: s.radius,
487 },
488 Surface::Torus(t) => axiolid_curve::Carrier::Torus(axiolid_curve::TorusCarrier {
489 frame: t.frame,
490 major_radius: t.major_radius,
491 minor_radius: t.minor_radius,
492 }),
493 Surface::BSpline(b) => axiolid_curve::Carrier::Spline(Box::new(b.clone())),
496 _ => return Err(BooleanError::UnsupportedSplit),
497 };
498 if let Curve3::PairSection(pair) = §ion.curve {
499 let first = pair.side(&carrier).ok_or(BooleanError::UnsupportedSplit)?;
500 let (t0, t1) = (section.span.start, section.span.end);
501 let n = 4 * pair.nodes.len();
502 let mut guide = Vec::with_capacity(n + 1);
503 for i in 0..=n {
504 let (a, b, _) = pair
505 .solve(t0 + (t1 - t0) * i as Scalar / n as Scalar)
506 .ok_or(BooleanError::Evaluation)?;
507 guide.push(if first { a } else { b });
508 }
509 return Ok(Piece {
510 curve: section.curve.clone(),
511 span: section.span,
512 pcurve: Curve2::Lifted(axiolid_curve::LiftedCurve2 {
513 curve: Box::new(section.curve.clone()),
514 carrier,
515 start: t0,
516 end: t1,
517 guide,
518 }),
519 pspan: section.span,
520 source: PieceSource::Section(index),
521 });
522 }
523 let (pu, pv) = periods(surface);
524 let n = 128;
525 let (t0, t1) = (section.span.start, section.span.end);
526 let mut guide: Vec<Point2> = Vec::with_capacity(n + 1);
527 for i in 0..=n {
528 let t = t0 + (t1 - t0) * i as Scalar / n as Scalar;
529 let p = evaluate3(§ion.curve, t).map_err(|_| BooleanError::Evaluation)?;
530 let (mut u, mut v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
531 if let Some(last) = guide.last() {
532 if pu {
533 u += ((last.x - u) / TAU).round() * TAU;
534 }
535 if pv {
536 v += ((last.y - v) / TAU).round() * TAU;
537 }
538 }
539 guide.push(Point2::new(u, v));
540 }
541 let middle = guide[n / 2];
543 let into = |x: Scalar, a: Scalar, b: Scalar, periodic: bool| {
544 if !periodic || (x >= a - 1e-9 && x <= b + 1e-9) {
545 0.0
546 } else {
547 ((0.5 * (a + b) - x) / TAU).round() * TAU
548 }
549 };
550 let shift = Vec2::new(
551 into(middle.x, lo.x, hi.x, pu),
552 into(middle.y, lo.y, hi.y, pv),
553 );
554 for p in &mut guide {
555 *p += shift;
556 }
557 Ok(Piece {
558 curve: section.curve.clone(),
559 span: section.span,
560 pcurve: Curve2::Lifted(axiolid_curve::LiftedCurve2 {
561 curve: Box::new(section.curve.clone()),
562 carrier,
563 start: t0,
564 end: t1,
565 guide,
566 }),
567 pspan: section.span,
568 source: PieceSource::Section(index),
569 })
570}
571
572#[allow(clippy::too_many_arguments)]
576fn implicit_piece(
577 surface: &Surface,
578 other: &Surface,
579 section: &SectionEdge,
580 index: usize,
581 lo: Point2,
582 hi: Point2,
583 traces: &mut Traces,
584 tolerance: Tolerance,
585) -> Result<Piece, BooleanError> {
586 let uv = |p: Point3| -> Result<Point2, BooleanError> {
587 let (u, v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
588 Ok(Point2::new(u, v))
589 };
590 let at = |f: Scalar| {
591 evaluate3(
592 §ion.curve,
593 section.span.start + f * (section.span.end - section.span.start),
594 )
595 .map_err(|_| BooleanError::Evaluation)
596 };
597 let closed = (section.start - section.end).length() <= tolerance.linear().max(1e-9);
598 let curves = match traces.of(surface, other, lo, hi) {
599 Ok(curves) => curves,
600 Err(BooleanError::UnsupportedSplit) => {
605 return lifted_piece(surface, section, index, lo, hi, tolerance);
606 }
607 Err(e) => return Err(e),
608 };
609 let stretch = axiolid_nurbs::extract_stretch(
610 curves,
611 periods(surface),
612 uv(section.start)?,
613 [uv(at(1.0 / 3.0)?)?, uv(at(2.0 / 3.0)?)?],
614 uv(section.end)?,
615 closed,
616 )
617 .ok_or(BooleanError::UnsupportedSplit)?;
618 let (pu, pv) = periods(surface);
620 let middle = stretch
621 .point(0.5 * stretch.end())
622 .ok_or(BooleanError::Evaluation)?;
623 let into = |x: Scalar, a: Scalar, b: Scalar, periodic: bool| {
624 if !periodic || (x >= a - 1e-9 && x <= b + 1e-9) {
625 return 0.0;
626 }
627 let k = ((0.5 * (a + b) - x) / TAU).round();
628 k * TAU
629 };
630 let stretch = stretch.shifted(
631 into(middle.x, lo.x, hi.x, pu),
632 into(middle.y, lo.y, hi.y, pv),
633 );
634 let end = stretch.end();
635 Ok(Piece {
636 curve: section.curve.clone(),
637 span: section.span,
638 pcurve: Curve2::Implicit(stretch),
639 pspan: Interval::new(0.0, end),
640 source: PieceSource::Section(index),
641 })
642}
643
644fn iso_curve(surface: &Surface, curve: &Curve3, tolerance: Tolerance) -> bool {
649 let eps = tolerance.linear().max(1e-9);
650 let parallel = |a: axiolid_core::Vec3, b: axiolid_core::Vec3| {
651 a.normalize().cross(b.normalize()).length() <= 1e-12
652 };
653 let on_axis =
654 |p: Point3, o: Point3, z: axiolid_core::Vec3| (p - o).cross(z.normalize()).length() <= eps;
655 match (surface, curve) {
656 (Surface::Sphere(sp), Curve3::Circle(c)) => {
657 let n = c.frame.x.cross(c.frame.y);
658 let meridian = (c.frame.origin - sp.frame.origin).length() <= eps
659 && (c.radius - sp.radius).abs() <= eps
660 && n.normalize().dot(sp.frame.z.normalize()).abs() <= 1e-12;
661 let latitude =
662 parallel(n, sp.frame.z) && on_axis(c.frame.origin, sp.frame.origin, sp.frame.z);
663 meridian || latitude
664 }
665 (Surface::Cone(k), Curve3::Line(l)) => {
666 let slope = k.semi_angle.tan();
667 let apex = k.frame.origin - k.frame.z.normalize() * (k.radius / slope);
668 let d = l.direction.normalize();
669 let through = (apex - l.origin).cross(d).length() <= eps;
670 let axis = k.frame.z.normalize();
671 through && (d.dot(axis).abs() - k.semi_angle.cos().abs()).abs() <= 1e-12
672 }
673 (Surface::Cone(k), Curve3::Circle(c)) => {
674 let n = c.frame.x.cross(c.frame.y);
675 parallel(n, k.frame.z) && on_axis(c.frame.origin, k.frame.origin, k.frame.z)
676 }
677 (Surface::Torus(t), Curve3::Circle(c)) => {
678 let n = c.frame.x.cross(c.frame.y);
679 let z = t.frame.z.normalize();
680 let ring = parallel(n, z) && on_axis(c.frame.origin, t.frame.origin, z);
681 let d = c.frame.origin - t.frame.origin;
682 let tube = n.normalize().dot(z).abs() <= 1e-12
683 && d.dot(z).abs() <= eps
684 && (d.length() - t.major_radius).abs() <= eps
685 && (c.radius - t.minor_radius).abs() <= eps;
686 ring || tube
687 }
688 _ => false,
689 }
690}
691
692fn affine_pcurve(
696 surface: &Surface,
697 section: &SectionEdge,
698 start_uv: Point2,
699 tolerance: Tolerance,
700) -> Result<(Curve2, Interval), BooleanError> {
701 let (t0, t1) = (section.span.start, section.span.end);
702 let (ta, tb) = (t0 + 0.25 * (t1 - t0), t0 + 0.75 * (t1 - t0));
703 let (pu, pv) = periods(surface);
704 let uv = |t: Scalar, near: Point2| -> Result<Point2, BooleanError> {
705 let p = evaluate3(§ion.curve, t).map_err(|_| BooleanError::Evaluation)?;
706 let (mut u, mut v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
707 if pu {
708 u += ((near.x - u) / TAU).round() * TAU;
709 }
710 if pv {
711 v += ((near.y - v) / TAU).round() * TAU;
712 }
713 Ok(Point2::new(u, v))
714 };
715 let a = uv(ta, start_uv)?;
716 let b = uv(tb, a)?;
717 let direction = (b - a) / (tb - ta);
718 Ok((
719 Curve2::Line(Line2 {
720 origin: a - direction * ta,
721 direction,
722 }),
723 section.span,
724 ))
725}
726
727fn section_piece_from(
729 surface: &Surface,
730 section: &SectionEdge,
731 index: usize,
732 start_uv: Point2,
733 tolerance: Tolerance,
734) -> Result<Piece, BooleanError> {
735 let (pcurve, pspan) = match (surface, §ion.curve) {
736 _ if iso_curve(surface, §ion.curve, tolerance) => {
737 affine_pcurve(surface, section, start_uv, tolerance)?
738 }
739 (Surface::Plane(p), curve) => {
740 let f = p.frame;
741 let local = |q: Point3| Point2::new((q - f.origin).dot(f.x), (q - f.origin).dot(f.y));
742 let dir = |d: axiolid_core::Vec3| Vec2::new(d.dot(f.x), d.dot(f.y));
743 let pcurve = match curve {
744 Curve3::Line(l) => Curve2::Line(Line2 {
745 origin: local(l.origin),
746 direction: dir(l.direction),
747 }),
748 Curve3::Circle(c) => Curve2::Ellipse(Ellipse2 {
749 frame: Frame2 {
750 origin: local(c.frame.origin),
751 x: dir(c.frame.x),
752 y: dir(c.frame.y),
753 },
754 semi_axis_x: c.radius,
755 semi_axis_y: c.radius,
756 }),
757 Curve3::Ellipse(e) => Curve2::Ellipse(Ellipse2 {
758 frame: Frame2 {
759 origin: local(e.frame.origin),
760 x: dir(e.frame.x),
761 y: dir(e.frame.y),
762 },
763 semi_axis_x: e.semi_axis_x,
764 semi_axis_y: e.semi_axis_y,
765 }),
766 _ => return Err(BooleanError::UnsupportedSplit),
767 };
768 (pcurve, section.span)
770 }
771 (Surface::Cylinder(c), Curve3::Line(l)) => {
772 let pcurve = Curve2::Line(Line2 {
774 origin: start_uv - Vec2::new(0.0, l.direction.dot(c.frame.z)) * section.span.start,
775 direction: Vec2::new(0.0, l.direction.dot(c.frame.z)),
776 });
777 (pcurve, section.span)
778 }
779 (Surface::Cylinder(c), Curve3::Circle(circle)) => {
780 let turn = circle.frame.x.cross(circle.frame.y).dot(c.frame.z).signum();
783 let pcurve = Curve2::Line(Line2 {
784 origin: start_uv - Vec2::new(turn, 0.0) * section.span.start,
785 direction: Vec2::new(turn, 0.0),
786 });
787 (pcurve, section.span)
788 }
789 (Surface::Cylinder(c), Curve3::Ellipse(ellipse)) => {
790 let n = ellipse.frame.x.cross(ellipse.frame.y);
793 let nz = n.dot(c.frame.z);
794 if nz == 0.0 {
795 return Err(BooleanError::UnsupportedSplit);
796 }
797 let wave = Sinusoid2 {
798 mean: n.dot(ellipse.frame.origin - c.frame.origin) / nz,
799 cosine: -c.radius * n.dot(c.frame.x) / nz,
800 sine: -c.radius * n.dot(c.frame.y) / nz,
801 };
802 let pcurve = Curve2::Sinusoid(wave);
803 let span = angle_span(surface, section, start_uv.x, tolerance)?;
804 (pcurve, span)
805 }
806 _ => return Err(BooleanError::UnsupportedSplit),
807 };
808 Ok(Piece {
809 curve: section.curve.clone(),
810 span: section.span,
811 pcurve,
812 pspan,
813 source: PieceSource::Section(index),
814 })
815}
816
817fn angle_span(
820 surface: &Surface,
821 section: &SectionEdge,
822 start: Scalar,
823 tolerance: Tolerance,
824) -> Result<Interval, BooleanError> {
825 let at = |t: Scalar| -> Result<Scalar, BooleanError> {
826 let p = evaluate3(§ion.curve, t).map_err(|_| BooleanError::Evaluation)?;
827 Ok(locate(surface, p, tolerance)
828 .map_err(|_| BooleanError::Evaluation)?
829 .0)
830 };
831 let near = |raw: Scalar, reference: Scalar| raw + TAU * ((reference - raw) / TAU).round();
832 let (t0, t1) = (section.span.start, section.span.end);
833 let full = (t1 - t0).abs() >= TAU - 1e-12;
834 let q1 = near(at(t0 + 0.25 * (t1 - t0))?, start);
835 let q2 = near(at(t0 + 0.5 * (t1 - t0))?, q1);
836 let q3 = near(at(t0 + 0.75 * (t1 - t0))?, q2);
837 let end = if full {
838 start + TAU * (q1 - start).signum()
839 } else {
840 near(at(t1)?, q3)
841 };
842 Ok(Interval::new(start, end))
843}
844
845fn place(
848 surface: &Surface,
849 point: Point3,
850 lo: Point2,
851 hi: Point2,
852 tolerance: Tolerance,
853) -> Result<Point2, BooleanError> {
854 let (mut u, mut v) = locate(surface, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
855 let (pu, pv) = periods(surface);
856 let slack = 1e-9;
857 let wrap = |x: &mut Scalar, a: Scalar, b: Scalar| {
858 while *x < a - slack {
859 *x += TAU;
860 }
861 while *x > b + slack {
862 *x -= TAU;
863 }
864 };
865 if pu {
866 wrap(&mut u, lo.x, hi.x);
867 }
868 if pv {
869 wrap(&mut v, lo.y, hi.y);
870 }
871 Ok(Point2::new(u, v))
872}
873
874fn split_use(
876 surface: &Surface,
877 piece: Piece,
878 ends: &[Point3],
879 tolerance: Tolerance,
880) -> Result<Vec<Piece>, BooleanError> {
881 let mut cuts: Vec<(Scalar, Scalar)> = Vec::new();
882 let (lo, hi) = (
883 piece.span.start.min(piece.span.end),
884 piece.span.start.max(piece.span.end),
885 );
886 let slack = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
887 for &point in ends {
888 let Ok(t) = locate3(&piece.curve, point, tolerance) else {
889 continue;
890 };
891 let t = if matches!(piece.curve, Curve3::Circle(_) | Curve3::Ellipse(_)) {
892 [t - TAU, t, t + TAU, t + 2.0 * TAU]
893 .into_iter()
894 .find(|c| *c > lo + slack && *c < hi - slack)
895 } else {
896 (t > lo + slack && t < hi - slack).then_some(t)
897 };
898 let Some(t) = t else { continue };
899 let on = evaluate3(&piece.curve, t).map_err(|_| BooleanError::Evaluation)?;
900 if (on - point).length() > tolerance.linear().max(1e-9) {
901 continue;
902 }
903 let (u, v) = locate(surface, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
905 let (pu, pv) = periods(surface);
906 let turns: &[Scalar] = &[0.0, TAU, -TAU, 2.0 * TAU, -2.0 * TAU];
907 let mut shifts = Vec::new();
908 for &a in if pu { turns } else { &turns[..1] } {
909 for &b in if pv { turns } else { &turns[..1] } {
910 shifts.push((a, b));
911 }
912 }
913 let mut found = None;
914 for (du, dv) in shifts {
915 if let Ok(p) = locate2(&piece.pcurve, Point2::new(u + du, v + dv), tolerance) {
916 let (plo, phi) = (
917 piece.pspan.start.min(piece.pspan.end),
918 piece.pspan.start.max(piece.pspan.end),
919 );
920 let candidates = [p - TAU, p, p + TAU];
921 if let Some(p) = candidates
922 .into_iter()
923 .find(|c| *c >= plo - slack && *c <= phi + slack)
924 {
925 found = Some(p);
926 break;
927 }
928 }
929 }
930 let p = found.ok_or(BooleanError::Evaluation)?;
931 if !cuts.iter().any(|(c, _)| (c - t).abs() <= slack) {
932 cuts.push((t, p));
933 }
934 }
935 if cuts.is_empty() {
936 return Ok(vec![piece]);
937 }
938 let forward = piece.span.end >= piece.span.start;
939 cuts.sort_by(|a, b| {
940 let order = a.0.total_cmp(&b.0);
941 if forward {
942 order
943 } else {
944 order.reverse()
945 }
946 });
947 let mut out = Vec::with_capacity(cuts.len() + 1);
948 let (mut t0, mut p0) = (piece.span.start, piece.pspan.start);
949 for (t, p) in cuts {
950 out.push(Piece {
951 span: Interval::new(t0, t),
952 pspan: Interval::new(p0, p),
953 ..piece.clone()
954 });
955 (t0, p0) = (t, p);
956 }
957 out.push(Piece {
958 span: Interval::new(t0, piece.span.end),
959 pspan: Interval::new(p0, piece.pspan.end),
960 ..piece
961 });
962 Ok(out)
963}
964
965#[derive(Debug, Clone, Copy)]
967struct Half {
968 piece: usize,
969 reversed: bool,
970 from: usize,
971 to: usize,
972 leave: Scalar,
974 arrive: Scalar,
975 bend_leave: Scalar,
978 bend_arrive: Scalar,
979 probe_leave: [Point2; 3],
984 probe_arrive: [Point2; 3],
985}
986
987const PROBES: [Scalar; 3] = [1e-4, 1e-3, 1e-2];
989
990fn directed(piece: &Piece, reversed: bool) -> Piece {
991 if reversed {
992 Piece {
993 span: Interval::new(piece.span.end, piece.span.start),
994 pspan: Interval::new(piece.pspan.end, piece.pspan.start),
995 ..piece.clone()
996 }
997 } else {
998 piece.clone()
999 }
1000}
1001
1002fn angle_of(v: Vec2) -> Scalar {
1003 v.y.atan2(v.x)
1004}
1005
1006fn tangent(piece: &Piece, at_start: bool) -> Result<Vec2, BooleanError> {
1009 let p = if at_start {
1010 piece.pspan.start
1011 } else {
1012 piece.pspan.end
1013 };
1014 let d = derivative2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?;
1015 let sign = if piece.pspan.end >= piece.pspan.start {
1016 1.0
1017 } else {
1018 -1.0
1019 };
1020 Ok(d * sign)
1021}
1022
1023fn bend(piece: &Piece, at_start: bool) -> Result<Scalar, BooleanError> {
1026 let p = if at_start {
1027 piece.pspan.start
1028 } else {
1029 piece.pspan.end
1030 };
1031 let d = derivative2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?;
1032 let dd = second_derivative2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?;
1033 let speed = d.length();
1034 if speed == 0.0 {
1035 return Err(BooleanError::Evaluation);
1036 }
1037 let sign = if piece.pspan.end >= piece.pspan.start {
1039 1.0
1040 } else {
1041 -1.0
1042 };
1043 Ok(sign * (d.x * dd.y - d.y * dd.x) / (speed * speed * speed))
1044}
1045
1046fn trace(pieces: &[(Piece, bool)]) -> Result<Vec<Region>, BooleanError> {
1047 let mut vertices: Vec<Point2> = Vec::new();
1049 let mut vertex = |p: Point2| -> usize {
1050 let slack = 1e-7 * (1.0 + p.x.abs().max(p.y.abs()));
1051 if let Some(i) = vertices.iter().position(|q| (*q - p).length() <= slack) {
1052 return i;
1053 }
1054 vertices.push(p);
1055 vertices.len() - 1
1056 };
1057 let mut halves = Vec::new();
1058 for (index, (piece, both_ways)) in pieces.iter().enumerate() {
1059 let a =
1060 evaluate2(&piece.pcurve, piece.pspan.start).map_err(|_| BooleanError::Evaluation)?;
1061 let b = evaluate2(&piece.pcurve, piece.pspan.end).map_err(|_| BooleanError::Evaluation)?;
1062 let (va, vb) = (vertex(a), vertex(b));
1063 let leave = angle_of(tangent(piece, true)?);
1064 let arrive = angle_of(tangent(piece, false)?);
1065 let (bend_leave, bend_arrive) = (bend(piece, true)?, bend(piece, false)?);
1066 let (t0, t1) = (piece.pspan.start, piece.pspan.end);
1067 let probe = |f: Scalar| -> Result<Point2, BooleanError> {
1068 evaluate2(&piece.pcurve, t0 + (t1 - t0) * f).map_err(|_| BooleanError::Evaluation)
1069 };
1070 let near_start = [probe(PROBES[0])?, probe(PROBES[1])?, probe(PROBES[2])?];
1071 let near_end = [
1072 probe(1.0 - PROBES[0])?,
1073 probe(1.0 - PROBES[1])?,
1074 probe(1.0 - PROBES[2])?,
1075 ];
1076 halves.push(Half {
1077 piece: index,
1078 reversed: false,
1079 from: va,
1080 to: vb,
1081 leave,
1082 arrive,
1083 bend_leave,
1084 bend_arrive,
1085 probe_leave: near_start,
1086 probe_arrive: near_end,
1087 });
1088 if *both_ways {
1089 halves.push(Half {
1090 piece: index,
1091 reversed: true,
1092 from: vb,
1093 to: va,
1094 leave: arrive + PI,
1095 arrive: leave + PI,
1096 bend_leave: -bend_arrive,
1097 bend_arrive: -bend_leave,
1098 probe_leave: near_end,
1099 probe_arrive: near_start,
1100 });
1101 }
1102 }
1103
1104 let mut used = vec![false; halves.len()];
1106 let mut loops: Vec<Vec<usize>> = Vec::new();
1107 for first in 0..halves.len() {
1108 if used[first] {
1109 continue;
1110 }
1111 let mut walk = Vec::new();
1112 let mut current = first;
1113 loop {
1114 if used[current] {
1115 if current == first {
1116 break;
1117 }
1118 return Err(BooleanError::UnclosedSplit);
1119 }
1120 used[current] = true;
1121 walk.push(current);
1122 let here = halves[current];
1123 let back = here.arrive + PI;
1128 let back_bend = -here.bend_arrive;
1129 let at = vertices[here.to];
1130 let chord_turns = |c: &Half| -> [Scalar; 3] {
1133 [0, 1, 2].map(|k| {
1134 let (b, o) = (here.probe_arrive[k] - at, c.probe_leave[k] - at);
1135 (angle_of(b) - angle_of(o)).rem_euclid(TAU)
1136 })
1137 };
1138 let mut keyed: Vec<((Scalar, Scalar), usize)> = Vec::new();
1139 for (index, candidate) in halves.iter().enumerate() {
1140 if candidate.from != here.to {
1141 continue;
1142 }
1143 let is_twin = candidate.piece == here.piece && candidate.reversed != here.reversed;
1144 let turn = (back - candidate.leave).rem_euclid(TAU);
1145 let delta = back_bend - candidate.bend_leave;
1148 let key = if is_twin {
1149 (TAU, Scalar::INFINITY)
1150 } else if !(1e-9..=TAU - 1e-9).contains(&turn) {
1151 if delta.abs() <= 1e-9 * (1.0 + back_bend.abs()) {
1152 let c = chord_turns(candidate);
1155 let Some(t) = c.into_iter().find(|t| *t > 1e-12 && *t < TAU - 1e-12) else {
1156 return Err(BooleanError::TangentSplit);
1157 };
1158 keyed.push(((if t < PI { t.min(1e-10) } else { TAU }, t), index));
1159 continue;
1160 }
1161 if delta > 0.0 {
1162 (0.0, delta)
1163 } else {
1164 (TAU, delta)
1165 }
1166 } else {
1167 (turn, delta)
1168 };
1169 keyed.push((key, index));
1170 }
1171 let same_turn = |x: Scalar, y: Scalar| (x - y).abs() < 1e-9;
1172 let same_bend =
1173 |x: Scalar, y: Scalar| (x - y).abs() <= 1e-9 * (1.0 + x.abs().min(1e12));
1174 let chords = |a: usize, b: usize| -> core::cmp::Ordering {
1177 let (ca, cb) = (chord_turns(&halves[a]), chord_turns(&halves[b]));
1178 for k in 0..3 {
1179 if (ca[k] - cb[k]).abs() > 1e-12 {
1180 return ca[k].total_cmp(&cb[k]);
1181 }
1182 }
1183 core::cmp::Ordering::Equal
1184 };
1185 let order = |x: &((Scalar, Scalar), usize), y: &((Scalar, Scalar), usize)| {
1186 let ((xt, xb), (yt, yb)) = (x.0, y.0);
1187 if !same_turn(xt, yt) {
1188 xt.total_cmp(&yt)
1189 } else if !same_bend(xb, yb) {
1190 xb.total_cmp(&yb)
1191 } else {
1192 chords(x.1, y.1)
1193 }
1194 };
1195 keyed.sort_by(order);
1196 if let [first_entry, second_entry, ..] = keyed.as_slice() {
1197 let ((ft, fb), (st, sb)) = (first_entry.0, second_entry.0);
1198 if same_turn(ft, st)
1199 && same_bend(fb, sb)
1200 && ft < TAU
1201 && chords(first_entry.1, second_entry.1) == core::cmp::Ordering::Equal
1202 {
1203 return Err(BooleanError::TangentSplit);
1204 }
1205 }
1206 let best = keyed.first().copied();
1207 current = best.ok_or(BooleanError::UnclosedSplit)?.1;
1208 }
1209 loops.push(walk);
1210 }
1211
1212 let as_pieces = |walk: &[usize]| -> Vec<Piece> {
1214 walk.iter()
1215 .map(|&h| directed(&pieces[halves[h].piece].0, halves[h].reversed))
1216 .collect()
1217 };
1218 let mut outers: Vec<(Vec<Piece>, Scalar, Vec<Point2>)> = Vec::new();
1219 let mut holes: Vec<(Vec<Piece>, Vec<Point2>)> = Vec::new();
1220 for walk in &loops {
1221 let loop_pieces = as_pieces(walk);
1222 let polygon = sample(&loop_pieces)?;
1223 let area = shoelace(&polygon);
1224 if area > 0.0 {
1225 outers.push((loop_pieces, area, polygon));
1226 } else {
1227 holes.push((loop_pieces, polygon));
1228 }
1229 }
1230 let mut regions: Vec<Region> = outers
1231 .iter()
1232 .map(|(pieces, _, _)| Region {
1233 outer: pieces.clone(),
1234 holes: Vec::new(),
1235 against: false,
1236 })
1237 .collect();
1238 for (hole, polygon) in holes {
1239 let piece = &hole[0];
1243 let t = 0.5 * (piece.pspan.start + piece.pspan.end);
1244 let at = evaluate2(&piece.pcurve, t).map_err(|_| BooleanError::Evaluation)?;
1245 let mut d = derivative2(&piece.pcurve, t).map_err(|_| BooleanError::Evaluation)?;
1246 if piece.pspan.end < piece.pspan.start {
1247 d = -d;
1248 }
1249 let size = {
1250 let (mut lo, mut hi) = (polygon[0], polygon[0]);
1251 for p in &polygon {
1252 lo = lo.min(*p);
1253 hi = hi.max(*p);
1254 }
1255 (hi - lo).length()
1256 };
1257 let left = Vec2::new(-d.y, d.x).normalize_or_zero();
1258 let probe = at + left * (1e-6 * size.max(1e-9));
1259 let owner = outers
1260 .iter()
1261 .enumerate()
1262 .filter(|(_, (_, _, outer))| inside_polygon(outer, probe))
1263 .min_by(|a, b| a.1 .1.total_cmp(&b.1 .1))
1264 .map(|(index, _)| index)
1265 .ok_or(BooleanError::UnclosedSplit)?;
1266 regions[owner].holes.push(hole);
1267 }
1268 Ok(regions)
1269}
1270
1271fn sample(pieces: &[Piece]) -> Result<Vec<Point2>, BooleanError> {
1273 let mut out = Vec::new();
1274 for piece in pieces {
1275 for i in 0..32 {
1276 let p = piece.pspan.start + (piece.pspan.end - piece.pspan.start) * i as Scalar / 32.0;
1277 out.push(evaluate2(&piece.pcurve, p).map_err(|_| BooleanError::Evaluation)?);
1278 }
1279 }
1280 Ok(out)
1281}
1282
1283fn shoelace(polygon: &[Point2]) -> Scalar {
1284 let mut area = 0.0;
1285 for i in 0..polygon.len() {
1286 let (p, q) = (polygon[i], polygon[(i + 1) % polygon.len()]);
1287 area += p.x * q.y - q.x * p.y;
1288 }
1289 0.5 * area
1290}
1291
1292pub(crate) fn inside_polygon(polygon: &[Point2], p: Point2) -> bool {
1293 let mut inside = false;
1294 for i in 0..polygon.len() {
1295 let (a, b) = (polygon[i], polygon[(i + 1) % polygon.len()]);
1296 if (a.y > p.y) != (b.y > p.y) {
1297 let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
1298 if x > p.x {
1299 inside = !inside;
1300 }
1301 }
1302 }
1303 inside
1304}