Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/core/length.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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<Length> for Length {
type Output = Length;
Expand Down
7 changes: 5 additions & 2 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions src/faces/face.rs
Original file line number Diff line number Diff line change
@@ -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<ffi::TopoDS_Face>);
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)
}
}
96 changes: 96 additions & 0 deletions src/faces/iterator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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();
/// assert_eq!(face_iterator.clone().len(), 6);
/// for face in face_iterator {
/// // ...
/// }
/// ```
pub enum FaceIterator {
/// A FaceIterator that is not empty.
NotEmpty(Part, UniquePtr<ffi::TopExp_Explorer>),
/// A FaceIterator from an empty shape.
Empty,
}

impl Iterator for FaceIterator {
type Item = Face;

fn next(&mut self) -> Option<Self::Item> {
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 {
match self {
Self::NotEmpty(_, _) => {
let self_clone = self.clone();
let mut len = 0;
for _ in self_clone {
len += 1;
}
len
}
Self::Empty => 0,
}
}
}
impl Clone for FaceIterator {
/// Return a clone of this `FaceIterator`.
///
/// WARNING: Iterator position will not be cloned.
fn clone(&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())
}
}
5 changes: 5 additions & 0 deletions src/faces/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod face;
mod iterator;

pub use face::Face;
pub use iterator::FaceIterator;
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
#![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;

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},
Expand Down
1 change: 1 addition & 0 deletions src/meshes/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod textured_mesh;
159 changes: 159 additions & 0 deletions src/meshes/textured_mesh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use opencascade_sys::ffi;

use crate::{Dir, Error, Face, IntoLength, Length, Point};

#[derive(Clone, Debug, PartialEq)]
pub struct TexturedMesh {
points: Vec<Point<3>>,
indices: Vec<[usize; 3]>,
normals: Vec<Dir<3>>,
uvs: Vec<[f64; 2]>,
}
impl TryFrom<Face> for TexturedMesh {
type Error = Error;
fn try_from(value: Face) -> Result<Self, Self::Error> {
Self::try_from((value, 0.1.mm()))
}
}
impl TryFrom<(Face, Length)> for TexturedMesh {
type Error = Error;
fn try_from(value: (Face, Length)) -> Result<Self, Self::Error> {
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 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 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 - 1);

node_ids.sort(); // depending on device, nodes may be sorted differently - sorting them makes the order deterministic
indices.push(node_ids);
}

Ok(TexturedMesh {
points,
indices,
normals,
uvs,
})
} else {
Err(Error::Triangulation)
}
}
}

#[cfg(test)]
mod tests {
use crate::{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!(
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![[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!(
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![[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.]]
})
)
}
}
14 changes: 13 additions & 1 deletion src/parts/part.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading