diff --git a/Cargo.lock b/Cargo.lock index 7bdc843..e79e0b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,7 +237,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opencascade-sys" version = "0.2.0" -source = "git+https://github.com/bschwind/opencascade-rs?rev=d1db1bf1fb58dd094144532aa0e5c22106d61083#d1db1bf1fb58dd094144532aa0e5c22106d61083" +source = "git+https://github.com/bschwind/opencascade-rs?rev=c30da56647c2a60393984458439180886ecaf951#c30da56647c2a60393984458439180886ecaf951" dependencies = [ "cmake", "cxx", diff --git a/Cargo.toml b/Cargo.toml index f37377d..749c06d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ builtin = [ "opencascade-sys/builtin" ] [dependencies] cxx = "1" iter_fixed = "0.4.0" -opencascade-sys = { git = "https://github.com/bschwind/opencascade-rs", rev = "d1db1bf1fb58dd094144532aa0e5c22106d61083" } +opencascade-sys = { git = "https://github.com/bschwind/opencascade-rs", rev = "c30da56647c2a60393984458439180886ecaf951" } tempfile = "3.19.1" [dev-dependencies] diff --git a/src/core/dir.rs b/src/core/dir.rs index 1398c22..6f608d4 100644 --- a/src/core/dir.rs +++ b/src/core/dir.rs @@ -63,6 +63,23 @@ impl Dir { pub fn dot(&self, other: Self) -> f64 { self.0.into_iter().zip(other.0).map(|(a, b)| a * b).sum() } + + /// Return true if this `Dir` has less than a 0.000001% difference to another. + /// + /// ```rust + /// use anvil::dir; + /// + /// assert!(dir!(1, 1).approx_eq(dir!(1.00000001, 1))); + /// assert!(!dir!(1, 1).approx_eq(dir!(0.5, 1))); + /// ``` + pub fn approx_eq(&self, other: Dir) -> bool { + for (s, o) in self.0.iter().zip(other.0) { + if (s / o - 1.).abs() > 0.0000001 { + return false; + } + } + true + } } impl Dir<2> { diff --git a/src/core/length.rs b/src/core/length.rs index 8a73bf4..fb6ee05 100644 --- a/src/core/length.rs +++ b/src/core/length.rs @@ -1,4 +1,7 @@ -use std::ops::{Add, Div, Mul, Neg, Sub}; +use std::{ + fmt::Debug, + ops::{Add, Div, Mul, Neg, Sub}, +}; use crate::{Dir, IntoF64, Point}; @@ -27,7 +30,7 @@ use crate::{Dir, IntoF64, Point}; /// assert_eq!(4.5.cm(), Length::from_cm(4.5)); /// assert_eq!(12.in_(), Length::from_in(12.)); /// ``` -#[derive(Debug, PartialEq, Copy, Clone, PartialOrd)] +#[derive(PartialEq, Copy, Clone, PartialOrd)] pub struct Length { meters: f64, } @@ -41,7 +44,7 @@ impl Length { /// let len = Length::zero(); /// assert_eq!(len.m(), 0.); /// ``` - pub fn zero() -> Self { + pub const fn zero() -> Self { Self { meters: 0. } } /// Construct a `Length` from a value of unit meters. @@ -53,11 +56,11 @@ impl Length { /// let len = Length::from_m(3.2); /// assert_eq!(len.mm(), 3200.); /// ``` - pub fn from_m(value: f64) -> Self { + pub const fn from_m(value: f64) -> Self { Self { meters: value } } /// Return the value of this `Length` in millimeters. - pub fn m(&self) -> f64 { + pub const fn m(&self) -> f64 { self.meters } /// Construct a `Length` from a value of unit yards. @@ -69,11 +72,11 @@ impl Length { /// let len = Length::from_yd(1.); /// assert_eq!(len.m(), 0.9144); /// ``` - pub fn from_yd(value: f64) -> Self { + pub const fn from_yd(value: f64) -> Self { Self::from_m(value * 0.9144) } /// Return the value of this `Length` in yards. - pub fn yd(&self) -> f64 { + pub const fn yd(&self) -> f64 { self.m() / 0.9144 } /// Construct a `Length` from a value of unit feet. @@ -85,11 +88,11 @@ impl Length { /// let len = Length::from_ft(1.); /// assert_eq!(len.cm(), 30.48); /// ``` - pub fn from_ft(value: f64) -> Self { + pub const fn from_ft(value: f64) -> Self { Self::from_m(value * 0.3048) } /// Return the value of this `Length` in feet. - pub fn ft(&self) -> f64 { + pub const fn ft(&self) -> f64 { self.m() / 0.3048 } /// Construct a `Length` from a value of unit decimeters. @@ -101,11 +104,11 @@ impl Length { /// let len = Length::from_dm(5.1); /// assert_eq!(len.mm(), 510.); /// ``` - pub fn from_dm(value: f64) -> Self { + pub const fn from_dm(value: f64) -> Self { Self::from_m(value / 10.) } /// Return the value of this `Length` in decimeters. - pub fn dm(&self) -> f64 { + pub const fn dm(&self) -> f64 { self.m() * 10. } /// Construct a `Length` from a value of unit inches. @@ -117,14 +120,14 @@ impl Length { /// let len = Length::from_in(1.); /// assert_eq!(len.cm(), 2.54); /// ``` - pub fn from_in(value: f64) -> Self { + pub const fn from_in(value: f64) -> Self { Self::from_m(value * 0.0254) } /// Return the value of this `Length` in inches. /// /// This method breaks the pattern with the trailing underscore, because `in` is a reserved /// keyword in Rust. - pub fn in_(&self) -> f64 { + pub const fn in_(&self) -> f64 { self.m() / 0.0254 } /// Construct a `Length` from a value of unit centimeters. @@ -136,11 +139,11 @@ impl Length { /// let len = Length::from_cm(5.1); /// assert_eq!(len.mm(), 51.); /// ``` - pub fn from_cm(value: f64) -> Self { + pub const fn from_cm(value: f64) -> Self { Self::from_m(value / 100.) } /// Return the value of this `Length` in centimeters. - pub fn cm(&self) -> f64 { + pub const fn cm(&self) -> f64 { self.m() * 100. } /// Construct a `Length` from a value of unit millimeters. @@ -152,11 +155,11 @@ impl Length { /// let len = Length::from_mm(5.4); /// assert_eq!(len.m(), 0.0054); /// ``` - pub fn from_mm(value: f64) -> Self { + pub const fn from_mm(value: f64) -> Self { Self::from_m(value / 1000.) } /// Return the value of this `Length` in millimeters. - pub fn mm(&self) -> f64 { + pub const fn mm(&self) -> f64 { self.m() * 1000. } @@ -168,7 +171,7 @@ impl Length { /// assert_eq!((-5).m().abs(), 5.m()); /// assert_eq!(5.m().abs(), 5.m()); /// ``` - pub fn abs(&self) -> Self { + pub const fn abs(&self) -> Self { Self { meters: self.meters.abs(), } @@ -184,7 +187,7 @@ impl Length { /// assert_eq!(len1.min(&len2), len1); /// assert_eq!(len2.min(&len1), len1); /// ``` - pub fn min(&self, other: &Self) -> Self { + pub const fn min(&self, other: &Self) -> Self { Length::from_m(self.m().min(other.m())) } /// Return the larger of two lengths. @@ -198,10 +201,15 @@ impl Length { /// assert_eq!(len1.max(&len2), len2); /// assert_eq!(len2.max(&len1), len2); /// ``` - pub fn max(&self, other: &Self) -> Self { + pub const fn max(&self, other: &Self) -> Self { Length::from_m(self.m().max(other.m())) } } +impl Debug for Length { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(format!("{}m", self.m()).as_str()) + } +} impl Add for Length { type Output = Length; diff --git a/src/errors.rs b/src/errors.rs index 19dd94e..a523c07 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -17,11 +17,14 @@ pub enum Error { /// Occurs when a `Part` could not be written to a .stl file at a given path. StlWrite(PathBuf), - /// Occurs when an operation that requires a length is performed on a `Dir3D` with a magnitude of zero. - ZeroVector, + /// Occurs when a `Face` or `Part` can not be triangulated. + Triangulation, /// Occurs when two vectors that are required to be orthogonal, are not. VectorsNotOrthogonal(Dir<3>, Dir<3>), + + /// Occurs when an operation that requires a length is performed on a `Dir3D` with a magnitude of zero. + ZeroVector, } impl StdError for Error {} impl fmt::Display for Error { diff --git a/src/faces/face.rs b/src/faces/face.rs new file mode 100644 index 0000000..5d34db4 --- /dev/null +++ b/src/faces/face.rs @@ -0,0 +1,16 @@ +use cxx::UniquePtr; +use opencascade_sys::ffi; + +/// A 2D surface that has a clear bound. +pub struct Face(pub(crate) UniquePtr); +impl Face { + pub(crate) fn from_occt(occt: &ffi::TopoDS_Face) -> Self { + Self(ffi::TopoDS_Face_to_owned(occt)) + } +} + +impl Clone for Face { + fn clone(&self) -> Self { + Self::from_occt(&self.0) + } +} diff --git a/src/faces/iterator.rs b/src/faces/iterator.rs new file mode 100644 index 0000000..36a2fbc --- /dev/null +++ b/src/faces/iterator.rs @@ -0,0 +1,90 @@ +use cxx::UniquePtr; +use opencascade_sys::ffi; + +use crate::Part; + +use super::face::Face; + +/// Iterator over the `Face`s of a `Part`. +/// +/// ```rust +/// use anvil::{Cube, Face, FaceIterator, IntoLength}; +/// +/// let face_iterator: FaceIterator = Cube::from_size(1.m()).faces(); +/// for face in face_iterator { +/// // ... +/// } +/// ``` +pub enum FaceIterator { + /// A FaceIterator that is not empty. + NotEmpty(Part, UniquePtr), + /// A FaceIterator from an empty shape. + Empty, +} + +impl Iterator for FaceIterator { + type Item = Face; + + fn next(&mut self) -> Option { + match self { + Self::NotEmpty(_, explorer) => { + if explorer.More() { + let face = ffi::TopoDS_cast_to_face(explorer.Current()); + let face = Face::from_occt(face); + explorer.pin_mut().Next(); + Some(face) + } else { + None + } + } + Self::Empty => None, + } + } +} +impl ExactSizeIterator for FaceIterator { + fn len(&self) -> usize { + match self { + Self::NotEmpty(_, _) => { + let mut len = 0; + for _ in self.clone_without_position() { + len += 1; + } + len + } + Self::Empty => 0, + } + } +} +impl FaceIterator { + /// Return `true` if this `FaceIterator` has a length of 0. + pub fn is_empty(self) -> bool { + self.len() == 0 + } + fn clone_without_position(&self) -> Self { + match self { + Self::NotEmpty(part, _) => part.faces(), + Self::Empty => Self::Empty, + } + } +} +impl From<&Part> for FaceIterator { + fn from(value: &Part) -> Self { + match &value.inner { + Some(inner) => { + let explorer = ffi::TopExp_Explorer_ctor(inner, ffi::TopAbs_ShapeEnum::TopAbs_FACE); + Self::NotEmpty(value.clone(), explorer) + } + None => Self::Empty, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty() { + assert!(Part::empty().faces().is_empty()) + } +} diff --git a/src/faces/mod.rs b/src/faces/mod.rs new file mode 100644 index 0000000..438ec73 --- /dev/null +++ b/src/faces/mod.rs @@ -0,0 +1,5 @@ +mod face; +mod iterator; + +pub use face::Face; +pub use iterator::FaceIterator; diff --git a/src/lib.rs b/src/lib.rs index e34fda0..92fc083 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,13 @@ #![doc = "A CAD engine."] #![allow(clippy::approx_constant)] #![warn(missing_docs)] +#![warn(clippy::todo)] +#![warn(clippy::unimplemented)] mod core; mod errors; +mod faces; +mod meshes; mod parts; mod sketches; @@ -11,6 +15,8 @@ pub use core::{ Angle, Axis, Dir, Edge, IntoAngle, IntoF64, IntoLength, Length, Path, Plane, Point, }; pub use errors::Error; +pub use faces::{Face, FaceIterator}; +pub use meshes::RenderMesh; pub use parts::{ Part, primitives::{Cube, Cuboid, Cylinder, Sphere}, diff --git a/src/meshes/mod.rs b/src/meshes/mod.rs new file mode 100644 index 0000000..56cb275 --- /dev/null +++ b/src/meshes/mod.rs @@ -0,0 +1,3 @@ +mod render_mesh; + +pub use render_mesh::RenderMesh; diff --git a/src/meshes/render_mesh.rs b/src/meshes/render_mesh.rs new file mode 100644 index 0000000..f98bbed --- /dev/null +++ b/src/meshes/render_mesh.rs @@ -0,0 +1,438 @@ +use opencascade_sys::ffi; + +use crate::{Dir, Error, Face, IntoLength, Length, Part, Point}; + +const DEFAULT_TOLERANCE: Length = Length::from_m(0.000001); + +/// A triangular mesh of one or more `Face`s optimized for 3D rendering. +#[derive(Clone, Debug, PartialEq)] +pub struct RenderMesh { + points: Vec>, + indices: Vec<[usize; 3]>, + normals: Vec>, + uvs: Vec<[f64; 2]>, +} +impl RenderMesh { + /// Return a clone of this `RenderMesh` with the individual indices sorted. + /// + /// Sorting of the triangle indices depends on the machine executing the tests which introduces + /// non-deterministic behavior. This function enables comparing `RenderMesh`es across devices. + pub fn sorted(&self) -> Self { + Self { + points: self.points.clone(), + indices: { + let mut sorted_indices = vec![]; + for triangle in self.indices.clone() { + let mut sorted_triangle = triangle; + sorted_triangle.sort(); + sorted_indices.push(sorted_triangle); + } + sorted_indices + }, + normals: self.normals.clone(), + uvs: self.uvs.clone(), + } + } + + /// Return the `Point`s of this `RenderMesh`. + pub fn points(&self) -> &Vec> { + &self.points + } + /// Return the `Point` indices defining the triangles of this `RenderMesh`. + pub fn indices(&self) -> &Vec<[usize; 3]> { + &self.indices + } + /// Return the normal `Dir` of every `Point` in this `RenderMesh`. + pub fn normals(&self) -> &Vec> { + &self.normals + } + /// Return the relative position of every `Point` on the 2D-grid of this `RenderMesh`. + pub fn uvs(&self) -> &Vec<[f64; 2]> { + &self.uvs + } + + /// Return the collective area spanned by the triangles in a `RenderedMesh` in square meters. + /// + /// ```rust + /// use anvil::{Cube, IntoLength, Plane, Rectangle, RenderMesh}; + /// + /// let rect = Rectangle::from_dim(2.m(), 3.m()); + /// let mesh = RenderMesh::try_from(rect.to_face(Plane::xy()).unwrap()).unwrap(); + /// assert!((mesh.area() - 6.).abs() < 0.0001); + /// + /// let cube = Cube::from_size(2.m()); + /// let mesh = RenderMesh::try_from(cube).unwrap(); + /// assert!((mesh.area() - 24.).abs() < 0.0001); + /// ``` + pub fn area(&self) -> f64 { + let mut total_area = 0.; + for triangle in &self.indices { + let point1 = *self + .points + .get(triangle[0]) + .expect("index should be a valid point"); + let point2 = *self + .points + .get(triangle[1]) + .expect("index should be a valid point"); + let point3 = *self + .points + .get(triangle[2]) + .expect("index should be a valid point"); + let edge_1 = point2 - point1; + let edge_2 = point3 - point1; + + let cross = ( + edge_1.y().m() * edge_2.z().m() - edge_1.z().m() * edge_2.y().m(), + edge_1.z().m() * edge_2.x().m() - edge_1.x().m() * edge_2.z().m(), + edge_1.x().m() * edge_2.y().m() - edge_1.y().m() * edge_2.x().m(), + ); + + total_area += 0.5 * f64::sqrt(cross.0.powi(2) + cross.1.powi(2) + cross.2.powi(2)); + } + total_area + } + /// Return the center point of the `RenderMesh`, i.e. the average of all mesh points. + /// + /// ```rust + /// use anvil::{IntoLength, Plane, Rectangle, RenderMesh, point}; + /// + /// let rect = Rectangle::from_dim(1.m(), 1.m()).move_to(point!(2.m(), 3.m())); + /// let mesh = RenderMesh::try_from(rect.to_face(Plane::xy()).unwrap()).unwrap(); + /// let mesh_center = mesh.center(); + /// assert!((mesh_center.x() - 2.m()).abs() < 0.0001.m()); + /// assert!((mesh_center.y() - 3.m()).abs() < 0.0001.m()); + /// assert!(mesh_center.z().abs() < 0.0001.m()); + /// ``` + pub fn center(&self) -> Point<3> { + let mut sum_of_points = Point::<3>::origin(); + for point in &self.points { + sum_of_points = sum_of_points + *point; + } + sum_of_points / self.points.len() as f64 + } + + fn empty() -> Self { + Self { + points: vec![], + indices: vec![], + normals: vec![], + uvs: vec![], + } + } + + fn merge_with(&mut self, other: Self) { + self.indices.extend(other.indices().iter().map(|t| { + [ + t[0] + self.points.len(), + t[1] + self.points.len(), + t[2] + self.points.len(), + ] + })); + self.points.extend(other.points()); + self.normals.extend(other.normals()); + self.uvs.extend(other.uvs()); + } +} + +impl TryFrom for RenderMesh { + type Error = Error; + fn try_from(face: Face) -> Result { + (face, DEFAULT_TOLERANCE).try_into() + } +} +impl TryFrom<(Face, Length)> for RenderMesh { + type Error = Error; + fn try_from((face, tolerance): (Face, Length)) -> Result { + let mesh = ffi::BRepMesh_IncrementalMesh_ctor( + ffi::cast_face_to_shape(face.0.as_ref().unwrap()), + tolerance.m(), + ); + let face = ffi::TopoDS_cast_to_face(mesh.as_ref().unwrap().Shape()); + let mut location = ffi::TopLoc_Location_ctor(); + + let triangulation_handle = ffi::BRep_Tool_Triangulation(face, location.pin_mut()); + let transformation = ffi::TopLoc_Location_Transformation(&location); + + if let Ok(triangulation) = ffi::HandlePoly_Triangulation_Get(&triangulation_handle) { + let mut points = vec![]; + let mut indices = vec![]; + let mut normals = vec![]; + let mut uvs = vec![]; + + let orientation = face.Orientation(); + let face_point_count = triangulation.NbNodes(); + ffi::compute_normals(face, &triangulation_handle); + + for node_index in 1..=face_point_count { + let mut point = ffi::Poly_Triangulation_Node(triangulation, node_index); + point.pin_mut().Transform(&transformation); + points.push(Point::<3>::new([ + point.X().m(), + point.Y().m(), + point.Z().m(), + ])); + + let uv = ffi::Poly_Triangulation_UV(triangulation, node_index); + uvs.push([uv.X(), uv.Y()]); + + let mut normal = ffi::Poly_Triangulation_Normal(triangulation, node_index); + normal.pin_mut().Transform(&transformation); + let m = if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { + -1. + } else { + 1. + }; + normals.push( + Dir::try_from([normal.X() * m, normal.Y() * m, normal.Z() * m]) + .expect("normals should not be zero"), + ); + } + + let mut u_min = f64::INFINITY; + let mut v_min = f64::INFINITY; + let mut u_max = f64::NEG_INFINITY; + let mut v_max = f64::NEG_INFINITY; + + for &[u, v] in &uvs { + u_min = u_min.min(u); + v_min = v_min.min(v); + u_max = u_max.max(u); + v_max = v_max.max(v); + } + + for [u, v] in &mut uvs { + *u = (*u - u_min) / (u_max - u_min); + *v = (*v - v_min) / (v_max - v_min); + + if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { + *u = 1.0 - *u; + } + } + + for triangle_index in 1..=triangulation.NbTriangles() { + let triangle = triangulation.Triangle(triangle_index); + let mut node_ids = [triangle.Value(1), triangle.Value(2), triangle.Value(3)] + .map(|id| id as usize - 1); + + if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { + node_ids.swap(1, 2); + } + indices.push(node_ids); + } + + Ok(RenderMesh { + points, + indices, + normals, + uvs, + }) + } else { + Err(Error::Triangulation) + } + } +} +impl TryFrom for RenderMesh { + type Error = Error; + fn try_from(part: Part) -> Result { + (part, DEFAULT_TOLERANCE).try_into() + } +} +impl TryFrom<(Part, Length)> for RenderMesh { + type Error = Error; + fn try_from((part, tolerance): (Part, Length)) -> Result { + let meshes = part + .faces() + .map(|face| RenderMesh::try_from((face, tolerance))) + .collect::, Error>>()?; + Ok(merge(meshes)) + } +} + +fn merge(meshes: Vec) -> RenderMesh { + let mut merged_mesh = RenderMesh::empty(); + for mesh in meshes { + merged_mesh.merge_with(mesh); + } + merged_mesh +} + +#[cfg(test)] +mod tests { + use core::f64; + + use crate::{Axis, Circle, Cube, IntoAngle, IntoLength, Path, Plane, Rectangle, dir, point}; + + use super::*; + + #[test] + fn triangle() { + let face = Path::at(point!(0, 0)) + .line_to(point!(1.m(), 0.m())) + .line_to(point!(0.m(), 1.m())) + .close() + .to_face(Plane::xy()) + .unwrap(); + + assert_eq!( + RenderMesh::try_from(face).unwrap().sorted(), + RenderMesh { + points: vec![ + point!(0, 0, 0), + point!(1.m(), 0.m(), 0.m()), + point!(0.m(), 1.m(), 0.m()) + ], + indices: vec![[0, 1, 2]], + normals: vec![dir!(0, 0, 1), dir!(0, 0, 1), dir!(0, 0, 1)], + uvs: vec![[0., 0.], [1., 0.], [0., 1.]] + } + ) + } + + #[test] + fn rectangle() { + let face = Rectangle::from_corners(point!(0, 0), point!(1.m(), 1.m())) + .to_face(Plane::xy()) + .unwrap(); + + assert_eq!( + RenderMesh::try_from(face).unwrap().sorted(), + RenderMesh { + points: vec![ + point!(0, 0, 0), + point!(1.m(), 0.m(), 0.m()), + point!(1.m(), 1.m(), 0.m()), + point!(0.m(), 1.m(), 0.m()), + ], + indices: vec![[0, 1, 2], [0, 2, 3]], + normals: vec![dir!(0, 0, 1), dir!(0, 0, 1), dir!(0, 0, 1), dir!(0, 0, 1)], + uvs: vec![[0., 0.], [1., 0.], [1., 1.], [0., 1.]] + } + ) + } + + #[test] + fn circle() { + let mesh = + RenderMesh::try_from(Circle::from_radius(1.m()).to_face(Plane::xy()).unwrap()).unwrap(); + assert!(mesh.center().x().abs().m() < 0.00001); + assert!(mesh.center().y().abs().m() < 0.00001); + assert!(mesh.center().z().abs().m() < 0.00001); + assert!((mesh.area() - f64::consts::PI).abs() < 0.00001); + + assert_eq!(mesh.normals(), &vec![dir!(0, 0, -1); mesh.normals().len()]); + } + + #[test] + fn rotated_cube_has_correct_normals() { + let cube = Cube::from_size(1.m()) + .rotate_around(Axis::<3>::x(), 45.deg()) + .rotate_around(Axis::<3>::z(), 45.deg()); + let mesh = + RenderMesh::try_from(cube.faces().collect::>().first().unwrap().clone()) + .unwrap(); + for normal in mesh.normals { + assert!(normal.approx_eq(dir!(-1, -1, 0))) + } + } + + #[test] + fn cube() { + let cube_mesh = RenderMesh::try_from(Cube::from_size(2.m())) + .unwrap() + .sorted(); + assert_eq!( + cube_mesh.points(), + &vec![ + // -x face + point!(-1.m(), -1.m(), -1.m()), + point!(-1.m(), -1.m(), 1.m()), + point!(-1.m(), 1.m(), -1.m()), + point!(-1.m(), 1.m(), 1.m()), + // +x face + point!(1.m(), -1.m(), -1.m()), + point!(1.m(), -1.m(), 1.m()), + point!(1.m(), 1.m(), -1.m()), + point!(1.m(), 1.m(), 1.m()), + // -y face + point!(-1.m(), -1.m(), -1.m()), + point!(1.m(), -1.m(), -1.m()), + point!(-1.m(), -1.m(), 1.m()), + point!(1.m(), -1.m(), 1.m()), + // +y face + point!(-1.m(), 1.m(), -1.m()), + point!(1.m(), 1.m(), -1.m()), + point!(-1.m(), 1.m(), 1.m()), + point!(1.m(), 1.m(), 1.m()), + // -z face + point!(-1.m(), -1.m(), -1.m()), + point!(-1.m(), 1.m(), -1.m()), + point!(1.m(), -1.m(), -1.m()), + point!(1.m(), 1.m(), -1.m()), + // +z face + point!(-1.m(), -1.m(), 1.m()), + point!(-1.m(), 1.m(), 1.m()), + point!(1.m(), -1.m(), 1.m()), + point!(1.m(), 1.m(), 1.m()), + ] + ); + assert_eq!( + cube_mesh.indices(), + &vec![ + // -x face + [0, 1, 2], + [1, 2, 3], + // +x face + [4, 5, 6], + [5, 6, 7], + // -y face + [8, 9, 11], + [8, 10, 11], + // +y face + [12, 13, 15], + [12, 14, 15], + // -z face + [16, 17, 19], + [16, 18, 19], + // +z face + [20, 21, 23], + [20, 22, 23], + ] + ); + assert_eq!( + cube_mesh.normals(), + &vec![ + // -x face + dir!(-1, 0, 0), + dir!(-1, 0, 0), + dir!(-1, 0, 0), + dir!(-1, 0, 0), + // +x face + dir!(1, 0, 0), + dir!(1, 0, 0), + dir!(1, 0, 0), + dir!(1, 0, 0), + // -y face + dir!(0, -1, 0), + dir!(0, -1, 0), + dir!(0, -1, 0), + dir!(0, -1, 0), + // +y face + dir!(0, 1, 0), + dir!(0, 1, 0), + dir!(0, 1, 0), + dir!(0, 1, 0), + // -z face + dir!(0, 0, -1), + dir!(0, 0, -1), + dir!(0, 0, -1), + dir!(0, 0, -1), + // +z face + dir!(0, 0, 1), + dir!(0, 0, 1), + dir!(0, 0, 1), + dir!(0, 0, 1), + ] + ) + } +} diff --git a/src/parts/part.rs b/src/parts/part.rs index 87ac02d..bcd006f 100644 --- a/src/parts/part.rs +++ b/src/parts/part.rs @@ -9,7 +9,7 @@ use cxx::UniquePtr; use opencascade_sys::ffi; use tempfile::NamedTempFile; -use crate::{Angle, Axis, Error, IntoAngle, Length, Point, point}; +use crate::{Angle, Axis, Error, FaceIterator, IntoAngle, Length, Point, point}; /// A 3D object in space. pub struct Part { @@ -90,6 +90,18 @@ impl Part { } new_shape } + /// Return the faces spanned by this `Part`. + /// + /// ```rust + /// use anvil::{Cube, Cylinder, IntoLength, Sphere}; + /// + /// assert_eq!(Cube::from_size(1.m()).faces().len(), 6); + /// assert_eq!(Cylinder::from_radius(1.m(), 1.m()).faces().len(), 3); + /// assert_eq!(Sphere::from_radius(1.m()).faces().len(), 1); + /// ``` + pub fn faces(&self) -> FaceIterator { + self.into() + } /// Return the `Part` that is created from the overlapping volume between this one and another. /// /// ```rust diff --git a/src/sketches/sketch.rs b/src/sketches/sketch.rs index 26dfd60..523eb7b 100644 --- a/src/sketches/sketch.rs +++ b/src/sketches/sketch.rs @@ -3,7 +3,7 @@ use std::vec; use cxx::UniquePtr; use opencascade_sys::ffi; -use crate::{Angle, Axis, Edge, Error, IntoAngle, IntoLength, Length, Part, Plane, Point}; +use crate::{Angle, Axis, Edge, Error, Face, IntoAngle, IntoLength, Length, Part, Plane, Point}; /// A closed shape in 2D space. #[derive(Debug, Clone)] @@ -292,6 +292,13 @@ impl Sketch { Ok(Part::from_occt(make_solid.pin_mut().Shape())) } + /// Try to convert this `Sketch` into a `Face`. + pub fn to_face(self, plane: Plane) -> Result { + Ok(Face::from_occt(ffi::TopoDS_cast_to_face( + self.to_occt(plane)?.as_ref().unwrap(), + ))) + } + pub(crate) fn from_edges(edges: Vec) -> Self { Self(vec![SketchAction::AddEdges(edges)]) } @@ -390,10 +397,7 @@ impl SketchAction { Some(ffi::TopoDS_Shape_to_owned(operation.pin_mut().Shape())) } }, - SketchAction::AddEdges(edges) => match sketch { - None => edges_to_occt(edges, plane).ok(), - Some(_) => todo!(), - }, + SketchAction::AddEdges(edges) => edges_to_occt(edges, plane).ok(), SketchAction::Intersect(other) => match (sketch, other.to_occt(plane).ok()) { (Some(self_shape), Some(other_shape)) => { let mut operation = ffi::BRepAlgoAPI_Common_ctor(&self_shape, &other_shape);