use cxx::UniquePtr;
use ferrocad_curves::Vector3;
use ferrocad_geom::{GeomError, Shape};
use opencascade_sys as occt;
use occt::top_tools::TopTools_ListOfShape;
use occt::topo_ds::TopoDS_Shape;
use crate::error::OcctError;
use crate::shape::{downcast, OcctShape};
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("ferrocad_occt/include/fc_surface_ops.hxx");
type TopoDS_Shape = opencascade_sys::topo_ds::TopoDS_Shape;
type TopTools_ListOfShape = opencascade_sys::top_tools::TopTools_ListOfShape;
fn fc_make_sphere_slice(
radius: f64,
x: f64,
y: f64,
z: f64,
u1: f64,
u2: f64,
v1: f64,
v2: f64,
) -> UniquePtr<TopoDS_Shape>;
fn fc_fillet_variable(
shape: &TopoDS_Shape,
edges: &TopTools_ListOfShape,
start_radii: &[f64],
end_radii: &[f64],
) -> UniquePtr<TopoDS_Shape>;
fn fc_offset2d(
shape: &TopoDS_Shape,
distance: f64,
kind: i32,
fill: bool,
open_result: bool,
) -> UniquePtr<TopoDS_Shape>;
fn fc_make_hollow_solid(shells: &TopTools_ListOfShape) -> UniquePtr<TopoDS_Shape>;
fn fc_make_helix_edge(
pitch: f64,
height: f64,
radius: f64,
angle: f64,
left_handed: bool,
) -> UniquePtr<TopoDS_Shape>;
fn fc_make_pipe_shell(
spine: &TopoDS_Shape,
profiles: &TopTools_ListOfShape,
solid: bool,
frenet: bool,
) -> UniquePtr<TopoDS_Shape>;
fn fc_spine_start_frame(spine: &TopoDS_Shape) -> Vec<f64>;
fn fc_edge_line_direction(edge: &TopoDS_Shape) -> Vec<f64>;
fn fc_shape_plane_normal(shape: &TopoDS_Shape) -> Vec<f64>;
}
}
fn owned(shape: UniquePtr<TopoDS_Shape>, operation: &'static str) -> Result<Shape, GeomError> {
if shape.is_null() {
return Err(OcctError::NotDone { operation }.into());
}
Ok(Shape::new(OcctShape(shape)))
}
fn vector3(values: &[f64], operation: &'static str) -> Result<Vector3, GeomError> {
if values.len() != 3 {
return Err(OcctError::NotDone { operation }.into());
}
Ok(Vector3::new(values[0], values[1], values[2]))
}
pub fn spine_start_frame(spine: &Shape) -> Result<(Vector3, Vector3), GeomError> {
let occt_spine = downcast(spine)?;
let guard = occt_spine.borrow();
let inner = guard
.as_ref()
.ok_or(OcctError::InvalidInput { what: "null spine" })?;
let values = ffi::fc_spine_start_frame(inner);
if values.len() != 6 {
return Err(OcctError::NotDone {
operation: "spine_start_frame",
}
.into());
}
Ok((
Vector3::new(values[0], values[1], values[2]),
Vector3::new(values[3], values[4], values[5]),
))
}
pub fn edge_line_direction(edge: &Shape) -> Result<Vector3, GeomError> {
let occt_edge = downcast(edge)?;
let guard = occt_edge.borrow();
let inner = guard
.as_ref()
.ok_or(OcctError::InvalidInput { what: "null edge" })?;
let values = ffi::fc_edge_line_direction(inner);
vector3(&values, "edge_line_direction")
}
pub fn shape_plane_normal(shape: &Shape) -> Result<Vector3, GeomError> {
let occt_shape = downcast(shape)?;
let guard = occt_shape.borrow();
let inner = guard
.as_ref()
.ok_or(OcctError::InvalidInput { what: "null shape" })?;
let values = ffi::fc_shape_plane_normal(inner);
vector3(&values, "shape_plane_normal")
}
fn shape_list(shapes: &[Shape], what: &'static str) -> Result<UniquePtr<TopTools_ListOfShape>, GeomError> {
let mut list = occt::top_tools::new_list_of_shape();
for shape in shapes {
let occt_shape = downcast(shape)?;
let guard = occt_shape.borrow();
let inner = guard
.as_ref()
.ok_or(OcctError::InvalidInput { what })?;
list.pin_mut().Append(inner);
}
Ok(list)
}
fn finite(value: f64, what: &'static str) -> Result<(), GeomError> {
if value.is_finite() {
Ok(())
} else {
Err(OcctError::InvalidInput { what }.into())
}
}
pub fn make_sphere_slice(
radius: f64,
center: Vector3,
u1: f64,
u2: f64,
v1: f64,
v2: f64,
) -> Result<Shape, GeomError> {
finite(radius, "the sphere radius must be finite")?;
if radius <= 0.0 {
return Err(OcctError::InvalidInput {
what: "the sphere radius must be positive",
}
.into());
}
for (angle, what) in [
(u1, "the first longitude must be finite"),
(u2, "the second longitude must be finite"),
(v1, "the first latitude must be finite"),
(v2, "the second latitude must be finite"),
] {
finite(angle, what)?;
}
if u2 - u1 <= 0.0 || u2 - u1 > 360.0 + 1e-7 {
return Err(OcctError::InvalidInput {
what: "the longitude range must be positive and at most 360 degrees",
}
.into());
}
if v2 - v1 <= 0.0 || v1 < -90.0 - 1e-7 || v2 > 90.0 + 1e-7 {
return Err(OcctError::InvalidInput {
what: "the latitude range must be positive and within -90..90 degrees",
}
.into());
}
owned(
ffi::fc_make_sphere_slice(radius, center.x, center.y, center.z, u1, u2, v1, v2),
"make_sphere_slice",
)
}
pub fn fillet_variable(
shape: &Shape,
edges: &[Shape],
start_radii: &[f64],
end_radii: &[f64],
) -> Result<Shape, GeomError> {
if edges.is_empty() {
return Err(OcctError::InvalidInput {
what: "a variable fillet needs at least one edge",
}
.into());
}
if edges.len() != start_radii.len() || edges.len() != end_radii.len() {
return Err(OcctError::InvalidInput {
what: "the edge and radius lists must have the same length",
}
.into());
}
for (&start, &end) in start_radii.iter().zip(end_radii) {
finite(start, "a fillet radius must be finite")?;
finite(end, "a fillet radius must be finite")?;
if start <= 0.0 || end <= 0.0 {
return Err(OcctError::InvalidInput {
what: "a fillet radius must be positive",
}
.into());
}
}
let occt_shape = downcast(shape)?;
let guard = occt_shape.borrow();
let inner = guard
.as_ref()
.ok_or(OcctError::InvalidInput { what: "null shape" })?;
let list = shape_list(edges, "a fillet edge must be an OCCT edge")?;
owned(
ffi::fc_fillet_variable(inner, &list, start_radii, end_radii),
"fillet_variable",
)
}
pub fn offset2d(
shape: &Shape,
distance: f64,
kind: i32,
fill: bool,
open_result: bool,
) -> Result<Shape, GeomError> {
finite(distance, "the offset distance must be finite")?;
if fill && distance.abs() <= 1e-7 {
return Err(OcctError::InvalidInput {
what: "a filled offset cannot have a zero distance",
}
.into());
}
if !(0..=2).contains(&kind) {
return Err(OcctError::InvalidInput {
what: "the join kind must be 0 (arc), 1 (tangent) or 2 (intersection)",
}
.into());
}
let occt_shape = downcast(shape)?;
let guard = occt_shape.borrow();
let inner = guard
.as_ref()
.ok_or(OcctError::InvalidInput { what: "null shape" })?;
owned(
ffi::fc_offset2d(inner, distance, kind, fill, open_result),
"offset2d",
)
}
pub fn make_hollow_solid(shells: &[Shape]) -> Result<Shape, GeomError> {
if shells.is_empty() {
return Err(OcctError::InvalidInput {
what: "a solid needs at least one shell",
}
.into());
}
let list = shape_list(shells, "a solid shell must be an OCCT shell")?;
owned(ffi::fc_make_hollow_solid(&list), "make_hollow_solid")
}
pub fn make_helix_edge(
pitch: f64,
height: f64,
radius: f64,
angle: f64,
left_handed: bool,
) -> Result<Shape, GeomError> {
finite(pitch, "the helix pitch must be finite")?;
finite(height, "the helix height must be finite")?;
finite(radius, "the helix radius must be finite")?;
finite(angle, "the helix angle must be finite")?;
if pitch.abs() <= 1e-7 {
return Err(OcctError::InvalidInput {
what: "the helix pitch is too small",
}
.into());
}
if height.abs() <= 1e-7 {
return Err(OcctError::InvalidInput {
what: "the helix height is too small",
}
.into());
}
if (height > 0.0 && pitch < 0.0) || (height < 0.0 && pitch > 0.0) {
return Err(OcctError::InvalidInput {
what: "the helix pitch and height signs are incompatible",
}
.into());
}
if angle <= 1e-9 && radius <= 0.0 {
return Err(OcctError::InvalidInput {
what: "the helix radius is too small",
}
.into());
}
owned(
ffi::fc_make_helix_edge(pitch, height, radius, angle, left_handed),
"make_helix_edge",
)
}
pub fn make_pipe_shell(
spine: &Shape,
profiles: &[Shape],
solid: bool,
frenet: bool,
) -> Result<Shape, GeomError> {
if profiles.is_empty() {
return Err(OcctError::InvalidInput {
what: "a pipe needs at least one profile",
}
.into());
}
let occt_spine = downcast(spine)?;
let guard = occt_spine.borrow();
let inner = guard
.as_ref()
.ok_or(OcctError::InvalidInput { what: "null spine" })?;
let list = shape_list(profiles, "a pipe profile must be an OCCT shape")?;
owned(
ffi::fc_make_pipe_shell(inner, &list, solid, frenet),
"make_pipe_shell",
)
}