#![warn(missing_docs)]
use manifold_rs::{Manifold, Mesh};
use nalgebra::Vector3;
use std::f64::consts::PI;
use thiserror::Error;
pub mod export;
pub mod step;
pub use export::{Material, Materials};
#[derive(Error, Debug)]
pub enum CadError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Empty geometry")]
EmptyGeometry,
}
pub struct Part {
pub name: String,
manifold: Manifold,
}
impl Part {
pub fn new(name: impl Into<String>, manifold: Manifold) -> Self {
Self {
name: name.into(),
manifold,
}
}
pub fn empty(name: impl Into<String>) -> Self {
Self::new(name, Manifold::empty())
}
pub fn cube(name: impl Into<String>, x: f64, y: f64, z: f64) -> Self {
let manifold = Manifold::cube(x, y, z);
Self::new(name, manifold)
}
pub fn cylinder(name: impl Into<String>, radius: f64, height: f64, segments: u32) -> Self {
let manifold = Manifold::cylinder(radius, radius, height, segments);
Self::new(name, manifold)
}
pub fn cone(
name: impl Into<String>,
radius_bottom: f64,
radius_top: f64,
height: f64,
segments: u32,
) -> Self {
let manifold = Manifold::cylinder(radius_bottom, radius_top, height, segments);
Self::new(name, manifold)
}
pub fn sphere(name: impl Into<String>, radius: f64, segments: u32) -> Self {
let manifold = Manifold::sphere(radius, segments);
Self::new(name, manifold)
}
pub fn difference(&self, other: &Part) -> Self {
Self::new(
format!("{}-diff", self.name),
self.manifold.difference(&other.manifold),
)
}
pub fn union(&self, other: &Part) -> Self {
Self::new(
format!("{}-union", self.name),
self.manifold.union(&other.manifold),
)
}
pub fn intersection(&self, other: &Part) -> Self {
Self::new(
format!("{}-intersect", self.name),
self.manifold.intersection(&other.manifold),
)
}
pub fn translate(&self, x: f64, y: f64, z: f64) -> Self {
Self::new(self.name.clone(), self.manifold.translate(x, y, z))
}
pub fn translate_vec(&self, v: Vector3<f64>) -> Self {
self.translate(v.x, v.y, v.z)
}
pub fn rotate(&self, x_deg: f64, y_deg: f64, z_deg: f64) -> Self {
Self::new(self.name.clone(), self.manifold.rotate(x_deg, y_deg, z_deg))
}
pub fn scale(&self, x: f64, y: f64, z: f64) -> Self {
Self::new(self.name.clone(), self.manifold.scale(x, y, z))
}
pub fn scale_uniform(&self, s: f64) -> Self {
self.scale(s, s, s)
}
pub fn is_empty(&self) -> bool {
self.manifold.is_empty()
}
pub fn to_mesh(&self) -> Mesh {
self.manifold.to_mesh()
}
pub fn to_stl(&self) -> Result<Vec<u8>, CadError> {
export::stl::to_stl_bytes(self)
}
pub fn write_stl(&self, path: impl AsRef<std::path::Path>) -> Result<(), CadError> {
export::stl::export_stl(self, path)
}
}
pub fn centered_cube(name: impl Into<String>, x: f64, y: f64, z: f64) -> Part {
Part::cube(name, x, y, z).translate(-x / 2.0, -y / 2.0, -z / 2.0)
}
pub fn centered_cylinder(name: impl Into<String>, radius: f64, height: f64, segments: u32) -> Part {
Part::cylinder(name, radius, height, segments).translate(0.0, 0.0, -height / 2.0)
}
pub fn counterbore_hole(
through_diameter: f64,
counterbore_diameter: f64,
counterbore_depth: f64,
total_depth: f64,
segments: u32,
) -> Part {
let through = Part::cylinder("through", through_diameter / 2.0, total_depth, segments);
let counterbore = Part::cylinder(
"counterbore",
counterbore_diameter / 2.0,
counterbore_depth,
segments,
)
.translate(0.0, 0.0, total_depth - counterbore_depth);
through.union(&counterbore)
}
pub fn bolt_pattern(
num_holes: usize,
bolt_circle_diameter: f64,
hole_diameter: f64,
depth: f64,
segments: u32,
) -> Part {
let radius = bolt_circle_diameter / 2.0;
let mut result = Part::empty("bolt_pattern");
for i in 0..num_holes {
let angle = 2.0 * PI * (i as f64) / (num_holes as f64);
let x = radius * angle.cos();
let y = radius * angle.sin();
let hole =
Part::cylinder("hole", hole_diameter / 2.0, depth, segments).translate(x, y, 0.0);
result = result.union(&hole);
}
result
}
impl std::ops::Add for &Part {
type Output = Part;
fn add(self, rhs: &Part) -> Part {
self.union(rhs)
}
}
impl std::ops::Add for Part {
type Output = Part;
fn add(self, rhs: Part) -> Part {
self.union(&rhs)
}
}
impl std::ops::Sub for &Part {
type Output = Part;
fn sub(self, rhs: &Part) -> Part {
self.difference(rhs)
}
}
impl std::ops::Sub for Part {
type Output = Part;
fn sub(self, rhs: Part) -> Part {
self.difference(&rhs)
}
}
impl std::ops::BitAnd for &Part {
type Output = Part;
fn bitand(self, rhs: &Part) -> Part {
self.intersection(rhs)
}
}
impl std::ops::BitAnd for Part {
type Output = Part;
fn bitand(self, rhs: Part) -> Part {
self.intersection(&rhs)
}
}
impl Part {
pub fn volume(&self) -> f64 {
let mesh = self.manifold.to_mesh();
let verts = mesh.vertices();
let indices = mesh.indices();
let mut vol = 0.0;
for tri in indices.chunks(3) {
let (i0, i1, i2) = (
tri[0] as usize * 3,
tri[1] as usize * 3,
tri[2] as usize * 3,
);
let v0 = [verts[i0] as f64, verts[i0 + 1] as f64, verts[i0 + 2] as f64];
let v1 = [verts[i1] as f64, verts[i1 + 1] as f64, verts[i1 + 2] as f64];
let v2 = [verts[i2] as f64, verts[i2 + 1] as f64, verts[i2 + 2] as f64];
vol += v0[0] * (v1[1] * v2[2] - v2[1] * v1[2])
- v1[0] * (v0[1] * v2[2] - v2[1] * v0[2])
+ v2[0] * (v0[1] * v1[2] - v1[1] * v0[2]);
}
(vol / 6.0).abs()
}
pub fn surface_area(&self) -> f64 {
let mesh = self.manifold.to_mesh();
let verts = mesh.vertices();
let indices = mesh.indices();
let mut area = 0.0;
for tri in indices.chunks(3) {
let (i0, i1, i2) = (
tri[0] as usize * 3,
tri[1] as usize * 3,
tri[2] as usize * 3,
);
let v0 = Vector3::new(verts[i0] as f64, verts[i0 + 1] as f64, verts[i0 + 2] as f64);
let v1 = Vector3::new(verts[i1] as f64, verts[i1 + 1] as f64, verts[i1 + 2] as f64);
let v2 = Vector3::new(verts[i2] as f64, verts[i2 + 1] as f64, verts[i2 + 2] as f64);
area += (v1 - v0).cross(&(v2 - v0)).norm() / 2.0;
}
area
}
pub fn bounding_box(&self) -> ([f64; 3], [f64; 3]) {
let mesh = self.manifold.to_mesh();
let verts = mesh.vertices();
let mut min = [f64::MAX; 3];
let mut max = [f64::MIN; 3];
for chunk in verts.chunks(3) {
for i in 0..3 {
let v = chunk[i] as f64;
if v < min[i] {
min[i] = v;
}
if v > max[i] {
max[i] = v;
}
}
}
(min, max)
}
pub fn center_of_mass(&self) -> [f64; 3] {
let mesh = self.manifold.to_mesh();
let verts = mesh.vertices();
let indices = mesh.indices();
let mut cx = 0.0;
let mut cy = 0.0;
let mut cz = 0.0;
let mut total_vol = 0.0;
for tri in indices.chunks(3) {
let (i0, i1, i2) = (
tri[0] as usize * 3,
tri[1] as usize * 3,
tri[2] as usize * 3,
);
let v0 = [verts[i0] as f64, verts[i0 + 1] as f64, verts[i0 + 2] as f64];
let v1 = [verts[i1] as f64, verts[i1 + 1] as f64, verts[i1 + 2] as f64];
let v2 = [verts[i2] as f64, verts[i2 + 1] as f64, verts[i2 + 2] as f64];
let vol = v0[0] * (v1[1] * v2[2] - v2[1] * v1[2])
- v1[0] * (v0[1] * v2[2] - v2[1] * v0[2])
+ v2[0] * (v0[1] * v1[2] - v1[1] * v0[2]);
total_vol += vol;
cx += vol * (v0[0] + v1[0] + v2[0]);
cy += vol * (v0[1] + v1[1] + v2[1]);
cz += vol * (v0[2] + v1[2] + v2[2]);
}
if total_vol.abs() < 1e-15 {
return [0.0; 3];
}
let s = 1.0 / (4.0 * total_vol);
[cx * s, cy * s, cz * s]
}
pub fn num_triangles(&self) -> usize {
let mesh = self.manifold.to_mesh();
mesh.indices().len() / 3
}
}
impl Part {
pub fn mirror_x(&self) -> Part {
self.scale(-1.0, 1.0, 1.0)
}
pub fn mirror_y(&self) -> Part {
self.scale(1.0, -1.0, 1.0)
}
pub fn mirror_z(&self) -> Part {
self.scale(1.0, 1.0, -1.0)
}
pub fn linear_pattern(&self, dx: f64, dy: f64, dz: f64, count: usize) -> Part {
let mut result = self.translate(0.0, 0.0, 0.0); for i in 1..count {
let n = i as f64;
result = result.union(&self.translate(dx * n, dy * n, dz * n));
}
result
}
pub fn circular_pattern(&self, radius: f64, count: usize) -> Part {
let mut result = Part::empty("circular_pattern");
for i in 0..count {
let angle = 360.0 * (i as f64) / (count as f64);
let copy = self.translate(radius, 0.0, 0.0).rotate(0.0, 0.0, angle);
result = result.union(©);
}
result
}
}
pub struct SceneNode {
pub part: Part,
pub material_key: String,
}
impl SceneNode {
pub fn new(part: Part, material_key: impl Into<String>) -> Self {
Self {
part,
material_key: material_key.into(),
}
}
}
pub struct Scene {
pub name: String,
pub nodes: Vec<SceneNode>,
}
impl Scene {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
nodes: Vec::new(),
}
}
pub fn add(&mut self, part: Part, material_key: impl Into<String>) {
self.nodes.push(SceneNode::new(part, material_key));
}
pub fn add_default(&mut self, part: Part) {
self.nodes.push(SceneNode::new(part, "default"));
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cube_creation() {
let cube = Part::cube("test", 10.0, 10.0, 10.0);
assert!(!cube.is_empty());
}
#[test]
fn test_cylinder_creation() {
let cyl = Part::cylinder("test", 5.0, 10.0, 32);
assert!(!cyl.is_empty());
}
#[test]
fn test_difference() {
let cube = Part::cube("cube", 10.0, 10.0, 10.0);
let hole = Part::cylinder("hole", 3.0, 15.0, 32).translate(5.0, 5.0, -1.0);
let result = cube.difference(&hole);
assert!(!result.is_empty());
}
#[test]
fn test_operator_overloads() {
let a = Part::cube("a", 10.0, 10.0, 10.0);
let b = Part::cube("b", 10.0, 10.0, 10.0).translate(5.0, 0.0, 0.0);
let union = Part::cube("a", 10.0, 10.0, 10.0) + Part::cube("b", 10.0, 10.0, 10.0);
assert!(!union.is_empty());
let diff = Part::cube("a", 10.0, 10.0, 10.0)
- Part::cube("b", 5.0, 5.0, 5.0).translate(2.5, 2.5, 2.5);
assert!(!diff.is_empty());
let isect = Part::cube("a", 10.0, 10.0, 10.0)
& Part::cube("b", 10.0, 10.0, 10.0).translate(5.0, 5.0, 5.0);
assert!(!isect.is_empty());
let union_ref = &a + &b;
assert!(!union_ref.is_empty());
let diff_ref = &a - &b;
assert!(!diff_ref.is_empty());
let isect_ref = &a & &b;
assert!(!isect_ref.is_empty());
}
#[test]
fn test_volume() {
let cube = Part::cube("cube", 10.0, 10.0, 10.0);
let vol = cube.volume();
assert!((vol - 1000.0).abs() < 1.0, "expected ~1000, got {vol}");
}
#[test]
fn test_surface_area() {
let cube = Part::cube("cube", 10.0, 10.0, 10.0);
let area = cube.surface_area();
assert!((area - 600.0).abs() < 1.0, "expected ~600, got {area}");
}
#[test]
fn test_bounding_box() {
let cube = Part::cube("cube", 10.0, 20.0, 30.0);
let (min, max) = cube.bounding_box();
assert!((max[0] - min[0] - 10.0).abs() < 0.01);
assert!((max[1] - min[1] - 20.0).abs() < 0.01);
assert!((max[2] - min[2] - 30.0).abs() < 0.01);
}
#[test]
fn test_center_of_mass() {
let cube = Part::cube("cube", 10.0, 10.0, 10.0);
let com = cube.center_of_mass();
assert!((com[0] - 5.0).abs() < 0.1, "cx: {}", com[0]);
assert!((com[1] - 5.0).abs() < 0.1, "cy: {}", com[1]);
assert!((com[2] - 5.0).abs() < 0.1, "cz: {}", com[2]);
}
#[test]
fn test_num_triangles() {
let cube = Part::cube("cube", 10.0, 10.0, 10.0);
assert!(
cube.num_triangles() >= 12,
"cube should have at least 12 triangles"
);
}
#[test]
fn test_mirror() {
let cube = Part::cube("cube", 10.0, 10.0, 10.0).translate(5.0, 0.0, 0.0);
let mirrored = cube.mirror_x();
let (min, _max) = mirrored.bounding_box();
assert!(
min[0] < 0.0,
"mirrored min x should be negative: {}",
min[0]
);
}
#[test]
fn test_linear_pattern() {
let cube = Part::cube("cube", 5.0, 5.0, 5.0);
let pattern = cube.linear_pattern(10.0, 0.0, 0.0, 3);
let (min, max) = pattern.bounding_box();
assert!((max[0] - min[0] - 25.0).abs() < 0.1);
}
#[test]
fn test_circular_pattern() {
let cube = Part::cube("cube", 2.0, 2.0, 2.0);
let pattern = cube.circular_pattern(10.0, 4);
assert!(!pattern.is_empty());
let (min, max) = pattern.bounding_box();
assert!(max[0] > 10.0);
assert!(min[0] < -10.0 + 2.0); }
}