diff --git a/crates/opencascade-sys/include/b_rep.hxx b/crates/opencascade-sys/include/b_rep.hxx index 74872521f..eaaebe6ff 100644 --- a/crates/opencascade-sys/include/b_rep.hxx +++ b/crates/opencascade-sys/include/b_rep.hxx @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -30,3 +31,5 @@ inline std::unique_ptr BRep_Tool_Triangulation(const return std::unique_ptr( new opencascade::handle(BRep_Tool::Triangulation(face, location))); } + +inline bool BRep_Tool_IsClosed(const TopoDS_Wire &wire) { return BRep_Tool::IsClosed(wire); } diff --git a/crates/opencascade-sys/include/b_rep_adaptor.hxx b/crates/opencascade-sys/include/b_rep_adaptor.hxx index d99203099..da20a40c4 100644 --- a/crates/opencascade-sys/include/b_rep_adaptor.hxx +++ b/crates/opencascade-sys/include/b_rep_adaptor.hxx @@ -1,7 +1,17 @@ #include #include +#include #include +#include inline std::unique_ptr BRepAdaptor_Curve_value(const BRepAdaptor_Curve &curve, const Standard_Real U) { return std::unique_ptr(new gp_Pnt(curve.Value(U))); } + +inline void BRepAdaptor_Curve_D1(const BRepAdaptor_Curve &curve, const Standard_Real U, gp_Pnt &P, gp_Vec &V1) { + curve.D1(U, P, V1); +} + +inline std::unique_ptr BRepAdaptor_Curve_Circle(const BRepAdaptor_Curve &curve) { + return std::unique_ptr(new gp_Circ(curve.Circle())); +} diff --git a/crates/opencascade-sys/include/geom.hxx b/crates/opencascade-sys/include/geom.hxx index b063d9204..f76b362bd 100644 --- a/crates/opencascade-sys/include/geom.hxx +++ b/crates/opencascade-sys/include/geom.hxx @@ -1,11 +1,17 @@ #include #include #include +#include +#include #include #include +#include #include +#include #include #include +#include +#include inline std::unique_ptr Geom_CylindricalSurface_new(const gp_Ax3 &axis, double radius) { return std::unique_ptr( @@ -43,3 +49,76 @@ inline std::unique_ptr HandleGeomCurve_Value(const Handle_Geom_Curve &cu } inline const Handle_Standard_Type &DynamicType(const Handle_Geom_Surface &surface) { return surface->DynamicType(); } + +inline std::unique_ptr new_HandleGeomCircle_from_HandleGeomCurve(const Handle_Geom_Curve &curve) { + Handle_Geom_Circle circle_handle = opencascade::handle::DownCast(curve); + return std::unique_ptr(new opencascade::handle(circle_handle)); +} + +inline std::unique_ptr HandleGeomCircle_Circ(const Handle_Geom_Circle &circle) { + return std::unique_ptr(new gp_Circ(circle->Circ())); +} + +// CylindricalSurface downcast + accessors + +inline std::unique_ptr +new_HandleGeomCylindricalSurface_from_HandleGeomSurface(const Handle_Geom_Surface &surface) { + Handle_Geom_CylindricalSurface handle = opencascade::handle::DownCast(surface); + return std::unique_ptr(new opencascade::handle(handle)); +} + +inline double HandleGeomCylindricalSurface_Radius(const Handle_Geom_CylindricalSurface &cyl) { return cyl->Radius(); } + +inline const gp_Ax3 &HandleGeomCylindricalSurface_Position(const Handle_Geom_CylindricalSurface &cyl) { + return cyl->Position(); +} + +// SphericalSurface downcast + accessors + +inline std::unique_ptr +new_HandleGeomSphericalSurface_from_HandleGeomSurface(const Handle_Geom_Surface &surface) { + Handle_Geom_SphericalSurface handle = opencascade::handle::DownCast(surface); + return std::unique_ptr(new opencascade::handle(handle)); +} + +inline double HandleGeomSphericalSurface_Radius(const Handle_Geom_SphericalSurface &sphere) { return sphere->Radius(); } + +inline const gp_Ax3 &HandleGeomSphericalSurface_Position(const Handle_Geom_SphericalSurface &sphere) { + return sphere->Position(); +} + +// ConicalSurface downcast + accessors + +inline std::unique_ptr +new_HandleGeomConicalSurface_from_HandleGeomSurface(const Handle_Geom_Surface &surface) { + Handle_Geom_ConicalSurface handle = opencascade::handle::DownCast(surface); + return std::unique_ptr(new opencascade::handle(handle)); +} + +inline double HandleGeomConicalSurface_RefRadius(const Handle_Geom_ConicalSurface &cone) { return cone->RefRadius(); } + +inline double HandleGeomConicalSurface_SemiAngle(const Handle_Geom_ConicalSurface &cone) { return cone->SemiAngle(); } + +inline const gp_Ax3 &HandleGeomConicalSurface_Position(const Handle_Geom_ConicalSurface &cone) { + return cone->Position(); +} + +// ToroidalSurface downcast + accessors + +inline std::unique_ptr +new_HandleGeomToroidalSurface_from_HandleGeomSurface(const Handle_Geom_Surface &surface) { + Handle_Geom_ToroidalSurface handle = opencascade::handle::DownCast(surface); + return std::unique_ptr(new opencascade::handle(handle)); +} + +inline double HandleGeomToroidalSurface_MajorRadius(const Handle_Geom_ToroidalSurface &torus) { + return torus->MajorRadius(); +} + +inline double HandleGeomToroidalSurface_MinorRadius(const Handle_Geom_ToroidalSurface &torus) { + return torus->MinorRadius(); +} + +inline const gp_Ax3 &HandleGeomToroidalSurface_Position(const Handle_Geom_ToroidalSurface &torus) { + return torus->Position(); +} diff --git a/crates/opencascade-sys/src/b_rep.rs b/crates/opencascade-sys/src/b_rep.rs index 6acf59eee..09a05ce2e 100644 --- a/crates/opencascade-sys/src/b_rep.rs +++ b/crates/opencascade-sys/src/b_rep.rs @@ -10,6 +10,7 @@ mod inner { type TopoDS_Face = crate::topo_ds::TopoDS_Face; type TopoDS_Edge = crate::topo_ds::TopoDS_Edge; type TopoDS_Vertex = crate::topo_ds::TopoDS_Vertex; + type TopoDS_Wire = crate::topo_ds::TopoDS_Wire; type Handle_Geom_Surface = crate::geom::Handle_Geom_Surface; type Handle_Geom_Curve = crate::geom::Handle_Geom_Curve; type Handle_Poly_Triangulation = crate::poly::Handle_Poly_Triangulation; @@ -32,5 +33,6 @@ mod inner { face: &TopoDS_Face, location: Pin<&mut TopLoc_Location>, ) -> UniquePtr; + pub fn BRep_Tool_IsClosed(wire: &TopoDS_Wire) -> bool; } } diff --git a/crates/opencascade-sys/src/b_rep_adaptor.rs b/crates/opencascade-sys/src/b_rep_adaptor.rs index ee4dba123..31a3e5edf 100644 --- a/crates/opencascade-sys/src/b_rep_adaptor.rs +++ b/crates/opencascade-sys/src/b_rep_adaptor.rs @@ -6,6 +6,8 @@ mod inner { include!("opencascade-sys/include/b_rep_adaptor.hxx"); type gp_Pnt = crate::gp::gp_Pnt; + type gp_Vec = crate::gp::gp_Vec; + type gp_Circ = crate::gp::gp_Circ; type GeomAbs_CurveType = crate::geom_abs::GeomAbs_CurveType; type TopoDS_Edge = crate::topo_ds::TopoDS_Edge; @@ -15,6 +17,13 @@ mod inner { pub fn FirstParameter(self: &BRepAdaptor_Curve) -> f64; pub fn LastParameter(self: &BRepAdaptor_Curve) -> f64; pub fn BRepAdaptor_Curve_value(curve: &BRepAdaptor_Curve, u: f64) -> UniquePtr; + pub fn BRepAdaptor_Curve_D1( + curve: &BRepAdaptor_Curve, + u: f64, + point: Pin<&mut gp_Pnt>, + vec: Pin<&mut gp_Vec>, + ); + pub fn BRepAdaptor_Curve_Circle(curve: &BRepAdaptor_Curve) -> UniquePtr; pub fn GetType(self: &BRepAdaptor_Curve) -> GeomAbs_CurveType; } } diff --git a/crates/opencascade-sys/src/geom.rs b/crates/opencascade-sys/src/geom.rs index 5f8cb7507..d289e8dfe 100644 --- a/crates/opencascade-sys/src/geom.rs +++ b/crates/opencascade-sys/src/geom.rs @@ -7,6 +7,7 @@ mod inner { type gp_Ax3 = crate::gp::gp_Ax3; type gp_Pnt = crate::gp::gp_Pnt; + type gp_Circ = crate::gp::gp_Circ; type TColgp_Array2OfPnt = crate::t_col_gp::TColgp_Array2OfPnt; type TColgp_HArray1OfPnt = crate::t_col_gp::TColgp_HArray1OfPnt; type Handle_Standard_Type = crate::standard::Handle_Standard_Type; @@ -35,6 +36,18 @@ mod inner { type Handle_Geom_Plane; pub fn IsNull(self: &Handle_Geom_Plane) -> bool; + type Handle_Geom_SphericalSurface; + pub fn IsNull(self: &Handle_Geom_SphericalSurface) -> bool; + + type Handle_Geom_ConicalSurface; + pub fn IsNull(self: &Handle_Geom_ConicalSurface) -> bool; + + type Handle_Geom_ToroidalSurface; + pub fn IsNull(self: &Handle_Geom_ToroidalSurface) -> bool; + + type Handle_Geom_Circle; + pub fn IsNull(self: &Handle_Geom_Circle) -> bool; + type Handle_Geom_CylindricalSurface; pub fn IsNull(self: &Handle_Geom_CylindricalSurface) -> bool; // End Handles @@ -74,6 +87,41 @@ mod inner { geom_surface_handle: &Handle_Geom_Surface, ) -> UniquePtr; + pub fn new_HandleGeomCylindricalSurface_from_HandleGeomSurface( + geom_surface_handle: &Handle_Geom_Surface, + ) -> UniquePtr; + pub fn HandleGeomCylindricalSurface_Radius(cyl: &Handle_Geom_CylindricalSurface) -> f64; + pub fn HandleGeomCylindricalSurface_Position( + cyl: &Handle_Geom_CylindricalSurface, + ) -> &gp_Ax3; + + pub fn new_HandleGeomSphericalSurface_from_HandleGeomSurface( + geom_surface_handle: &Handle_Geom_Surface, + ) -> UniquePtr; + pub fn HandleGeomSphericalSurface_Radius(sphere: &Handle_Geom_SphericalSurface) -> f64; + pub fn HandleGeomSphericalSurface_Position( + sphere: &Handle_Geom_SphericalSurface, + ) -> &gp_Ax3; + + pub fn new_HandleGeomConicalSurface_from_HandleGeomSurface( + geom_surface_handle: &Handle_Geom_Surface, + ) -> UniquePtr; + pub fn HandleGeomConicalSurface_RefRadius(cone: &Handle_Geom_ConicalSurface) -> f64; + pub fn HandleGeomConicalSurface_SemiAngle(cone: &Handle_Geom_ConicalSurface) -> f64; + pub fn HandleGeomConicalSurface_Position(cone: &Handle_Geom_ConicalSurface) -> &gp_Ax3; + + pub fn new_HandleGeomToroidalSurface_from_HandleGeomSurface( + geom_surface_handle: &Handle_Geom_Surface, + ) -> UniquePtr; + pub fn HandleGeomToroidalSurface_MajorRadius(torus: &Handle_Geom_ToroidalSurface) -> f64; + pub fn HandleGeomToroidalSurface_MinorRadius(torus: &Handle_Geom_ToroidalSurface) -> f64; + pub fn HandleGeomToroidalSurface_Position(torus: &Handle_Geom_ToroidalSurface) -> &gp_Ax3; + + pub fn new_HandleGeomCircle_from_HandleGeomCurve( + curve: &Handle_Geom_Curve, + ) -> UniquePtr; + pub fn HandleGeomCircle_Circ(circle: &Handle_Geom_Circle) -> UniquePtr; + #[cxx_name = "construct_unique"] pub fn new_HandleGeomCurve_from_HandleGeom_BSplineCurve( bspline_curve_handle: &Handle_Geom_BSplineCurve, @@ -90,7 +138,11 @@ mod inner { ) -> UniquePtr; } + impl UniquePtr {} impl UniquePtr {} + impl UniquePtr {} + impl UniquePtr {} + impl UniquePtr {} impl UniquePtr {} impl UniquePtr {} impl UniquePtr {} diff --git a/crates/opencascade-sys/src/gp.rs b/crates/opencascade-sys/src/gp.rs index 92fd3eb2a..74f267e1e 100644 --- a/crates/opencascade-sys/src/gp.rs +++ b/crates/opencascade-sys/src/gp.rs @@ -45,6 +45,8 @@ mod inner { type gp_Circ; #[cxx_name = "construct_unique"] pub fn gp_Circ_new(axis: &gp_Ax2, radius: f64) -> UniquePtr; + pub fn Radius(self: &gp_Circ) -> f64; + pub fn Position(self: &gp_Circ) -> &gp_Ax2; type gp_Ax1; #[cxx_name = "construct_unique"] @@ -53,10 +55,14 @@ mod inner { type gp_Ax2; #[cxx_name = "construct_unique"] pub fn gp_Ax2_new(origin: &gp_Pnt, main_dir: &gp_Dir) -> UniquePtr; + pub fn Location(self: &gp_Ax2) -> &gp_Pnt; + pub fn Direction(self: &gp_Ax2) -> &gp_Dir; type gp_Ax3; #[cxx_name = "construct_unique"] pub fn gp_Ax3_from_gp_Ax2(axis: &gp_Ax2) -> UniquePtr; + pub fn Location(self: &gp_Ax3) -> &gp_Pnt; + pub fn Direction(self: &gp_Ax3) -> &gp_Dir; type gp_Dir2d; #[cxx_name = "construct_unique"] diff --git a/crates/opencascade/src/primitives.rs b/crates/opencascade/src/primitives.rs index 4b3ef9194..3e45c6991 100644 --- a/crates/opencascade/src/primitives.rs +++ b/crates/opencascade/src/primitives.rs @@ -24,6 +24,12 @@ pub use surface::*; pub use vertex::*; pub use wire::*; +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum PositionMode { + Parameter, + Length, +} + #[derive(Debug, Copy, Clone, PartialEq)] pub enum ShapeType { /// Abstract topological data structure describes a basic entity. @@ -267,3 +273,36 @@ impl From for ffi::geom_abs::GeomAbs_JoinType { } } } + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum SurfaceType { + Plane, + Cylinder, + Cone, + Sphere, + Torus, + BezierSurface, + BSplineSurface, + SurfaceOfRevolution, + SurfaceOfExtrusion, + OffsetSurface, + OtherSurface, +} + +impl std::fmt::Display for SurfaceType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SurfaceType::Plane => write!(f, "Plane"), + SurfaceType::Cylinder => write!(f, "Cylinder"), + SurfaceType::Cone => write!(f, "Cone"), + SurfaceType::Sphere => write!(f, "Sphere"), + SurfaceType::Torus => write!(f, "Torus"), + SurfaceType::BezierSurface => write!(f, "BezierSurface"), + SurfaceType::BSplineSurface => write!(f, "BSplineSurface"), + SurfaceType::SurfaceOfRevolution => write!(f, "SurfaceOfRevolution"), + SurfaceType::SurfaceOfExtrusion => write!(f, "SurfaceOfExtrusion"), + SurfaceType::OffsetSurface => write!(f, "OffsetSurface"), + SurfaceType::OtherSurface => write!(f, "OtherSurface"), + } + } +} diff --git a/crates/opencascade/src/primitives/edge.rs b/crates/opencascade/src/primitives/edge.rs index 7e6f5001e..7ff7afc48 100644 --- a/crates/opencascade/src/primitives/edge.rs +++ b/crates/opencascade/src/primitives/edge.rs @@ -1,5 +1,5 @@ use super::make_vec; -use crate::primitives::{make_axis_2, make_point}; +use crate::primitives::{make_axis_2, make_point, PositionMode}; use cxx::UniquePtr; use glam::{dvec3, DVec3}; use opencascade_sys as ffi; @@ -174,6 +174,55 @@ impl Edge { EdgeType::from(curve.GetType()) } + + pub fn length(&self) -> f64 { + let mut props = ffi::g_prop::GProps_new(); + ffi::b_rep_g_prop::BRepGProp::LinearProperties( + ffi::topo_ds::cast_edge_to_shape(&self.inner), + props.pin_mut(), + false, + false, + ); + props.Mass() + } + + pub fn tangent_at(&self, position: f64, mode: PositionMode) -> DVec3 { + let curve = ffi::b_rep_adaptor::BRepAdaptor_Curve_new(&self.inner); + let param = match mode { + PositionMode::Parameter => position, + PositionMode::Length => { + let first = curve.FirstParameter(); + let last = curve.LastParameter(); + first + position * (last - first) + }, + }; + + let mut point = ffi::gp::new_point(0.0, 0.0, 0.0); + let mut vec = ffi::gp::new_vec(0.0, 0.0, 0.0); + ffi::b_rep_adaptor::BRepAdaptor_Curve_D1(&curve, param, point.pin_mut(), vec.pin_mut()); + + dvec3(vec.X(), vec.Y(), vec.Z()).normalize() + } + + pub fn arc_center(&self) -> Option { + let curve = ffi::b_rep_adaptor::BRepAdaptor_Curve_new(&self.inner); + if curve.GetType() != ffi::geom_abs::GeomAbs_CurveType::GeomAbs_Circle { + return None; + } + let circ = ffi::b_rep_adaptor::BRepAdaptor_Curve_Circle(&curve); + let pos = circ.Position(); + let loc = pos.Location(); + Some(dvec3(loc.X(), loc.Y(), loc.Z())) + } + + pub fn radius(&self) -> Option { + let curve = ffi::b_rep_adaptor::BRepAdaptor_Curve_new(&self.inner); + if curve.GetType() != ffi::geom_abs::GeomAbs_CurveType::GeomAbs_Circle { + return None; + } + let circ = ffi::b_rep_adaptor::BRepAdaptor_Curve_Circle(&curve); + Some(circ.Radius()) + } } pub struct ApproximationSegmentIterator { diff --git a/crates/opencascade/src/primitives/face.rs b/crates/opencascade/src/primitives/face.rs index 051fb4fcc..7e20284bc 100644 --- a/crates/opencascade/src/primitives/face.rs +++ b/crates/opencascade/src/primitives/face.rs @@ -3,7 +3,8 @@ use crate::{ law_function::law_function_from_graph, make_pipe_shell::make_pipe_shell_with_law_function, primitives::{ - make_axis_1, make_point, make_vec, EdgeIterator, JoinType, Shape, Solid, Surface, Wire, + make_axis_1, make_point, make_vec, EdgeIterator, JoinType, Shape, Solid, Surface, + SurfaceType, Wire, }, workplane::Workplane, }; @@ -394,6 +395,42 @@ impl Face { props.Mass() } + pub fn surface_axis(&self) -> Option<(DVec3, DVec3)> { + let handle = ffi::b_rep::BRep_Tool_Surface(&self.inner); + let surface = Surface { inner: handle }; + surface.axis() + } + + pub fn surface_radius(&self) -> Option { + let handle = ffi::b_rep::BRep_Tool_Surface(&self.inner); + let surface = Surface { inner: handle }; + surface.radius() + } + + pub fn surface_type(&self) -> SurfaceType { + let surface = ffi::b_rep::BRep_Tool_Surface(&self.inner); + let dynamic_type = ffi::geom::DynamicType(&surface); + let name = ffi::standard::type_name(dynamic_type); + + match name.as_str() { + "Geom_Plane" => SurfaceType::Plane, + "Geom_CylindricalSurface" => SurfaceType::Cylinder, + "Geom_ConicalSurface" => SurfaceType::Cone, + "Geom_SphericalSurface" => SurfaceType::Sphere, + "Geom_ToroidalSurface" => SurfaceType::Torus, + "Geom_BezierSurface" => SurfaceType::BezierSurface, + "Geom_BSplineSurface" => SurfaceType::BSplineSurface, + "Geom_SurfaceOfRevolution" => SurfaceType::SurfaceOfRevolution, + "Geom_SurfaceOfExtrusion" => SurfaceType::SurfaceOfExtrusion, + "Geom_OffsetSurface" => SurfaceType::OffsetSurface, + _ => SurfaceType::OtherSurface, + } + } + + pub fn is_planar(&self) -> bool { + self.surface_type() == SurfaceType::Plane + } + pub fn orientation(&self) -> FaceOrientation { FaceOrientation::from(self.inner.Orientation()) } @@ -559,6 +596,7 @@ impl From for FaceOrientation { #[cfg(test)] mod tests { use super::*; + use crate::primitives::shape::Shape; #[test] fn test_add() { @@ -570,6 +608,56 @@ mod tests { ); } + #[test] + fn test_surface_type_plane() { + let face = Workplane::xy().rect(7.0, 5.0).to_face(); + assert_eq!(face.surface_type(), SurfaceType::Plane); + } + + #[test] + fn test_surface_type_cylinder() { + let shape = Shape::cylinder_radius_height(3.0, 10.0); + let has_cylinder = shape.faces().any(|f| f.surface_type() == SurfaceType::Cylinder); + assert!(has_cylinder, "Expected at least one cylindrical face"); + } + + #[test] + fn test_surface_type_sphere() { + let shape = Shape::sphere(5.0).build(); + let has_sphere = shape.faces().any(|f| f.surface_type() == SurfaceType::Sphere); + assert!(has_sphere, "Expected at least one spherical face"); + } + + #[test] + fn test_is_planar() { + let plane_face = Workplane::xy().rect(7.0, 5.0).to_face(); + assert!(plane_face.is_planar()); + + let shape = Shape::cylinder_radius_height(3.0, 10.0); + let cylinder_face = shape + .faces() + .find(|f| f.surface_type() == SurfaceType::Cylinder) + .expect("Expected a cylindrical face"); + assert!(!cylinder_face.is_planar()); + } + + #[test] + fn test_cylindrical_face_axis_is_z_direction() { + let shape = Shape::cylinder_radius_height(3.0, 10.0); + let cylinder_face = shape + .faces() + .find(|f| f.surface_type() == SurfaceType::Cylinder) + .expect("Expected a cylindrical face"); + let axis = cylinder_face.surface_axis(); + assert!(axis.is_some(), "Expected Some axis for cylindrical face"); + let (_origin, direction) = axis.unwrap(); + assert!( + (direction - DVec3::Z).length() < 0.0001 || (direction + DVec3::Z).length() < 0.0001, + "Expected Z direction, got {:?}", + direction + ); + } + #[test] fn test_from_wire_with_no_holes() { let outer = Workplane::xy().rect(10.0, 10.0); @@ -581,6 +669,30 @@ mod tests { ); } + #[test] + fn test_planar_face_axis_returns_none() { + let face = Workplane::xy().rect(7.0, 5.0).to_face(); + assert_eq!(face.surface_axis(), None); + } + + #[test] + fn test_cylindrical_face_radius() { + let shape = Shape::cylinder_radius_height(5.0, 10.0); + let cylinder_face = shape + .faces() + .find(|f| f.surface_type() == SurfaceType::Cylinder) + .expect("Expected a cylindrical face"); + let radius = cylinder_face.surface_radius(); + assert!(radius.is_some(), "Expected Some radius for cylindrical face"); + assert!((radius.unwrap() - 5.0).abs() < 0.0001, "Expected radius ~5.0, got {:?}", radius); + } + + #[test] + fn test_planar_face_radius_returns_none() { + let face = Workplane::xy().rect(7.0, 5.0).to_face(); + assert_eq!(face.surface_radius(), None); + } + #[test] fn test_from_wire_with_holes() { let outer = Workplane::xy().rect(10.0, 10.0); diff --git a/crates/opencascade/src/primitives/shape.rs b/crates/opencascade/src/primitives/shape.rs index 158fdb904..b752d5e9e 100644 --- a/crates/opencascade/src/primitives/shape.rs +++ b/crates/opencascade/src/primitives/shape.rs @@ -832,6 +832,36 @@ impl Shape { Self::from_shape(brep.pin_mut().Shape()) } + pub fn center_of_mass(&self) -> DVec3 { + let mut props = ffi::g_prop::GProps_new(); + + match self.shape_type() { + ShapeType::Face => ffi::b_rep_g_prop::BRepGProp::SurfaceProperties( + &self.inner, + props.pin_mut(), + false, + false, + ), + ShapeType::Solid => ffi::b_rep_g_prop::BRepGProp::VolumeProperties( + &self.inner, + props.pin_mut(), + true, + false, + false, + ), + _ => ffi::b_rep_g_prop::BRepGProp::VolumeProperties( + &self.inner, + props.pin_mut(), + true, + false, + false, + ), + } + + let center = ffi::g_prop::GProp_GProps_CentreOfMass(&props); + dvec3(center.X(), center.Y(), center.Z()) + } + /// Create a translated copy of this shape. #[must_use] pub fn translated(&self, offset: DVec3) -> Self { @@ -1001,6 +1031,32 @@ mod tests { assert!(shape.as_face().is_none()); } + #[test] + fn test_center_of_mass_face() { + let shape = Shape::from(&Face::from_wire(&Wire::rect(7.0, 5.0))); + let com = shape.center_of_mass(); + assert!( + com.distance_squared(dvec3(0.0, 0.0, 0.0)) <= 0.00001, + "Expected center of mass at (0, 0, 0), got ({}, {}, {})", + com.x, + com.y, + com.z, + ); + } + + #[test] + fn test_center_of_mass_solid() { + let shape = Shape::box_centered(10.0, 10.0, 10.0); + let com = shape.center_of_mass(); + assert!( + com.distance_squared(dvec3(0.0, 0.0, 0.0)) <= 0.00001, + "Expected center of mass at (0, 0, 0), got ({}, {}, {})", + com.x, + com.y, + com.z, + ); + } + #[test] fn test_empty_shape() { let shape = Shape::empty(); diff --git a/crates/opencascade/src/primitives/solid.rs b/crates/opencascade/src/primitives/solid.rs index 8fa86d3e8..bd1750e53 100644 --- a/crates/opencascade/src/primitives/solid.rs +++ b/crates/opencascade/src/primitives/solid.rs @@ -132,4 +132,99 @@ impl Solid { let wire = Wire::from_ordered_points(points)?; Ok(Face::from_wire(&wire).extrude(dvec3(0.0, 0.0, h))) } + + #[must_use] + pub fn volume(&self) -> f64 { + let mut props = ffi::g_prop::GProps_new(); + let inner_shape = ffi::topo_ds::cast_solid_to_shape(&self.inner); + ffi::b_rep_g_prop::BRepGProp::VolumeProperties( + inner_shape, + props.pin_mut(), + true, + false, + false, + ); + props.Mass() + } + + #[must_use] + pub fn center_of_mass(&self) -> DVec3 { + let mut props = ffi::g_prop::GProps_new(); + let inner_shape = ffi::topo_ds::cast_solid_to_shape(&self.inner); + ffi::b_rep_g_prop::BRepGProp::VolumeProperties( + inner_shape, + props.pin_mut(), + true, + false, + false, + ); + let center = ffi::g_prop::GProp_GProps_CentreOfMass(&props); + dvec3(center.X(), center.Y(), center.Z()) + } + + #[must_use] + pub fn surface_area(&self) -> f64 { + let mut props = ffi::g_prop::GProps_new(); + let inner_shape = ffi::topo_ds::cast_solid_to_shape(&self.inner); + ffi::b_rep_g_prop::BRepGProp::SurfaceProperties(inner_shape, props.pin_mut(), false, false); + props.Mass() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_volume_of_box() { + let shape = Shape::box_centered(10.0, 10.0, 10.0); + let solid = shape.expect_solid(); + let volume = solid.volume(); + assert!((volume - 1000.0).abs() <= 0.00001, "Expected volume ~1000.0, got {volume}",); + } + + #[test] + fn test_volume_of_cylinder() { + let shape = Shape::cylinder_radius_height(3.0, 15.0); + let solid = shape.expect_solid(); + let volume = solid.volume(); + let expected = std::f64::consts::PI * 9.0 * 15.0; + assert!((volume - expected).abs() <= 0.001, "Expected volume ~{expected}, got {volume}",); + } + + #[test] + fn test_center_of_mass_of_centered_box() { + let shape = Shape::box_centered(10.0, 10.0, 10.0); + let solid = shape.expect_solid(); + let com = solid.center_of_mass(); + assert!( + com.distance_squared(dvec3(0.0, 0.0, 0.0)) <= 0.00001, + "Expected center of mass at (0, 0, 0), got ({}, {}, {})", + com.x, + com.y, + com.z, + ); + } + + #[test] + fn test_center_of_mass_of_translated_box() { + let shape = Shape::box_with_dimensions(10.0, 10.0, 10.0).translated(dvec3(5.0, 5.0, 5.0)); + let solid = shape.expect_solid(); + let com = solid.center_of_mass(); + assert!( + com.distance_squared(dvec3(10.0, 10.0, 10.0)) <= 0.00001, + "Expected center of mass at (10, 10, 10), got ({}, {}, {})", + com.x, + com.y, + com.z, + ); + } + + #[test] + fn test_surface_area_of_box() { + let shape = Shape::box_centered(10.0, 10.0, 10.0); + let solid = shape.expect_solid(); + let area = solid.surface_area(); + assert!((area - 600.0).abs() <= 0.00001, "Expected surface area ~600.0, got {area}",); + } } diff --git a/crates/opencascade/src/primitives/surface.rs b/crates/opencascade/src/primitives/surface.rs index 1c93498ec..098f21ed5 100644 --- a/crates/opencascade/src/primitives/surface.rs +++ b/crates/opencascade/src/primitives/surface.rs @@ -1,6 +1,6 @@ -use crate::primitives::make_point; +use crate::primitives::{make_point, SurfaceType}; use cxx::UniquePtr; -use glam::DVec3; +use glam::{dvec3, DVec3}; use opencascade_sys as ffi; pub struct Surface { @@ -31,4 +31,131 @@ impl Surface { Self { inner } } + + pub fn surface_type(&self) -> SurfaceType { + let dynamic_type = ffi::geom::DynamicType(&self.inner); + let name = ffi::standard::type_name(dynamic_type); + + match name.as_str() { + "Geom_Plane" => SurfaceType::Plane, + "Geom_CylindricalSurface" => SurfaceType::Cylinder, + "Geom_ConicalSurface" => SurfaceType::Cone, + "Geom_SphericalSurface" => SurfaceType::Sphere, + "Geom_ToroidalSurface" => SurfaceType::Torus, + "Geom_BezierSurface" => SurfaceType::BezierSurface, + "Geom_BSplineSurface" => SurfaceType::BSplineSurface, + "Geom_SurfaceOfRevolution" => SurfaceType::SurfaceOfRevolution, + "Geom_SurfaceOfExtrusion" => SurfaceType::SurfaceOfExtrusion, + "Geom_OffsetSurface" => SurfaceType::OffsetSurface, + _ => SurfaceType::OtherSurface, + } + } + + pub fn axis(&self) -> Option<(DVec3, DVec3)> { + let surface_type = self.surface_type(); + match surface_type { + SurfaceType::Cylinder => { + let cyl = + ffi::geom::new_HandleGeomCylindricalSurface_from_HandleGeomSurface(&self.inner); + if cyl.IsNull() { + return None; + } + let pos = ffi::geom::HandleGeomCylindricalSurface_Position(&cyl); + Some(( + dvec3(pos.Location().X(), pos.Location().Y(), pos.Location().Z()), + dvec3(pos.Direction().X(), pos.Direction().Y(), pos.Direction().Z()), + )) + }, + SurfaceType::Sphere => { + let sphere = + ffi::geom::new_HandleGeomSphericalSurface_from_HandleGeomSurface(&self.inner); + if sphere.IsNull() { + return None; + } + let pos = ffi::geom::HandleGeomSphericalSurface_Position(&sphere); + Some(( + dvec3(pos.Location().X(), pos.Location().Y(), pos.Location().Z()), + dvec3(pos.Direction().X(), pos.Direction().Y(), pos.Direction().Z()), + )) + }, + SurfaceType::Cone => { + let cone = + ffi::geom::new_HandleGeomConicalSurface_from_HandleGeomSurface(&self.inner); + if cone.IsNull() { + return None; + } + let pos = ffi::geom::HandleGeomConicalSurface_Position(&cone); + Some(( + dvec3(pos.Location().X(), pos.Location().Y(), pos.Location().Z()), + dvec3(pos.Direction().X(), pos.Direction().Y(), pos.Direction().Z()), + )) + }, + SurfaceType::Torus => { + let torus = + ffi::geom::new_HandleGeomToroidalSurface_from_HandleGeomSurface(&self.inner); + if torus.IsNull() { + return None; + } + let pos = ffi::geom::HandleGeomToroidalSurface_Position(&torus); + Some(( + dvec3(pos.Location().X(), pos.Location().Y(), pos.Location().Z()), + dvec3(pos.Direction().X(), pos.Direction().Y(), pos.Direction().Z()), + )) + }, + _ => None, + } + } + + pub fn radius(&self) -> Option { + let surface_type = self.surface_type(); + match surface_type { + SurfaceType::Cylinder => { + let cyl = + ffi::geom::new_HandleGeomCylindricalSurface_from_HandleGeomSurface(&self.inner); + if cyl.IsNull() { + return None; + } + Some(ffi::geom::HandleGeomCylindricalSurface_Radius(&cyl)) + }, + SurfaceType::Sphere => { + let sphere = + ffi::geom::new_HandleGeomSphericalSurface_from_HandleGeomSurface(&self.inner); + if sphere.IsNull() { + return None; + } + Some(ffi::geom::HandleGeomSphericalSurface_Radius(&sphere)) + }, + SurfaceType::Cone => { + let cone = + ffi::geom::new_HandleGeomConicalSurface_from_HandleGeomSurface(&self.inner); + if cone.IsNull() { + return None; + } + Some(ffi::geom::HandleGeomConicalSurface_RefRadius(&cone)) + }, + SurfaceType::Torus => { + let torus = + ffi::geom::new_HandleGeomToroidalSurface_from_HandleGeomSurface(&self.inner); + if torus.IsNull() { + return None; + } + Some(ffi::geom::HandleGeomToroidalSurface_MajorRadius(&torus)) + }, + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::workplane::Workplane; + + #[test] + fn test_surface_type_plane() { + let face = Workplane::xy().rect(7.0, 5.0).to_face(); + let handle = ffi::b_rep::BRep_Tool_Surface(&face.inner); + let surface = Surface { inner: handle }; + assert_eq!(surface.surface_type(), SurfaceType::Plane); + } } diff --git a/crates/opencascade/src/primitives/wire.rs b/crates/opencascade/src/primitives/wire.rs index 667a80ba0..253b1cdd5 100644 --- a/crates/opencascade/src/primitives/wire.rs +++ b/crates/opencascade/src/primitives/wire.rs @@ -263,6 +263,21 @@ impl Wire { Face::from_face(make_face.Face()) } + pub fn length(&self) -> f64 { + let mut props = ffi::g_prop::GProps_new(); + ffi::b_rep_g_prop::BRepGProp::LinearProperties( + ffi::topo_ds::cast_wire_to_shape(&self.inner), + props.pin_mut(), + false, + false, + ); + props.Mass() + } + + pub fn is_closed(&self) -> bool { + ffi::b_rep::BRep_Tool_IsClosed(&self.inner) + } + // Create a closure-based API pub fn freeform() {} } diff --git a/crates/opencascade/tests/edge_inspection.rs b/crates/opencascade/tests/edge_inspection.rs new file mode 100644 index 000000000..599054d14 --- /dev/null +++ b/crates/opencascade/tests/edge_inspection.rs @@ -0,0 +1,72 @@ +use glam::DVec3; +use opencascade::primitives::{Edge, PositionMode}; + +#[test] +fn line_segment_length() { + let edge = Edge::segment(DVec3::new(0.0, 0.0, 0.0), DVec3::new(3.0, 4.0, 0.0)); + let length = edge.length(); + approx_equal(length, 5.0); +} + +#[test] +fn circle_length() { + let edge = Edge::circle(DVec3::new(0.0, 0.0, 0.0), DVec3::Z, 5.0); + let length = edge.length(); + approx_equal(length, std::f64::consts::PI * 10.0); +} + +#[test] +fn line_tangent_at_midpoint() { + let edge = Edge::segment(DVec3::new(0.0, 0.0, 0.0), DVec3::new(10.0, 0.0, 0.0)); + let tangent = edge.tangent_at(0.5, PositionMode::Parameter); + let expected = DVec3::new(1.0, 0.0, 0.0); + assert!(tangent.distance_squared(expected) < 1e-6, "expected {expected:?}, got {tangent:?}"); +} + +#[test] +fn circle_tangent_at_start_perpendicular_to_radius() { + let edge = Edge::circle(DVec3::new(0.0, 0.0, 0.0), DVec3::Z, 5.0); + let tangent = edge.tangent_at(0.0, PositionMode::Parameter); + let radius_vec = edge.start_point() - DVec3::new(0.0, 0.0, 0.0); + let dot = tangent.dot(radius_vec); + assert!(dot.abs() < 1e-6, "tangent {tangent:?} dot radius {radius_vec:?} = {dot}, expected ~0"); + assert!(tangent.z.abs() < 1e-6, "tangent {tangent:?} should lie in XY plane"); +} + +#[test] +fn arc_center_returns_center_for_circle() { + let center = DVec3::new(10.0, 20.0, 30.0); + let edge = Edge::circle(center, DVec3::Z, 5.0); + let result = edge.arc_center(); + assert!(result.is_some(), "Expected Some center for circle edge"); + let result = result.unwrap(); + assert!(result.distance_squared(center) < 1e-6, "expected {center:?}, got {result:?}"); +} + +#[test] +fn arc_center_returns_none_for_line() { + let edge = Edge::segment(DVec3::ZERO, DVec3::X); + let result = edge.arc_center(); + assert!(result.is_none(), "Expected None for line edge, got {result:?}"); +} + +#[test] +fn radius_returns_value_for_circle() { + let edge = Edge::circle(DVec3::ZERO, DVec3::Z, 7.0); + let result = edge.radius(); + assert!(result.is_some(), "Expected Some radius for circle edge"); + approx_equal(result.unwrap(), 7.0); +} + +#[test] +fn radius_returns_none_for_line() { + let edge = Edge::segment(DVec3::ZERO, DVec3::X); + let result = edge.radius(); + assert!(result.is_none(), "Expected None for line edge, got {result:?}"); +} + +fn approx_equal(a: f64, b: f64) { + let diff = (a - b).abs(); + let rel = diff / b.abs().max(1e-12); + assert!(rel < 1e-4 || diff < 1e-6, "expected {b}, got {a} (diff={diff}, rel={rel})"); +} diff --git a/crates/opencascade/tests/wire_inspection.rs b/crates/opencascade/tests/wire_inspection.rs new file mode 100644 index 000000000..b6276a05e --- /dev/null +++ b/crates/opencascade/tests/wire_inspection.rs @@ -0,0 +1,28 @@ +use glam::DVec3; +use opencascade::primitives::{Edge, Wire}; + +#[test] +fn rectangle_length() { + let wire = Wire::rect(7.0, 5.0); + let length = wire.length(); + approx_equal(length, 24.0); +} + +#[test] +fn closed_wire_returns_true() { + let wire = Wire::rect(10.0, 10.0); + assert!(wire.is_closed()); +} + +#[test] +fn open_wire_returns_false() { + let edge = Edge::segment(DVec3::ZERO, DVec3::new(10.0, 0.0, 0.0)); + let wire = Wire::from_edges([&edge]); + assert!(!wire.is_closed()); +} + +fn approx_equal(a: f64, b: f64) { + let diff = (a - b).abs(); + let rel = diff / b.abs().max(1e-12); + assert!(rel < 1e-4 || diff < 1e-6, "expected {b}, got {a} (diff={diff}, rel={rel})"); +}