From db239c5d5f972e4679106cdb0fc106e4ffd9a7bd Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 10:41:24 +0200 Subject: [PATCH 01/18] feat: Part.faces() --- src/faces/face.rs | 15 ++++++++++++ src/faces/iterator.rs | 53 +++++++++++++++++++++++++++++++++++++++++++ src/faces/mod.rs | 5 ++++ src/lib.rs | 2 ++ src/parts/part.rs | 14 +++++++++++- 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/faces/face.rs create mode 100644 src/faces/iterator.rs create mode 100644 src/faces/mod.rs diff --git a/src/faces/face.rs b/src/faces/face.rs new file mode 100644 index 0000000..9e6ba04 --- /dev/null +++ b/src/faces/face.rs @@ -0,0 +1,15 @@ +use cxx::UniquePtr; +use opencascade_sys::ffi; + +pub struct Face(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..b8c2c07 --- /dev/null +++ b/src/faces/iterator.rs @@ -0,0 +1,53 @@ +use cxx::UniquePtr; +use opencascade_sys::ffi; + +use crate::Part; + +use super::face::Face; + +pub struct FaceIterator(Part, UniquePtr); +impl Iterator for FaceIterator { + type Item = Face; + + fn next(&mut self) -> Option { + if self.1.More() { + let face = ffi::TopoDS_cast_to_face(self.1.Current()); + let face = Face::from_occt(face); + self.1.pin_mut().Next(); + Some(face) + } else { + None + } + } +} +impl FaceIterator { + /// Return the number of `Face`s in this `FaceIterator`. + pub fn len(self) -> usize { + let mut self_clone = self.clone(); + let mut len = 0; + while let Some(_) = self_clone.next() { + len += 1; + } + len + } +} +impl Clone for FaceIterator { + /// Return a clone of this `FaceIterator`. + /// + /// WARNING: Iterator position will not be cloned. + fn clone(&self) -> Self { + self.0.faces() + } +} +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(value.clone(), explorer) + } + None => todo!(), + } + } +} 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..124de55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod core; mod errors; +mod faces; mod parts; mod sketches; @@ -11,6 +12,7 @@ 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 parts::{ Part, primitives::{Cube, Cuboid, Cylinder, Sphere}, 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 From 52f3f6459d50ba02d565312242037035ac653c75 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 10:46:58 +0200 Subject: [PATCH 02/18] ci: make clippy warn for todo and unimplemented macros --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 124de55..1a14f21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ #![doc = "A CAD engine."] #![allow(clippy::approx_constant)] #![warn(missing_docs)] +#![warn(clippy::todo)] +#![warn(clippy::unimplemented)] mod core; mod errors; From 26d7a4c21087e15e3086b0df02c0725a1a7e9195 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 13:41:29 +0200 Subject: [PATCH 03/18] feat: triangulation for points --- src/errors.rs | 7 +++- src/faces/face.rs | 2 +- src/lib.rs | 1 + src/meshes/mod.rs | 1 + src/meshes/textured_mesh.rs | 81 +++++++++++++++++++++++++++++++++++++ src/sketches/sketch.rs | 9 ++++- 6 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 src/meshes/mod.rs create mode 100644 src/meshes/textured_mesh.rs 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 index 9e6ba04..f879237 100644 --- a/src/faces/face.rs +++ b/src/faces/face.rs @@ -1,7 +1,7 @@ use cxx::UniquePtr; use opencascade_sys::ffi; -pub struct Face(UniquePtr); +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)) diff --git a/src/lib.rs b/src/lib.rs index 1a14f21..82f1758 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ mod core; mod errors; mod faces; +mod meshes; mod parts; mod sketches; diff --git a/src/meshes/mod.rs b/src/meshes/mod.rs new file mode 100644 index 0000000..deb8c3e --- /dev/null +++ b/src/meshes/mod.rs @@ -0,0 +1 @@ +mod textured_mesh; diff --git a/src/meshes/textured_mesh.rs b/src/meshes/textured_mesh.rs new file mode 100644 index 0000000..1c13cd1 --- /dev/null +++ b/src/meshes/textured_mesh.rs @@ -0,0 +1,81 @@ +use opencascade_sys::ffi; + +use crate::{Dir, Error, Face, IntoLength, Length, Point}; + +pub struct TexturedMesh { + points: Vec>, + indices: Vec<[usize; 3]>, + normals: Vec>, + uvs: Vec<[f64; 2]>, +} +impl TryFrom for TexturedMesh { + type Error = Error; + fn try_from(value: Face) -> Result { + Self::try_from((value, 0.1.mm())) + } +} +impl TryFrom<(Face, Length)> for TexturedMesh { + type Error = Error; + fn try_from(value: (Face, Length)) -> Result { + let mesh = ffi::BRepMesh_IncrementalMesh_ctor( + ffi::cast_face_to_shape(value.0.0.as_ref().unwrap()), + value.1.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 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(), + ])); + } + Ok(TexturedMesh { + points: points, + indices: vec![], + normals: vec![], + uvs: vec![], + }) + } else { + Err(Error::Triangulation) + } + } +} + +#[cfg(test)] +mod tests { + use crate::{IntoLength, Path, Plane, 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(); + let actual = TexturedMesh::try_from(face).unwrap(); + assert_eq!( + actual.points, + vec![ + point!(0, 0, 0), + point!(1.m(), 0.m(), 0.m()), + point!(0.m(), 1.m(), 0.m()) + ] + ); + } +} diff --git a/src/sketches/sketch.rs b/src/sketches/sketch.rs index 26dfd60..9355b86 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)]) } From fc73bc051452bbe537da021a8bb8ece5dec31613 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 13:51:23 +0200 Subject: [PATCH 04/18] feat: create custom Length Debug implementation --- src/core/length.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/length.rs b/src/core/length.rs index 8a73bf4..48961a4 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, } @@ -202,6 +205,11 @@ impl Length { 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; From 6a4218cb3fa332e4d6be3c129dec15fa72000aa2 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 14:26:33 +0200 Subject: [PATCH 05/18] feat: triangulation for other fields --- src/meshes/textured_mesh.rs | 107 +++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 13 deletions(-) diff --git a/src/meshes/textured_mesh.rs b/src/meshes/textured_mesh.rs index 1c13cd1..4611fdb 100644 --- a/src/meshes/textured_mesh.rs +++ b/src/meshes/textured_mesh.rs @@ -2,6 +2,7 @@ use opencascade_sys::ffi; use crate::{Dir, Error, Face, IntoLength, Length, Point}; +#[derive(Clone, Debug, PartialEq)] pub struct TexturedMesh { points: Vec>, indices: Vec<[usize; 3]>, @@ -30,6 +31,11 @@ impl TryFrom<(Face, Length)> for TexturedMesh { 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); @@ -41,12 +47,60 @@ impl TryFrom<(Face, Length)> for TexturedMesh { point.Y().m(), point.Z().m(), ])); + + let uv = ffi::Poly_Triangulation_UV(triangulation, node_index); + uvs.push([uv.X(), uv.Y()]); + + let normal = ffi::Poly_Triangulation_Normal(triangulation, node_index); + 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 + 0 - 1); + + if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { + node_ids.swap(1, 2); + } + + indices.push(node_ids); + } + Ok(TexturedMesh { - points: points, - indices: vec![], - normals: vec![], - uvs: vec![], + points, + indices, + normals, + uvs, }) } else { Err(Error::Triangulation) @@ -56,7 +110,7 @@ impl TryFrom<(Face, Length)> for TexturedMesh { #[cfg(test)] mod tests { - use crate::{IntoLength, Path, Plane, point}; + use crate::{IntoLength, Path, Plane, Rectangle, dir, point}; use super::*; @@ -68,14 +122,41 @@ mod tests { .close() .to_face(Plane::xy()) .unwrap(); - let actual = TexturedMesh::try_from(face).unwrap(); + assert_eq!( - actual.points, - vec![ - point!(0, 0, 0), - point!(1.m(), 0.m(), 0.m()), - point!(0.m(), 1.m(), 0.m()) - ] - ); + TexturedMesh::try_from(face), + Ok(TexturedMesh { + points: vec![ + point!(0, 0, 0), + point!(1.m(), 0.m(), 0.m()), + point!(0.m(), 1.m(), 0.m()) + ], + indices: vec![[1, 2, 0]], + 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!( + TexturedMesh::try_from(face), + Ok(TexturedMesh { + 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![[2, 0, 1], [2, 3, 0]], + 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.]] + }) + ) } } From 735a0dacb1468866564aee09576d2488437f7cb2 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 14:51:40 +0200 Subject: [PATCH 06/18] fix: make TexturedMesh index order deterministic --- src/meshes/textured_mesh.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/meshes/textured_mesh.rs b/src/meshes/textured_mesh.rs index 4611fdb..4a21c3b 100644 --- a/src/meshes/textured_mesh.rs +++ b/src/meshes/textured_mesh.rs @@ -87,12 +87,9 @@ impl TryFrom<(Face, Length)> for TexturedMesh { 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 + 0 - 1); - - if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { - node_ids.swap(1, 2); - } + .map(|id| id as usize - 1); + node_ids.sort(); // depending on device, nodes may be sorted differently - sorting them makes the order deterministic indices.push(node_ids); } @@ -131,7 +128,7 @@ mod tests { point!(1.m(), 0.m(), 0.m()), point!(0.m(), 1.m(), 0.m()) ], - indices: vec![[1, 2, 0]], + 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.]] }) @@ -153,7 +150,7 @@ mod tests { point!(1.m(), 1.m(), 0.m()), point!(0.m(), 1.m(), 0.m()), ], - indices: vec![[2, 0, 1], [2, 3, 0]], + 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.]] }) From 51d1002816a319aeb50c82275d790c8eaa6bb7e6 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 15:20:11 +0200 Subject: [PATCH 07/18] lint: obey clippy --- src/faces/face.rs | 1 + src/faces/iterator.rs | 79 ++++++++++++++++++++++++++++++++---------- src/sketches/sketch.rs | 5 +-- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/faces/face.rs b/src/faces/face.rs index f879237..5d34db4 100644 --- a/src/faces/face.rs +++ b/src/faces/face.rs @@ -1,6 +1,7 @@ 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 { diff --git a/src/faces/iterator.rs b/src/faces/iterator.rs index b8c2c07..5d31f21 100644 --- a/src/faces/iterator.rs +++ b/src/faces/iterator.rs @@ -5,30 +5,61 @@ use crate::Part; use super::face::Face; -pub struct FaceIterator(Part, UniquePtr); +/// 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(); +/// assert_eq!(face_iterator.clone().len(), 6); +/// 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 { - if self.1.More() { - let face = ffi::TopoDS_cast_to_face(self.1.Current()); - let face = Face::from_occt(face); - self.1.pin_mut().Next(); - Some(face) - } else { - None + 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 FaceIterator { + /// Return `true` if this `FaceIterator` has a length of 0. + pub fn is_empty(self) -> bool { + self.len() == 0 + } /// Return the number of `Face`s in this `FaceIterator`. pub fn len(self) -> usize { - let mut self_clone = self.clone(); - let mut len = 0; - while let Some(_) = self_clone.next() { - len += 1; + match self { + Self::NotEmpty(_, _) => { + let self_clone = self.clone(); + let mut len = 0; + for _ in self_clone { + len += 1; + } + len + } + Self::Empty => 0, } - len } } impl Clone for FaceIterator { @@ -36,18 +67,30 @@ impl Clone for FaceIterator { /// /// WARNING: Iterator position will not be cloned. fn clone(&self) -> Self { - self.0.faces() + 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(value.clone(), explorer) + let explorer = ffi::TopExp_Explorer_ctor(inner, ffi::TopAbs_ShapeEnum::TopAbs_FACE); + Self::NotEmpty(value.clone(), explorer) } - None => todo!(), + None => Self::Empty, } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty() { + assert!(Part::empty().faces().is_empty()) + } +} diff --git a/src/sketches/sketch.rs b/src/sketches/sketch.rs index 9355b86..523eb7b 100644 --- a/src/sketches/sketch.rs +++ b/src/sketches/sketch.rs @@ -397,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); From 17958fad514662b55125215a9d7d5c252d93c4c9 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 21:58:18 +0200 Subject: [PATCH 08/18] refactor: extract FaceIterator.len() into ExactSizeIterator --- src/faces/iterator.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/faces/iterator.rs b/src/faces/iterator.rs index 5d31f21..cfc4fd4 100644 --- a/src/faces/iterator.rs +++ b/src/faces/iterator.rs @@ -42,13 +42,8 @@ impl Iterator for FaceIterator { } } } -impl FaceIterator { - /// Return `true` if this `FaceIterator` has a length of 0. - pub fn is_empty(self) -> bool { - self.len() == 0 - } - /// Return the number of `Face`s in this `FaceIterator`. - pub fn len(self) -> usize { +impl ExactSizeIterator for FaceIterator { + fn len(&self) -> usize { match self { Self::NotEmpty(_, _) => { let self_clone = self.clone(); @@ -62,6 +57,12 @@ impl FaceIterator { } } } +impl FaceIterator { + /// Return `true` if this `FaceIterator` has a length of 0. + pub fn is_empty(self) -> bool { + self.len() == 0 + } +} impl Clone for FaceIterator { /// Return a clone of this `FaceIterator`. /// From 5c5f1f088a86a95f03fb325d2cec8d20db6fa4ed Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 22:01:39 +0200 Subject: [PATCH 09/18] refactor: privatise bad clone function --- src/faces/iterator.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/faces/iterator.rs b/src/faces/iterator.rs index cfc4fd4..36a2fbc 100644 --- a/src/faces/iterator.rs +++ b/src/faces/iterator.rs @@ -11,7 +11,6 @@ use super::face::Face; /// use anvil::{Cube, Face, FaceIterator, IntoLength}; /// /// let face_iterator: FaceIterator = Cube::from_size(1.m()).faces(); -/// assert_eq!(face_iterator.clone().len(), 6); /// for face in face_iterator { /// // ... /// } @@ -46,9 +45,8 @@ impl ExactSizeIterator for FaceIterator { fn len(&self) -> usize { match self { Self::NotEmpty(_, _) => { - let self_clone = self.clone(); let mut len = 0; - for _ in self_clone { + for _ in self.clone_without_position() { len += 1; } len @@ -62,12 +60,7 @@ impl FaceIterator { pub fn is_empty(self) -> bool { self.len() == 0 } -} -impl Clone for FaceIterator { - /// Return a clone of this `FaceIterator`. - /// - /// WARNING: Iterator position will not be cloned. - fn clone(&self) -> Self { + fn clone_without_position(&self) -> Self { match self { Self::NotEmpty(part, _) => part.faces(), Self::Empty => Self::Empty, From e21cad8aecfb7a284e201fa8db9ef0fd0f816156 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 13 Jun 2025 22:12:21 +0200 Subject: [PATCH 10/18] fix: transformation of normals --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/meshes/textured_mesh.rs | 12 +++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) 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/meshes/textured_mesh.rs b/src/meshes/textured_mesh.rs index 4a21c3b..7ce3512 100644 --- a/src/meshes/textured_mesh.rs +++ b/src/meshes/textured_mesh.rs @@ -23,7 +23,6 @@ impl TryFrom<(Face, Length)> for TexturedMesh { value.1.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()); @@ -51,7 +50,8 @@ impl TryFrom<(Face, Length)> for TexturedMesh { let uv = ffi::Poly_Triangulation_UV(triangulation, node_index); uvs.push([uv.X(), uv.Y()]); - let normal = ffi::Poly_Triangulation_Normal(triangulation, node_index); + 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 { @@ -89,7 +89,9 @@ impl TryFrom<(Face, Length)> for TexturedMesh { let mut node_ids = [triangle.Value(1), triangle.Value(2), triangle.Value(3)] .map(|id| id as usize - 1); - node_ids.sort(); // depending on device, nodes may be sorted differently - sorting them makes the order deterministic + if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { + node_ids.swap(1, 2); + } indices.push(node_ids); } @@ -128,7 +130,7 @@ mod tests { point!(1.m(), 0.m(), 0.m()), point!(0.m(), 1.m(), 0.m()) ], - indices: vec![[0, 1, 2]], + indices: vec![[1, 2, 0]], normals: vec![dir!(0, 0, 1), dir!(0, 0, 1), dir!(0, 0, 1)], uvs: vec![[0., 0.], [1., 0.], [0., 1.]] }) @@ -150,7 +152,7 @@ mod tests { point!(1.m(), 1.m(), 0.m()), point!(0.m(), 1.m(), 0.m()), ], - indices: vec![[0, 1, 2], [0, 2, 3]], + indices: vec![[2, 0, 1], [2, 3, 0]], 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.]] }) From 983e9acbdb7dd06b449425fee7dbde026b3e64fe Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Sun, 15 Jun 2025 12:49:04 +0200 Subject: [PATCH 11/18] refactor: rename TexturedMesh to RenderMesh --- src/meshes/mod.rs | 2 +- src/meshes/{textured_mesh.rs => render_mesh.rs} | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) rename src/meshes/{textured_mesh.rs => render_mesh.rs} (94%) diff --git a/src/meshes/mod.rs b/src/meshes/mod.rs index deb8c3e..11c0771 100644 --- a/src/meshes/mod.rs +++ b/src/meshes/mod.rs @@ -1 +1 @@ -mod textured_mesh; +mod render_mesh; diff --git a/src/meshes/textured_mesh.rs b/src/meshes/render_mesh.rs similarity index 94% rename from src/meshes/textured_mesh.rs rename to src/meshes/render_mesh.rs index 7ce3512..90cc81c 100644 --- a/src/meshes/textured_mesh.rs +++ b/src/meshes/render_mesh.rs @@ -3,19 +3,19 @@ use opencascade_sys::ffi; use crate::{Dir, Error, Face, IntoLength, Length, Point}; #[derive(Clone, Debug, PartialEq)] -pub struct TexturedMesh { +pub struct RenderMesh { points: Vec>, indices: Vec<[usize; 3]>, normals: Vec>, uvs: Vec<[f64; 2]>, } -impl TryFrom for TexturedMesh { +impl TryFrom for RenderMesh { type Error = Error; fn try_from(value: Face) -> Result { Self::try_from((value, 0.1.mm())) } } -impl TryFrom<(Face, Length)> for TexturedMesh { +impl TryFrom<(Face, Length)> for RenderMesh { type Error = Error; fn try_from(value: (Face, Length)) -> Result { let mesh = ffi::BRepMesh_IncrementalMesh_ctor( @@ -95,7 +95,7 @@ impl TryFrom<(Face, Length)> for TexturedMesh { indices.push(node_ids); } - Ok(TexturedMesh { + Ok(RenderMesh { points, indices, normals, @@ -123,8 +123,8 @@ mod tests { .unwrap(); assert_eq!( - TexturedMesh::try_from(face), - Ok(TexturedMesh { + RenderMesh::try_from(face), + Ok(RenderMesh { points: vec![ point!(0, 0, 0), point!(1.m(), 0.m(), 0.m()), @@ -144,8 +144,8 @@ mod tests { .unwrap(); assert_eq!( - TexturedMesh::try_from(face), - Ok(TexturedMesh { + RenderMesh::try_from(face), + Ok(RenderMesh { points: vec![ point!(0, 0, 0), point!(1.m(), 0.m(), 0.m()), From d77af3bf2f92dd116376cfa74d6f20fe158da36f Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Sun, 15 Jun 2025 13:14:33 +0200 Subject: [PATCH 12/18] feat: Dir.approx_eq() --- src/core/dir.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/core/dir.rs b/src/core/dir.rs index 1398c22..1a86a03 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.001% 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.0001 { + return false; + } + } + true + } } impl Dir<2> { From d82c25f7659652e4a86bd592370e3f24aba5cb80 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Sun, 15 Jun 2025 13:36:59 +0200 Subject: [PATCH 13/18] test: test rotated mesh normals --- src/core/dir.rs | 4 ++-- src/meshes/render_mesh.rs | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/core/dir.rs b/src/core/dir.rs index 1a86a03..6f608d4 100644 --- a/src/core/dir.rs +++ b/src/core/dir.rs @@ -64,7 +64,7 @@ impl Dir { self.0.into_iter().zip(other.0).map(|(a, b)| a * b).sum() } - /// Return true if this `Dir` has less than a 0.001% difference to another. + /// Return true if this `Dir` has less than a 0.000001% difference to another. /// /// ```rust /// use anvil::dir; @@ -74,7 +74,7 @@ impl Dir { /// ``` pub fn approx_eq(&self, other: Dir) -> bool { for (s, o) in self.0.iter().zip(other.0) { - if (s / o - 1.).abs() > 0.0001 { + if (s / o - 1.).abs() > 0.0000001 { return false; } } diff --git a/src/meshes/render_mesh.rs b/src/meshes/render_mesh.rs index 90cc81c..61fc5d4 100644 --- a/src/meshes/render_mesh.rs +++ b/src/meshes/render_mesh.rs @@ -109,7 +109,7 @@ impl TryFrom<(Face, Length)> for RenderMesh { #[cfg(test)] mod tests { - use crate::{IntoLength, Path, Plane, Rectangle, dir, point}; + use crate::{Axis, Cube, IntoAngle, IntoLength, Path, Plane, Rectangle, dir, point}; use super::*; @@ -158,4 +158,17 @@ mod tests { }) ) } + + #[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))) + } + } } From 14906c85fbb061de5293857bd40c725a92f8dcd3 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Sun, 15 Jun 2025 13:48:59 +0200 Subject: [PATCH 14/18] feat: RenderMesh.sorted() --- src/meshes/render_mesh.rs | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/meshes/render_mesh.rs b/src/meshes/render_mesh.rs index 61fc5d4..20bb2af 100644 --- a/src/meshes/render_mesh.rs +++ b/src/meshes/render_mesh.rs @@ -9,6 +9,29 @@ pub struct RenderMesh { 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(), + } + } +} + impl TryFrom for RenderMesh { type Error = Error; fn try_from(value: Face) -> Result { @@ -123,17 +146,17 @@ mod tests { .unwrap(); assert_eq!( - RenderMesh::try_from(face), - Ok(RenderMesh { + 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![[1, 2, 0]], + 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.]] - }) + } ) } @@ -144,18 +167,18 @@ mod tests { .unwrap(); assert_eq!( - RenderMesh::try_from(face), - Ok(RenderMesh { + 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![[2, 0, 1], [2, 3, 0]], + 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.]] - }) + } ) } From 2e1ace4beda6cf286020743f3293dfc26d5223d7 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Sun, 15 Jun 2025 13:58:06 +0200 Subject: [PATCH 15/18] docs: document and publish RenderedMesh --- src/lib.rs | 1 + src/meshes/mod.rs | 2 ++ src/meshes/render_mesh.rs | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 82f1758..92fc083 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub use core::{ }; 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 index 11c0771..56cb275 100644 --- a/src/meshes/mod.rs +++ b/src/meshes/mod.rs @@ -1 +1,3 @@ mod render_mesh; + +pub use render_mesh::RenderMesh; diff --git a/src/meshes/render_mesh.rs b/src/meshes/render_mesh.rs index 20bb2af..3ff81c3 100644 --- a/src/meshes/render_mesh.rs +++ b/src/meshes/render_mesh.rs @@ -2,6 +2,7 @@ use opencascade_sys::ffi; use crate::{Dir, Error, Face, IntoLength, Length, Point}; +/// A triangular mesh of one or more `Face`s optimized for 3D rendering. #[derive(Clone, Debug, PartialEq)] pub struct RenderMesh { points: Vec>, @@ -30,6 +31,23 @@ impl RenderMesh { 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 + } } impl TryFrom for RenderMesh { From b2e20ed95725ae49958f1f24d2d110bd497dc1f5 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Sun, 15 Jun 2025 14:22:18 +0200 Subject: [PATCH 16/18] feat: make Length methods const --- src/core/length.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/core/length.rs b/src/core/length.rs index 48961a4..fb6ee05 100644 --- a/src/core/length.rs +++ b/src/core/length.rs @@ -44,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. @@ -56,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. @@ -72,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. @@ -88,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. @@ -104,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. @@ -120,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. @@ -139,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. @@ -155,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. } @@ -171,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(), } @@ -187,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. @@ -201,7 +201,7 @@ 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())) } } From 7132be7e379635367469bbc7c6a40566117b0f18 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Sun, 15 Jun 2025 16:11:00 +0200 Subject: [PATCH 17/18] feat: transforming Part into a RenderMesh --- src/meshes/render_mesh.rs | 160 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 154 insertions(+), 6 deletions(-) diff --git a/src/meshes/render_mesh.rs b/src/meshes/render_mesh.rs index 3ff81c3..097222b 100644 --- a/src/meshes/render_mesh.rs +++ b/src/meshes/render_mesh.rs @@ -1,6 +1,8 @@ use opencascade_sys::ffi; -use crate::{Dir, Error, Face, IntoLength, Length, Point}; +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)] @@ -48,20 +50,42 @@ impl RenderMesh { pub fn uvs(&self) -> &Vec<[f64; 2]> { &self.uvs } + + 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(value: Face) -> Result { - Self::try_from((value, 0.1.mm())) + fn try_from(face: Face) -> Result { + (face, DEFAULT_TOLERANCE).try_into() } } impl TryFrom<(Face, Length)> for RenderMesh { type Error = Error; - fn try_from(value: (Face, Length)) -> Result { + fn try_from((face, tolerance): (Face, Length)) -> Result { let mesh = ffi::BRepMesh_IncrementalMesh_ctor( - ffi::cast_face_to_shape(value.0.0.as_ref().unwrap()), - value.1.m(), + 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(); @@ -147,6 +171,30 @@ impl TryFrom<(Face, Length)> for RenderMesh { } } } +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 { @@ -212,4 +260,104 @@ mod tests { 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), + ] + ) + } } From 3e33b74f31166e108ee134f9c290f94745b16c9d Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Wed, 18 Jun 2025 13:03:23 +0200 Subject: [PATCH 18/18] feat: RenderMesh.center() and .area() --- src/meshes/render_mesh.rs | 77 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/meshes/render_mesh.rs b/src/meshes/render_mesh.rs index 097222b..f98bbed 100644 --- a/src/meshes/render_mesh.rs +++ b/src/meshes/render_mesh.rs @@ -51,6 +51,67 @@ impl RenderMesh { &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![], @@ -198,7 +259,9 @@ fn merge(meshes: Vec) -> RenderMesh { #[cfg(test)] mod tests { - use crate::{Axis, Cube, IntoAngle, IntoLength, Path, Plane, Rectangle, dir, point}; + use core::f64; + + use crate::{Axis, Circle, Cube, IntoAngle, IntoLength, Path, Plane, Rectangle, dir, point}; use super::*; @@ -248,6 +311,18 @@ mod tests { ) } + #[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())