mod backend;
mod bridge;
pub mod chfi2d;
mod error;
mod shape;
pub mod shapefix;
pub mod surface_ops;
mod topology;
pub use backend::OcctBackend;
pub use error::OcctError;
pub use shape::{read_brep, write_brep, OcctShape};
#[cfg(test)]
mod tests {
use super::*;
use ferrocad_geom::{
Capabilities, Continuity, ElementKind, GeometryBackend, TessellationOptions,
};
#[test]
fn make_box_has_six_faces() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
assert!(backend.resolve(&cube, "Face1").is_some());
assert!(backend.resolve(&cube, "Face6").is_some());
assert!(backend.resolve(&cube, "Face7").is_none());
}
#[test]
fn a_box_tessellates_with_part_identity() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let mesh = backend
.tessellate(&cube, &TessellationOptions::default())
.unwrap();
assert_eq!(mesh.parts.len(), 6 + 12 + 8);
assert_eq!(mesh.triangle_count(), 12);
assert_eq!(mesh.triangle_parts.len(), 12);
assert!(mesh.triangle_parts.iter().all(|part| *part < 6));
assert_eq!(mesh.segment_count(), 12);
assert!(mesh.segment_parts.iter().all(|part| (6..18).contains(part)));
assert_eq!(mesh.vertices.len(), 8);
assert_eq!(mesh.vertex_parts.len(), 8);
assert!(mesh.vertex_parts.iter().all(|part| (18..26).contains(part)));
assert_eq!(mesh.part_name(0), Some("Face1"));
assert_eq!(mesh.part_name(6), Some("Edge1"));
assert_eq!(mesh.part_name(18), Some("Vertex1"));
assert!(mesh.normals.is_empty(), "the mesh is flat-shaded");
let positions = mesh.positions.len();
assert!(
mesh.triangles
.iter()
.all(|index| (*index as usize) < positions)
);
assert!(mesh.edges.iter().all(|index| (*index as usize) < positions));
assert!(
mesh.vertices
.iter()
.all(|index| (*index as usize) < positions)
);
assert!(
backend
.capabilities()
.contains(Capabilities::TESSELLATION | Capabilities::TOPOLOGY)
);
}
#[test]
fn element_names_round_trip_through_resolve() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
for kind in [
ElementKind::Solid,
ElementKind::Shell,
ElementKind::Face,
ElementKind::Edge,
ElementKind::Vertex,
] {
let elements = backend.elements(&cube, kind);
assert!(!elements.is_empty(), "{kind:?} has elements");
for (index, element) in elements.iter().enumerate() {
assert_eq!(element.name, format!("{}{}", kind.prefix(), index + 1));
let resolved = backend
.resolve(&cube, &element.name)
.unwrap_or_else(|| panic!("{} resolves", element.name));
let expected = crate::shape::downcast(&element.shape).unwrap();
let actual = crate::shape::downcast(&resolved).unwrap();
let expected = expected.borrow();
let actual = actual.borrow();
assert!(
expected.as_ref().unwrap().IsEqual(actual.as_ref().unwrap()),
"{} names the same sub-shape",
element.name
);
}
}
}
#[test]
fn ancestors_work_in_both_directions() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let edges = backend.elements(&cube, ElementKind::Edge);
let faces = backend.elements(&cube, ElementKind::Face);
assert_eq!(edges.len(), 12);
assert_eq!(faces.len(), 6);
for edge in &edges {
assert_eq!(
backend.ancestors(&cube, edge, ElementKind::Face).len(),
2,
"{} has two adjacent faces",
edge.name
);
}
for face in &faces {
assert_eq!(
backend.ancestors(&cube, face, ElementKind::Edge).len(),
4,
"{} has four boundary edges",
face.name
);
}
}
#[test]
fn a_box_corner_is_c0() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let face = backend.elements(&cube, ElementKind::Face)[0].clone();
let edges = backend.ancestors(&cube, &face, ElementKind::Edge);
assert_eq!(edges.len(), 4);
let mut corners = 0;
for i in 0..edges.len() {
for j in (i + 1)..edges.len() {
if let Some(continuity) = backend.continuity(&cube, &edges[i], &edges[j]) {
assert_eq!(continuity, Continuity::C0);
corners += 1;
}
}
}
assert_eq!(corners, 4, "four adjacent edge pairs");
}
#[test]
fn chamfer_reports_lineage() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let edge = backend.elements(&cube, ElementKind::Edge)[0].clone();
let out = backend
.chamfer(&cube, std::slice::from_ref(&edge), 1.0, None)
.unwrap();
assert_eq!(backend.elements(&out.shape, ElementKind::Face).len(), 7);
assert!(!out.history.is_empty(), "a chamfer changes the topology");
assert!(
out.history
.deleted
.iter()
.any(|deleted| deleted.name == edge.name),
"the seeded edge is consumed"
);
assert!(backend.capabilities().contains(Capabilities::CHAMFER));
}
#[test]
fn a_box_edge_has_no_tangent_chain() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let edge = backend.elements(&cube, ElementKind::Edge)[0].clone();
let chain = ferrocad_geom::tangent_chain(&backend, &cube, &edge);
assert_eq!(chain.len(), 1);
assert_eq!(chain[0].name, edge.name);
}
#[test]
fn a_fillet_leaves_a_tangent_chain_at_its_boundary() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let edge = backend.elements(&cube, ElementKind::Edge)[0].clone();
let out = backend
.fillet(&cube, std::slice::from_ref(&edge), 2.0)
.unwrap()
.shape;
let edges = backend.elements(&out, ElementKind::Edge);
let mut tangent = None;
for i in 0..edges.len() {
for j in (i + 1)..edges.len() {
if backend.continuity(&out, &edges[i], &edges[j]) == Some(Continuity::G1) {
tangent = Some((i, j));
}
}
}
let (i, j) = tangent.expect("the fillet boundary is tangent");
let chain = ferrocad_geom::tangent_chain(&backend, &out, &edges[i]);
assert!(
chain.iter().any(|chained| chained.name == edges[j].name),
"the smooth neighbour joins the chain"
);
}
#[test]
fn asymmetric_chamfer_builds() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let edge = backend.elements(&cube, ElementKind::Edge)[0].clone();
let out = backend
.chamfer(&cube, std::slice::from_ref(&edge), 1.0, Some(2.0))
.unwrap();
assert_eq!(backend.elements(&out.shape, ElementKind::Face).len(), 7);
}
#[test]
fn fillet_reports_generated_faces() {
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let edge = backend.elements(&cube, ElementKind::Edge)[0].clone();
let out = backend
.fillet(&cube, std::slice::from_ref(&edge), 1.0)
.unwrap();
assert_eq!(backend.elements(&out.shape, ElementKind::Face).len(), 7);
assert!(
out.history
.generated
.iter()
.any(|(input, _)| input.name == edge.name),
"the fillet face is generated from the edge"
);
assert!(
out.history
.deleted
.iter()
.any(|deleted| deleted.name == edge.name)
);
}
#[test]
fn a_placed_box_tessellates_at_its_location() {
use ferrocad_types::{Placement, Rotation, Vector3};
let backend = OcctBackend::new();
let cube = backend.make_box(10.0, 10.0, 10.0).unwrap();
let moved = backend
.place(
&cube,
&Placement {
base: Vector3::new(10.0, 0.0, 0.0),
rotation: Rotation::identity(),
},
)
.unwrap();
let mesh = backend
.tessellate(&moved, &TessellationOptions::absolute(0.5))
.unwrap();
let min_x = mesh
.positions
.iter()
.map(|position| position[0])
.fold(f32::INFINITY, f32::min);
assert!(
(min_x - 10.0).abs() < 1e-3,
"location applied: min x = {min_x}"
);
}
#[test]
fn fuse_reports_face_lineage() {
let backend = OcctBackend::new();
let a = backend.make_box(10.0, 10.0, 10.0).unwrap();
let b = backend.make_box(10.0, 10.0, 10.0).unwrap();
let out = backend.fuse(&a, &b).unwrap();
assert!(!out.history.is_empty());
assert!(out.history.deleted.is_empty());
}
#[test]
fn cut_removes_material() {
let backend = OcctBackend::new();
let a = backend.make_box(10.0, 10.0, 10.0).unwrap();
let b = backend.make_box(10.0, 10.0, 10.0).unwrap();
let out = backend.cut(&a, &b).unwrap();
assert!(!out.history.is_empty());
}
#[test]
fn place_and_resolve() {
use ferrocad_types::{Placement, Rotation, Vector3};
let backend = OcctBackend::new();
let cube = backend.make_box(1.0, 1.0, 1.0).unwrap();
let moved = backend
.place(
&cube,
&Placement {
base: Vector3::new(5.0, 0.0, 0.0),
rotation: Rotation::identity(),
},
)
.unwrap();
assert!(backend.resolve(&moved, "Face1").is_some());
assert!(!cube.is_same(&moved));
}
#[test]
fn rejects_a_foreign_shape() {
use ferrocad_geom::NullBackend;
let occt = OcctBackend::new();
let foreign = NullBackend.make_box(1.0, 1.0, 1.0).unwrap();
assert_eq!(occt.resolve(&foreign, "Face1"), None);
}
#[test]
fn a_line_segment_makes_an_edge_and_reports_its_basis_line() {
use ferrocad_curves::{EdgeCurve, LineSegment, Vector3};
let backend = OcctBackend::new();
let segment = EdgeCurve::LineSegment(
LineSegment::new(Vector3::new(1.0, 2.0, 3.0), Vector3::new(4.0, 2.0, 3.0)).unwrap(),
);
let edge = backend.make_edge(&segment).unwrap();
let curve = backend.curve_of(&edge).unwrap();
let EdgeCurve::Line(line) = curve else {
panic!("a segment's basis curve is a line, got {curve:?}");
};
assert!(line.distance_to(Vector3::new(1.0, 2.0, 3.0)) < 1e-9);
assert!(line.distance_to(Vector3::new(4.0, 2.0, 3.0)) < 1e-9);
assert!((line.direction().sub(&Vector3::new(1.0, 0.0, 0.0))).length() < 1e-9);
}
#[test]
fn a_circle_round_trips_through_an_edge() {
use ferrocad_curves::{Circle, EdgeCurve, Vector3};
let backend = OcctBackend::new();
let circle = Circle::new(
Vector3::new(1.0, 2.0, 3.0),
Vector3::new(0.0, 0.0, 1.0),
5.0,
)
.unwrap();
let edge = backend.make_edge(&EdgeCurve::Circle(circle)).unwrap();
let EdgeCurve::Circle(back) = backend.curve_of(&edge).unwrap() else {
panic!("a full circle edge is a circle");
};
assert!((back.center().sub(&circle.center())).length() < 1e-9);
assert!((back.radius() - 5.0).abs() < 1e-9);
assert!((back.normal().sub(&Vector3::new(0.0, 0.0, 1.0))).length() < 1e-9);
}
#[test]
fn an_arc_round_trips_with_its_sweep_and_endpoints() {
use ferrocad_curves::{Arc, Circle, EdgeCurve, Vector3};
use std::f64::consts::FRAC_PI_2;
let backend = OcctBackend::new();
let circle = Circle::new(Vector3::zero(), Vector3::new(0.0, 0.0, 1.0), 2.0).unwrap();
let arc = Arc::new(circle, 0.0, FRAC_PI_2).unwrap();
let edge = backend.make_edge(&EdgeCurve::Arc(arc)).unwrap();
let EdgeCurve::Arc(back) = backend.curve_of(&edge).unwrap() else {
panic!("a trimmed arc edge is an arc");
};
assert!((back.circle().radius() - 2.0).abs() < 1e-9);
assert!((back.sweep().abs() - FRAC_PI_2).abs() < 1e-9);
assert!(back.start().distance(&arc.start()) < 1e-9);
assert!(back.end().distance(&arc.end()) < 1e-9);
}
#[test]
fn make_edge_builds_the_bounded_kinds_and_rejects_an_infinite_line() {
use ferrocad_curves::{EdgeCurve, Ellipse, Line, Vector3};
let backend = OcctBackend::new();
let line =
EdgeCurve::Line(Line::new(Vector3::zero(), Vector3::new(1.0, 0.0, 0.0)).unwrap());
assert!(backend.make_edge(&line).is_err());
let ellipse = EdgeCurve::Ellipse(
Ellipse::new(Vector3::zero(), Vector3::new(0.0, 0.0, 1.0), 3.0, 1.0).unwrap(),
);
let edge = backend
.make_edge(&ellipse)
.expect("an ellipse now has an exact gp_Elips edge");
let EdgeCurve::Ellipse(back) = backend.curve_of(&edge).unwrap() else {
panic!("an elliptical edge reports its basis ellipse");
};
assert!((back.major_radius() - 3.0).abs() < 1e-9);
assert!((back.minor_radius() - 1.0).abs() < 1e-9);
}
#[test]
fn curve_of_a_non_edge_is_unsupported() {
let backend = OcctBackend::new();
let cube = backend.make_box(1.0, 1.0, 1.0).unwrap();
assert!(backend.curve_of(&cube).is_err());
}
}