use axiolid_brep::ExactBRep;
use axiolid_core::{Interval, Point2, Point3, Scalar, Tolerance};
use axiolid_curve::Curve3;
use axiolid_evaluate::surface::{locate, normal};
use axiolid_evaluate::{curve::locate3, evaluate3};
use axiolid_measure::FaceDomain;
use axiolid_nurbs::{
exact_curve_curve_intersection3, exact_curve_surface_intersection, exact_surface_intersection,
implicit_surface_intersection, spline_pair_intersection, ExactCurveIntersection,
ExactIntersectionRefusal,
};
use axiolid_surface::{Plane, Surface};
use axiolid_topology::FaceId;
use core::f64::consts::TAU;
use crate::support::{cleaned, same_support, window};
use crate::BooleanError;
const SAMPLE: Scalar = 0.414_213_562_373_095;
#[derive(Debug, Clone, PartialEq)]
pub struct SectionEdge {
pub face_a: FaceId,
pub face_b: FaceId,
pub curve: Curve3,
pub span: Interval,
pub start: Point3,
pub end: Point3,
pub along_a: bool,
pub along_b: bool,
pub other_a: Surface,
pub other_b: Surface,
}
pub fn section_edges(
a: &ExactBRep,
b: &ExactBRep,
tolerance: Tolerance,
) -> Result<Vec<SectionEdge>, BooleanError> {
let side_a = Side::new(a, tolerance)?;
let side_b = Side::new(b, tolerance)?;
let mut out = Vec::new();
#[allow(clippy::type_complexity)]
let mut closed_forms: Vec<(
Surface,
Surface,
Result<axiolid_nurbs::ExactIntersectionCurve, ExactIntersectionRefusal>,
)> = Vec::new();
for fa in 0..side_a.faces.len() {
for fb in 0..side_b.faces.len() {
let (sa, sb) = (side_a.surface(fa)?, side_b.surface(fb)?);
if same_support(sa, sb, tolerance) {
imprint(&side_a, fa, &side_b, fb, true, tolerance, &mut out)?;
imprint(&side_b, fb, &side_a, fa, false, tolerance, &mut out)?;
continue;
}
let splines = matches!((sa, sb), (Surface::BSpline(_), Surface::BSpline(_)));
let closed_form = if splines {
None
} else {
let known = closed_forms
.iter()
.find(|(a, b, _)| a == sa && b == sb)
.map(|(_, _, r)| r.clone());
let result = known.unwrap_or_else(|| {
let r = exact_surface_intersection(&cleaned(sa), &cleaned(sb));
closed_forms.push((sa.clone(), sb.clone(), r.clone()));
r
});
match result {
Ok(curve) => {
let conic = curve.branches.iter().zip(&curve.spans).all(|(b, s)| {
matches!(
(b, s),
(Curve3::Line(_), _)
| (Curve3::Circle(_) | Curve3::Ellipse(_), None)
)
});
conic.then_some(curve)
}
Err(
ExactIntersectionRefusal::Disjoint
| ExactIntersectionRefusal::NotRegularCurve,
) => continue,
Err(_) => None,
}
};
let branches: Vec<(Curve3, Option<Interval>)> = match closed_form {
Some(curve) => curve.branches.into_iter().zip(curve.spans).collect(),
None => {
let rank = |s: &Surface| match s {
Surface::BSpline(_) => 4,
Surface::Torus(_) => 3,
Surface::Sphere(_) => 2,
Surface::Plane(_) => 0,
_ => 1,
};
if let (Surface::BSpline(ba), Surface::BSpline(bb)) = (sa, sb) {
let (la, ha) = side_a.domains[fa].bounds();
let (lb, hb) = side_b.domains[fb].bounds();
match spline_pair_intersection(
ba,
bb,
Some((window(sa, la, ha), window(sb, lb, hb))),
) {
Ok(sections) => sections
.into_iter()
.map(|s| {
let end = s.end();
(Curve3::PairSection(s), Some(Interval::new(0.0, end)))
})
.collect(),
Err(ExactIntersectionRefusal::Disjoint) => continue,
Err(_) => return Err(BooleanError::UnsupportedSection),
}
} else {
let (carrier, other, side, face) = if rank(sa) >= rank(sb) {
(sa, sb, &side_a, fa)
} else {
(sb, sa, &side_b, fb)
};
let (lo, hi) = side.domains[face].bounds();
match implicit_surface_intersection(
carrier,
other,
Some(window(carrier, lo, hi)),
) {
Ok(sections) => sections
.into_iter()
.map(|s| {
let end = s.curve.end();
(Curve3::ImplicitSection(s), Some(Interval::new(0.0, end)))
})
.collect(),
Err(
ExactIntersectionRefusal::Disjoint
| ExactIntersectionRefusal::NotRegularCurve,
) => continue,
Err(_) => return Err(BooleanError::UnsupportedSection),
}
}
}
};
for (index, (branch, span)) in branches.iter().enumerate() {
let bounds = match (branch, span) {
(Curve3::Line(_), Some(span)) => Some(*span),
_ => None,
};
let branch = branch.clone();
let branch = &branch;
let (mut cuts, along_a) = side_a.cuts(fa, branch, sb, tolerance)?;
let (more, along_b) = side_b.cuts(fb, branch, sa, tolerance)?;
cuts.extend(more);
for (other, _) in branches
.iter()
.skip(index + 1)
.chain(branches.iter().take(index))
{
if let Ok(ExactCurveIntersection::Points(hits)) =
exact_curve_curve_intersection3(branch, other)
{
cuts.extend(hits.iter().map(|h| h.parameter.approx()));
}
}
if let Some(b) = bounds {
let (lo, hi) = (b.start.min(b.end), b.start.max(b.end));
cuts.retain(|&c| c >= lo && c <= hi);
cuts.extend([lo, hi].into_iter().filter(|x| x.is_finite()));
}
for (piece_curve, span) in pieces(branch, cuts)? {
let mid =
evaluate3(&piece_curve, span.start + SAMPLE * (span.end - span.start))
.map_err(|_| BooleanError::Evaluation)?;
let traced = matches!(
piece_curve,
Curve3::ImplicitSection(_) | Curve3::PairSection(_)
);
if !traced && touching(sa, sb, mid, tolerance)? {
continue;
}
let at_a = side_a.locate(fa, mid, &along_a, tolerance)?;
if at_a == Place::Outside {
continue;
}
let at_b = side_b.locate(fb, mid, &along_b, tolerance)?;
if at_b == Place::Outside {
continue;
}
out.push(SectionEdge {
face_a: side_a.faces[fa],
face_b: side_b.faces[fb],
start: evaluate3(&piece_curve, span.start)
.map_err(|_| BooleanError::Evaluation)?,
end: evaluate3(&piece_curve, span.end)
.map_err(|_| BooleanError::Evaluation)?,
curve: piece_curve,
span,
along_a: at_a == Place::Boundary,
along_b: at_b == Place::Boundary,
other_a: sb.clone(),
other_b: sa.clone(),
});
}
}
}
}
Ok(out)
}
fn poles(surface: &Surface) -> Vec<Point3> {
match surface {
Surface::Sphere(s) => {
let z = s.frame.z.normalize() * s.radius;
vec![s.frame.origin + z, s.frame.origin - z]
}
Surface::Cone(c) => {
let slope = c.semi_angle.tan();
if slope == 0.0 {
Vec::new()
} else {
vec![c.frame.origin - c.frame.z.normalize() * (c.radius / slope)]
}
}
_ => Vec::new(),
}
}
fn touching(
a: &Surface,
b: &Surface,
point: Point3,
tolerance: Tolerance,
) -> Result<bool, BooleanError> {
let at = |s: &Surface| -> Result<axiolid_core::Vec3, BooleanError> {
let (u, v) = locate(s, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
Ok(normal(s, u, v)
.map_err(|_| BooleanError::Evaluation)?
.normalize())
};
Ok(at(a)?.cross(at(b)?).length() <= 1e-9)
}
#[allow(clippy::too_many_arguments)]
fn imprint(
from: &Side<'_>,
face: usize,
onto: &Side<'_>,
other: usize,
first: bool,
tolerance: Tolerance,
out: &mut Vec<SectionEdge>,
) -> Result<(), BooleanError> {
for (edge, curve, span) in from.edges_of(face)? {
let bounding = from.cutter(face, edge, &curve, span, tolerance, false)?;
let (cuts, along) = onto.cuts(other, &curve, &bounding, tolerance)?;
for piece in pieces_within(&curve, span, cuts) {
let mid = evaluate3(&curve, piece.start + SAMPLE * (piece.end - piece.start))
.map_err(|_| BooleanError::Evaluation)?;
let at = onto.locate(other, mid, &along, tolerance)?;
if at == Place::Outside {
continue;
}
let (face_a, face_b, along_a, along_b) = if first {
(
from.faces[face],
onto.faces[other],
true,
at == Place::Boundary,
)
} else {
(
onto.faces[other],
from.faces[face],
at == Place::Boundary,
true,
)
};
out.push(SectionEdge {
face_a,
face_b,
curve: curve.clone(),
span: piece,
start: evaluate3(&curve, piece.start).map_err(|_| BooleanError::Evaluation)?,
end: evaluate3(&curve, piece.end).map_err(|_| BooleanError::Evaluation)?,
along_a,
along_b,
other_a: bounding.clone(),
other_b: bounding.clone(),
});
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Place {
Inside,
Boundary,
Outside,
}
fn pieces_within(curve: &Curve3, span: Interval, cuts: Vec<Scalar>) -> Vec<Interval> {
let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
let slack = 1e-12 * (1.0 + lo.abs().max(hi.abs()));
let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
let mut inside = vec![lo, hi];
for cut in cuts {
let candidates: &[Scalar] = if periodic {
&[cut - TAU, cut, cut + TAU, cut + 2.0 * TAU]
} else {
&[cut]
};
for &c in candidates {
if c > lo + slack && c < hi - slack {
inside.push(c);
}
}
}
inside.sort_by(Scalar::total_cmp);
inside.dedup_by(|x, y| (*x - *y).abs() <= 1e-12 * (1.0 + x.abs()));
inside
.windows(2)
.map(|pair| Interval::new(pair[0], pair[1]))
.collect()
}
fn pieces(curve: &Curve3, mut cuts: Vec<Scalar>) -> Result<Vec<(Curve3, Interval)>, BooleanError> {
cuts.sort_by(Scalar::total_cmp);
cuts.dedup_by(|x, y| (*x - *y).abs() <= 1e-12 * (1.0 + x.abs()));
let plain = |spans: Vec<Interval>| spans.into_iter().map(|s| (curve.clone(), s)).collect();
Ok(match curve {
Curve3::Line(_) => plain(
cuts.windows(2)
.map(|pair| Interval::new(pair[0], pair[1]))
.collect(),
),
Curve3::ImplicitSection(section) => {
let n = section.curve.end();
let (pu, pv) = section.carrier.periodic();
let closure = section.curve.closure(pu, pv);
let slack = 1e-9 * (1.0 + n);
let mut inner: Vec<Scalar> = cuts
.into_iter()
.filter(|&c| c > slack && c < n - slack)
.collect();
inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
let sub = if (a - b).abs() <= slack {
closure.and_then(|c| section.curve.rotated(a, c))
} else {
section.curve.sub(a, b, closure)
};
let sub = sub.ok_or(BooleanError::Evaluation)?;
let end = sub.end();
Ok((
Curve3::ImplicitSection(axiolid_curve::ImplicitSection3 {
carrier: section.carrier.clone(),
curve: sub,
}),
Interval::new(0.0, end),
))
};
if closure.is_some() {
if inner.is_empty() {
return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
}
let mut out = Vec::new();
for pair in inner.windows(2) {
out.push(own(pair[0], pair[1])?);
}
out.push(own(inner[inner.len() - 1], inner[0])?);
out
} else {
let mut ends = vec![0.0];
ends.extend(inner);
ends.push(n);
let mut out = Vec::new();
for pair in ends.windows(2) {
out.push(own(pair[0], pair[1])?);
}
out
}
}
Curve3::PairSection(section) => {
let n = section.end();
let slack = 1e-9 * (1.0 + n);
let mut inner: Vec<Scalar> = cuts
.into_iter()
.filter(|&c| c > slack && c < n - slack)
.collect();
inner.dedup_by(|x, y| (*x - *y).abs() <= slack);
let (first, last) = (
section.nodes[0].point,
section.nodes[section.nodes.len() - 1].point,
);
let closed = first == last;
let own = |a: Scalar, b: Scalar| -> Result<(Curve3, Interval), BooleanError> {
let sub = if a < b {
section.sub(a, b)
} else {
section
.sub(a, n)
.zip(section.sub(0.0, b))
.map(|(mut x, y)| {
x.nodes.extend(y.nodes.into_iter().skip(1));
x
})
};
let sub = sub.ok_or(BooleanError::Evaluation)?;
let end = sub.end();
Ok((Curve3::PairSection(sub), Interval::new(0.0, end)))
};
let mut out = Vec::new();
if closed {
if inner.is_empty() {
return Ok(vec![(curve.clone(), Interval::new(0.0, n))]);
}
for pair in inner.windows(2) {
out.push(own(pair[0], pair[1])?);
}
out.push(own(inner[inner.len() - 1], inner[0])?);
} else {
let mut ends = vec![0.0];
ends.extend(inner);
ends.push(n);
for pair in ends.windows(2) {
out.push(own(pair[0], pair[1])?);
}
}
out
}
_ => {
if cuts.is_empty() {
return Ok(vec![(curve.clone(), Interval::new(0.0, TAU))]);
}
let mut out: Vec<Interval> = cuts
.windows(2)
.map(|pair| Interval::new(pair[0], pair[1]))
.collect();
out.push(Interval::new(cuts[cuts.len() - 1], cuts[0] + TAU));
plain(out)
}
})
}
struct Side<'a> {
brep: &'a ExactBRep,
faces: Vec<FaceId>,
domains: Vec<FaceDomain<'a>>,
edge_faces: Vec<Vec<usize>>,
}
impl<'a> Side<'a> {
fn new(brep: &'a ExactBRep, tolerance: Tolerance) -> Result<Self, BooleanError> {
let topology = brep.topology();
let mut faces = Vec::with_capacity(topology.faces().len());
let mut domains = Vec::with_capacity(topology.faces().len());
for index in 0..topology.faces().len() {
let face = topology
.face_id_at(index)
.ok_or(BooleanError::DanglingReference)?;
let domain = FaceDomain::new(brep, face, tolerance)
.map_err(BooleanError::Measure)?
.ok_or(BooleanError::UnsupportedTrim)?;
faces.push(face);
domains.push(domain);
}
let mut edge_faces = vec![Vec::new(); topology.edges().len()];
for (index, face) in topology.faces().iter().enumerate() {
for bound in &face.bounds {
let wire = topology
.loops()
.get(bound.loop_id.index())
.ok_or(BooleanError::DanglingReference)?;
for use_ in &wire.edges {
edge_faces[use_.edge.index()].push(index);
}
}
}
Ok(Self {
brep,
faces,
domains,
edge_faces,
})
}
fn surface(&self, face: usize) -> Result<&'a Surface, BooleanError> {
self.brep.topology().faces()[face]
.surface
.and_then(|id| self.brep.surfaces().get(id.index()))
.ok_or(BooleanError::DanglingReference)
}
fn locate(
&self,
face: usize,
point: Point3,
along: &[(Curve3, Interval)],
tolerance: Tolerance,
) -> Result<Place, BooleanError> {
for (curve, span) in along {
if on_edge(curve, *span, point, tolerance)? {
return Ok(Place::Boundary);
}
}
let (u, v) =
locate(self.surface(face)?, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
match self.domains[face]
.contains(Point2::new(u, v))
.map_err(BooleanError::Measure)?
{
Some(true) => Ok(Place::Inside),
Some(false) => Ok(Place::Outside),
None => Err(BooleanError::Undecided),
}
}
fn edges_of(&self, face: usize) -> Result<Vec<(usize, Curve3, Interval)>, BooleanError> {
let topology = self.brep.topology();
let mut out = Vec::new();
let mut seen = Vec::new();
for bound in &topology.faces()[face].bounds {
let wire = topology
.loops()
.get(bound.loop_id.index())
.ok_or(BooleanError::DanglingReference)?;
for use_ in &wire.edges {
if seen.contains(&use_.edge) {
continue;
}
seen.push(use_.edge);
let curve = topology.edges()[use_.edge.index()]
.curve
.and_then(|id| self.brep.curves3().get(id.index()))
.ok_or(BooleanError::DanglingReference)?;
let span = self
.brep
.edge_interval(use_.edge)
.ok_or(BooleanError::DanglingReference)?;
out.push((use_.edge.index(), curve.clone(), span));
}
}
Ok(out)
}
#[allow(clippy::type_complexity)]
fn cuts(
&self,
face: usize,
curve: &Curve3,
meets: &Surface,
tolerance: Tolerance,
) -> Result<(Vec<Scalar>, Vec<(Curve3, Interval)>), BooleanError> {
let topology = self.brep.topology();
let mut out = Vec::new();
let mut along = Vec::new();
let mut seen = Vec::new();
for pole in poles(self.surface(face)?) {
if let Ok(t) = locate3(curve, pole, tolerance) {
let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
if (on - pole).length() <= tolerance.linear().max(1e-9) {
out.push(t);
}
}
}
for bound in &topology.faces()[face].bounds {
let wire = topology
.loops()
.get(bound.loop_id.index())
.ok_or(BooleanError::DanglingReference)?;
for use_ in &wire.edges {
if seen.contains(&use_.edge) {
continue;
}
seen.push(use_.edge);
let edge = &topology.edges()[use_.edge.index()];
let edge_curve = edge
.curve
.and_then(|id| self.brep.curves3().get(id.index()))
.ok_or(BooleanError::DanglingReference)?;
let span = self
.brep
.edge_interval(use_.edge)
.ok_or(BooleanError::DanglingReference)?;
let mut cutter =
self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, false)?;
let mut result = exact_curve_surface_intersection(curve, &cutter);
if matches!(result, Ok(ExactCurveIntersection::Contained)) {
if let Ok(seam) =
self.cutter(face, use_.edge.index(), edge_curve, span, tolerance, true)
{
cutter = seam;
result = exact_curve_surface_intersection(curve, &cutter);
}
}
match result {
Ok(ExactCurveIntersection::Points(hits)) => {
for hit in hits {
if on_edge(edge_curve, span, hit.point, tolerance)? {
out.push(hit.parameter.approx());
}
}
}
Ok(ExactCurveIntersection::Contained) => {
for t in [span.start, span.end] {
let end =
evaluate3(edge_curve, t).map_err(|_| BooleanError::Evaluation)?;
if let Ok(s) = locate3(curve, end, tolerance) {
let on =
evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
if (on - end).length() <= tolerance.linear().max(1e-9) {
out.push(s);
}
}
}
along.push((edge_curve.clone(), span));
}
Ok(_) => return Err(BooleanError::UnsupportedSection),
Err(_) => {
let hits = match exact_curve_surface_intersection(edge_curve, meets) {
Ok(ExactCurveIntersection::Points(hits)) => hits,
_ => return Err(BooleanError::UnsupportedTrim),
};
for hit in hits {
if !on_span(edge_curve, span, hit.point, tolerance)? {
continue;
}
if let Ok(s) = locate3(curve, hit.point, tolerance) {
let on =
evaluate3(curve, s).map_err(|_| BooleanError::Evaluation)?;
if (on - hit.point).length() <= tolerance.linear().max(1e-9) {
out.push(s);
}
}
}
}
}
}
}
Ok((out, along))
}
}
impl Side<'_> {
fn cutter(
&self,
face: usize,
edge: usize,
edge_curve: &Curve3,
span: Interval,
tolerance: Tolerance,
across_seam: bool,
) -> Result<Surface, BooleanError> {
let own = self.surface(face)?;
let adjacent = self.edge_faces[edge]
.iter()
.copied()
.find(|&other| other != face);
if let (Some(other), false) = (adjacent, across_seam) {
let theirs = self.surface(other)?;
if !same_support(own, theirs, tolerance) {
return Ok(theirs.clone());
}
}
if let Curve3::Circle(circle) = edge_curve {
return normal_sweep(own, circle, tolerance);
}
let Curve3::Line(line) = edge_curve else {
return Err(BooleanError::UnsupportedTrim);
};
let mid = evaluate3(edge_curve, 0.5 * (span.start + span.end))
.map_err(|_| BooleanError::Evaluation)?;
let (u, v) = locate(own, mid, tolerance).map_err(|_| BooleanError::Evaluation)?;
let n = normal(own, u, v).map_err(|_| BooleanError::Evaluation)?;
let across = line.direction.cross(n);
let length = across.length();
if length == 0.0 || !length.is_finite() {
return Err(BooleanError::UnsupportedTrim);
}
let z = across / length;
let x = line.direction.normalize();
Ok(Surface::Plane(Plane {
frame: axiolid_core::Frame3 {
origin: mid,
x,
y: z.cross(x),
z,
},
}))
}
}
fn normal_sweep(
surface: &Surface,
circle: &axiolid_curve::Circle3,
tolerance: Tolerance,
) -> Result<Surface, BooleanError> {
let f = circle.frame;
let axis = f.x.cross(f.y).normalize();
let x = f.x.normalize();
let y = axis.cross(x);
let p = f.origin + x * circle.radius;
let (u, v) = locate(surface, p, tolerance).map_err(|_| BooleanError::Evaluation)?;
let n = normal(surface, u, v)
.map_err(|_| BooleanError::Evaluation)?
.normalize();
let frame = axiolid_core::Frame3 {
origin: f.origin,
x,
y,
z: axis,
};
let along = n.dot(axis);
let radial = n.dot(x);
let plane = || {
Surface::Plane(Plane {
frame: axiolid_core::Frame3 {
origin: f.origin,
x,
y,
z: axis,
},
})
};
if along.abs() <= 1e-12 {
return Ok(plane());
}
if radial.abs() <= 1e-12 {
return Ok(Surface::Cylinder(axiolid_surface::Cylinder {
frame,
radius: circle.radius,
}));
}
Ok(Surface::Cone(axiolid_surface::Cone {
frame,
radius: circle.radius,
semi_angle: (radial / along).atan(),
}))
}
fn on_edge(
curve: &Curve3,
span: Interval,
point: Point3,
tolerance: Tolerance,
) -> Result<bool, BooleanError> {
let Ok(t) = locate3(curve, point, tolerance) else {
return Ok(false);
};
let on = evaluate3(curve, t).map_err(|_| BooleanError::Evaluation)?;
if (on - point).length() > tolerance.linear().max(1e-9) {
return Ok(false);
}
on_span(curve, span, point, tolerance)
}
fn on_span(
curve: &Curve3,
span: Interval,
point: Point3,
tolerance: Tolerance,
) -> Result<bool, BooleanError> {
let t = locate3(curve, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
let slack = 1e-9 * (1.0 + lo.abs().max(hi.abs()));
let periodic = matches!(curve, Curve3::Circle(_) | Curve3::Ellipse(_));
let candidates: &[Scalar] = if periodic {
&[t - TAU, t, t + TAU, t + 2.0 * TAU]
} else {
&[t]
};
Ok(candidates
.iter()
.any(|c| *c >= lo - slack && *c <= hi + slack))
}