use crate::boolean::{boolean_operation, BooleanOperation, BooleanOptions};
use crate::spatial::Aabb;
use crate::topology::{make_box_brep, make_cylinder_brep, BrepSolid};
use crate::transform_topology::{transform_brep, AffineTransform};
use crate::{
make_cone_brep, make_sphere_brep, make_torus_brep, AnalyticSurface, NurbsSurface, Vec3,
};
use serde::Deserialize;
fn solid_aabb(solid: &BrepSolid) -> Aabb {
let mut bounds = Aabb::empty();
for vertex in &solid.vertices {
bounds.include_point(vertex.point);
}
bounds
}
fn is_empty_piece(solid: &BrepSolid) -> bool {
solid.shells.is_empty() || solid.shells.iter().all(|shell| shell.faces.is_empty())
}
fn frame_transform(u: Vec3, v: Vec3, n: Vec3, center: Vec3) -> Result<AffineTransform, String> {
AffineTransform::new([
u.x, v.x, n.x, center.x, u.y, v.y, n.y, center.y, u.z, v.z, n.z, center.z, 0.0, 0.0, 0.0, 1.0,
])
}
pub fn split_solid_by_plane(
solid: &BrepSolid,
plane_point: Vec3,
plane_normal: Vec3,
) -> Result<(BrepSolid, BrepSolid), String> {
let n = plane_normal.normalized()?;
let u = n.perpendicular()?;
let v = n.cross(u);
let bounds = solid_aabb(solid);
if !bounds.minimum.x.is_finite() {
return Err("split_solid_by_plane: solid has no geometry".into());
}
let diagonal = bounds.diagonal();
if diagonal <= 0.0 {
return Err("split_solid_by_plane: solid is degenerate".into());
}
let length = 3.0 * diagonal;
let half = 0.5 * length;
let center = bounds.minimum.add(bounds.maximum).scale(0.5);
let center_on_plane = center.sub(n.scale(center.sub(plane_point).dot(n)));
let cube = make_box_brep(Vec3::new(-half, -half, -half), length, length, length)?;
let below_center = center_on_plane.sub(n.scale(half));
let tool_below = transform_brep(&cube, frame_transform(u, v, n, below_center)?, false)?;
let above_center = center_on_plane.add(n.scale(half));
let tool_above = transform_brep(&cube, frame_transform(u, v, n, above_center)?, false)?;
let options = BooleanOptions::default();
let below = boolean_operation(solid, &tool_below, BooleanOperation::Intersect, &options);
let above = boolean_operation(solid, &tool_above, BooleanOperation::Intersect, &options);
match (below, above) {
(Ok(below), Ok(above)) if !is_empty_piece(&below) && !is_empty_piece(&above) => {
Ok((below, above))
}
_ => Err("split_solid_by_plane: plane does not intersect the solid".into()),
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SplitSurface {
Plane { point: Vec3, normal: Vec3 },
Cylinder {
axis_point: Vec3,
axis_dir: Vec3,
radius: f64,
},
Sphere { center: Vec3, radius: f64 },
Cone {
apex: Vec3,
axis_dir: Vec3,
half_angle: f64,
},
Torus {
center: Vec3,
axis_dir: Vec3,
major_radius: f64,
minor_radius: f64,
},
}
fn solid_extent(solid: &BrepSolid) -> Result<(Aabb, f64), String> {
let bounds = solid_aabb(solid);
if !bounds.minimum.x.is_finite() {
return Err("split_solid_by_surface: solid has no geometry".into());
}
let diagonal = bounds.diagonal();
if diagonal <= 0.0 {
return Err("split_solid_by_surface: solid is degenerate".into());
}
Ok((bounds, diagonal))
}
fn build_tool_solid(solid: &BrepSolid, tool: &SplitSurface) -> Result<BrepSolid, String> {
let (_, diagonal) = solid_extent(solid)?;
let margin = diagonal.max(1.0);
match *tool {
SplitSurface::Plane { .. } => {
Err("build_tool_solid: plane is handled by the plane path".into())
}
SplitSurface::Cylinder {
axis_point,
axis_dir,
radius,
} => {
if radius <= 0.0 {
return Err("split_solid_by_surface: cylinder radius must be positive".into());
}
let axis = axis_dir.normalized()?;
let (t_min, t_max) = axis_span(solid, axis_point, axis);
let base = axis_point.add(axis.scale(t_min - margin));
let height = (t_max - t_min) + 2.0 * margin;
make_cylinder_brep(base, axis, radius, height)
}
SplitSurface::Sphere { center, radius } => {
if radius <= 0.0 {
return Err("split_solid_by_surface: sphere radius must be positive".into());
}
make_sphere_brep(center, radius, Vec3::new(0.0, 0.0, 1.0))
}
SplitSurface::Cone {
apex,
axis_dir,
half_angle,
} => {
if !(half_angle > 0.0 && half_angle < std::f64::consts::FRAC_PI_2) {
return Err("split_solid_by_surface: cone half-angle must be in (0, pi/2)".into());
}
let axis = axis_dir.normalized()?;
let (_, d_max) = axis_span(solid, apex, axis);
if d_max <= 0.0 {
return Err(
"split_solid_by_surface: cone does not reach the solid (body is behind the apex)"
.into(),
);
}
let big_h = d_max + margin;
let base = apex.add(axis.scale(big_h));
let base_radius = big_h * half_angle.tan();
make_cone_brep(base, axis.scale(-1.0), base_radius, 0.0, big_h)
}
SplitSurface::Torus {
center,
axis_dir,
major_radius,
minor_radius,
} => {
if minor_radius <= 0.0 || major_radius <= 0.0 {
return Err("split_solid_by_surface: torus radii must be positive".into());
}
make_torus_brep(center, axis_dir, major_radius, minor_radius)
}
}
}
fn axis_span(solid: &BrepSolid, origin: Vec3, axis: Vec3) -> (f64, f64) {
let mut t_min = f64::INFINITY;
let mut t_max = f64::NEG_INFINITY;
for vertex in &solid.vertices {
let t = vertex.point.sub(origin).dot(axis);
t_min = t_min.min(t);
t_max = t_max.max(t);
}
(t_min, t_max)
}
pub fn split_solid_by_surface(
solid: &BrepSolid,
tool: &SplitSurface,
) -> Result<Vec<BrepSolid>, String> {
if let SplitSurface::Plane { point, normal } = *tool {
let (below, above) = split_solid_by_plane(solid, point, normal)?;
return Ok(vec![below, above]);
}
let tool_solid = build_tool_solid(solid, tool)?;
let options = BooleanOptions::default();
let inside = boolean_operation(solid, &tool_solid, BooleanOperation::Intersect, &options);
let outside = boolean_operation(solid, &tool_solid, BooleanOperation::Subtract, &options);
match (inside, outside) {
(Ok(inside), Ok(outside))
if !is_empty_piece(&inside)
&& !is_empty_piece(&outside)
&& inside.validate().is_empty()
&& outside.validate().is_empty() =>
{
Ok(vec![inside, outside])
}
_ => Err("split_solid_by_surface: tool surface does not divide the solid".into()),
}
}
fn recognized_split_surface(surface: &NurbsSurface) -> Result<SplitSurface, String> {
let analytic = surface
.analytic()
.ok_or("split_solid_by_face_surface: selected face is not an analytic surface")?;
match analytic {
AnalyticSurface::Plane {
origin,
u_dir,
v_dir,
..
} => {
let normal = u_dir.cross(*v_dir).normalized()?;
Ok(SplitSurface::Plane {
point: *origin,
normal,
})
}
AnalyticSurface::RuledRevolution {
frame,
rho0,
rho1,
height,
} => {
let scale = rho0.abs().max(rho1.abs()).max(1.0);
if (rho0 - rho1).abs() <= 1e-9 * scale {
Ok(SplitSurface::Cylinder {
axis_point: frame.origin,
axis_dir: frame.axis,
radius: 0.5 * (rho0 + rho1),
})
} else {
let slope = (rho1 - rho0) / height;
let axial_apex = -rho0 / slope;
let apex = frame.origin.add(frame.axis.scale(axial_apex));
let axis_dir = if slope >= 0.0 {
frame.axis
} else {
frame.axis.scale(-1.0)
};
Ok(SplitSurface::Cone {
apex,
axis_dir,
half_angle: slope.abs().atan(),
})
}
}
AnalyticSurface::Sphere { frame, radius } => Ok(SplitSurface::Sphere {
center: frame.origin,
radius: *radius,
}),
AnalyticSurface::Torus {
frame,
major_radius,
minor_radius,
} => Ok(SplitSurface::Torus {
center: frame.origin,
axis_dir: frame.axis,
major_radius: *major_radius,
minor_radius: *minor_radius,
}),
AnalyticSurface::Revolution { .. } => Err(
"split_solid_by_face_surface: general revolved surfaces are not supported as a cut tool"
.into(),
),
}
}
pub fn split_solid_by_face_surface(
solid: &BrepSolid,
surface: &NurbsSurface,
) -> Result<Vec<BrepSolid>, String> {
let tool = recognized_split_surface(surface)?;
split_solid_by_surface(solid, &tool)
}