#![allow(dead_code)]
#[derive(Clone, Debug)]
pub enum ScadPrim {
Cube {
size: [f32; 3],
},
Sphere {
r: f32,
},
Cylinder {
h: f32,
r: f32,
},
Polyhedron {
points: Vec<[f32; 3]>,
faces: Vec<Vec<u32>>,
},
}
#[derive(Clone, Debug)]
pub enum ScadNode {
Primitive(ScadPrim),
Union(Vec<ScadNode>),
Difference(Vec<ScadNode>),
Intersection(Vec<ScadNode>),
Translate { v: [f32; 3], child: Box<ScadNode> },
}
#[derive(Clone, Debug, Default)]
pub struct OpenScadExport {
pub nodes: Vec<ScadNode>,
}
pub fn new_openscad_export() -> OpenScadExport {
OpenScadExport::default()
}
pub fn openscad_push_node(doc: &mut OpenScadExport, node: ScadNode) {
doc.nodes.push(node);
}
pub fn openscad_node_count(doc: &OpenScadExport) -> usize {
doc.nodes.len()
}
fn render_prim(p: &ScadPrim) -> String {
match p {
ScadPrim::Cube { size } => {
format!("cube([{:.6}, {:.6}, {:.6}]);", size[0], size[1], size[2])
}
ScadPrim::Sphere { r } => format!("sphere(r={:.6});", r),
ScadPrim::Cylinder { h, r } => format!("cylinder(h={:.6}, r={:.6});", h, r),
ScadPrim::Polyhedron { points, faces } => {
let pts: Vec<String> = points
.iter()
.map(|p| format!("[{:.6},{:.6},{:.6}]", p[0], p[1], p[2]))
.collect();
let fcs: Vec<String> = faces
.iter()
.map(|f| {
format!(
"[{}]",
f.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",")
)
})
.collect();
format!(
"polyhedron(points=[{}], faces=[{}]);",
pts.join(","),
fcs.join(",")
)
}
}
}
fn render_node(node: &ScadNode, indent: usize) -> String {
let pad = " ".repeat(indent);
match node {
ScadNode::Primitive(p) => format!("{}{}", pad, render_prim(p)),
ScadNode::Union(children) => {
let body: String = children
.iter()
.map(|c| render_node(c, indent + 2))
.collect::<Vec<_>>()
.join("\n");
format!("{}union() {{\n{}\n{}}}", pad, body, pad)
}
ScadNode::Difference(children) => {
let body: String = children
.iter()
.map(|c| render_node(c, indent + 2))
.collect::<Vec<_>>()
.join("\n");
format!("{}difference() {{\n{}\n{}}}", pad, body, pad)
}
ScadNode::Intersection(children) => {
let body: String = children
.iter()
.map(|c| render_node(c, indent + 2))
.collect::<Vec<_>>()
.join("\n");
format!("{}intersection() {{\n{}\n{}}}", pad, body, pad)
}
ScadNode::Translate { v, child } => {
let child_str = render_node(child, indent + 2);
format!(
"{}translate([{:.6},{:.6},{:.6}]) {{\n{}\n{}}}",
pad, v[0], v[1], v[2], child_str, pad
)
}
}
}
pub fn render_openscad(doc: &OpenScadExport) -> String {
let mut out = String::from("// Generated by oxihuman\n");
for node in &doc.nodes {
out.push_str(&render_node(node, 0));
out.push('\n');
}
out
}
pub fn export_mesh_as_openscad(positions: &[[f32; 3]], indices: &[u32]) -> OpenScadExport {
let mut doc = new_openscad_export();
let points: Vec<[f32; 3]> = positions.to_vec();
let faces: Vec<Vec<u32>> = indices.chunks(3).map(|t| t.to_vec()).collect();
openscad_push_node(
&mut doc,
ScadNode::Primitive(ScadPrim::Polyhedron { points, faces }),
);
doc
}
pub fn validate_openscad(doc: &OpenScadExport) -> bool {
!doc.nodes.is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_doc_empty() {
let d = new_openscad_export();
assert_eq!(openscad_node_count(&d), 0);
}
#[test]
fn push_node_increments_count() {
let mut d = new_openscad_export();
openscad_push_node(&mut d, ScadNode::Primitive(ScadPrim::Sphere { r: 1.0 }));
assert_eq!(openscad_node_count(&d), 1);
}
#[test]
fn render_sphere() {
let mut d = new_openscad_export();
openscad_push_node(&mut d, ScadNode::Primitive(ScadPrim::Sphere { r: 2.5 }));
let s = render_openscad(&d);
assert!(s.contains("sphere"), "got: {s}");
}
#[test]
fn render_cube() {
let mut d = new_openscad_export();
openscad_push_node(
&mut d,
ScadNode::Primitive(ScadPrim::Cube {
size: [1.0, 2.0, 3.0],
}),
);
let s = render_openscad(&d);
assert!(s.contains("cube"));
}
#[test]
fn export_mesh_adds_polyhedron() {
let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let idx = vec![0u32, 1, 2];
let d = export_mesh_as_openscad(&pos, &idx);
assert_eq!(openscad_node_count(&d), 1);
let s = render_openscad(&d);
assert!(s.contains("polyhedron"));
}
#[test]
fn validate_nonempty_doc() {
let mut d = new_openscad_export();
openscad_push_node(&mut d, ScadNode::Primitive(ScadPrim::Sphere { r: 1.0 }));
assert!(validate_openscad(&d));
}
#[test]
fn validate_empty_doc_fails() {
let d = new_openscad_export();
assert!(!validate_openscad(&d));
}
#[test]
fn render_translate_contains_translate() {
let mut d = new_openscad_export();
let child = ScadNode::Primitive(ScadPrim::Sphere { r: 1.0 });
openscad_push_node(
&mut d,
ScadNode::Translate {
v: [1.0, 0.0, 0.0],
child: Box::new(child),
},
);
let s = render_openscad(&d);
assert!(s.contains("translate"));
}
#[test]
fn render_union_contains_union() {
let mut d = new_openscad_export();
let c1 = ScadNode::Primitive(ScadPrim::Sphere { r: 1.0 });
let c2 = ScadNode::Primitive(ScadPrim::Cube {
size: [1.0, 1.0, 1.0],
});
openscad_push_node(&mut d, ScadNode::Union(vec![c1, c2]));
let s = render_openscad(&d);
assert!(s.contains("union"));
}
}