#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum ThreeMfUnits {
Millimeter,
Centimeter,
Meter,
Inch,
Foot,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ThreeMfConfig {
pub units: ThreeMfUnits,
pub author: String,
pub include_materials: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ThreeMfMesh {
pub vertices: Vec<[f32; 3]>,
pub triangles: Vec<[u32; 3]>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ThreeMfModel {
pub meshes: Vec<ThreeMfMesh>,
pub name: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ThreeMfExportResult {
pub xml_string: String,
pub triangle_count: usize,
pub vertex_count: usize,
}
#[allow(dead_code)]
pub fn default_threemf_config() -> ThreeMfConfig {
ThreeMfConfig {
units: ThreeMfUnits::Millimeter,
author: String::from("OxiHuman"),
include_materials: false,
}
}
#[allow(dead_code)]
pub fn new_threemf_model(name: &str) -> ThreeMfModel {
ThreeMfModel {
meshes: Vec::new(),
name: name.to_string(),
}
}
#[allow(dead_code)]
pub fn add_mesh_to_model(model: &mut ThreeMfModel, mesh: ThreeMfMesh) {
model.meshes.push(mesh);
}
#[allow(dead_code)]
pub fn new_threemf_mesh(verts: Vec<[f32; 3]>, tris: Vec<[u32; 3]>) -> ThreeMfMesh {
ThreeMfMesh {
vertices: verts,
triangles: tris,
}
}
#[allow(dead_code)]
pub fn model_to_3mf(model: &ThreeMfModel, cfg: &ThreeMfConfig) -> ThreeMfExportResult {
let mut xml = String::new();
xml.push_str(&threemf_header(cfg));
for mesh in &model.meshes {
xml.push_str(&threemf_mesh_xml(mesh));
}
xml.push_str(&threemf_footer());
let tri_count = threemf_triangle_count(model);
let vtx_count = threemf_vertex_count(model);
ThreeMfExportResult {
xml_string: xml,
triangle_count: tri_count,
vertex_count: vtx_count,
}
}
#[allow(dead_code)]
pub fn threemf_header(cfg: &ThreeMfConfig) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?><model unit="{}" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"><metadata name="Designer">{}</metadata><resources>"#,
units_name_3mf(cfg),
cfg.author
)
}
#[allow(dead_code)]
pub fn threemf_mesh_xml(mesh: &ThreeMfMesh) -> String {
let mut s = String::from("<object id=\"1\" type=\"model\"><mesh><vertices>");
for v in &mesh.vertices {
s.push_str(&format!(
r#"<vertex x="{:.6}" y="{:.6}" z="{:.6}"/>"#,
v[0], v[1], v[2]
));
}
s.push_str("</vertices><triangles>");
for t in &mesh.triangles {
s.push_str(&format!(
r#"<triangle v1="{}" v2="{}" v3="{}"/>"#,
t[0], t[1], t[2]
));
}
s.push_str("</triangles></mesh></object>");
s
}
#[allow(dead_code)]
pub fn threemf_footer() -> String {
String::from("</resources><build/></model>")
}
#[allow(dead_code)]
pub fn threemf_vertex_count(model: &ThreeMfModel) -> usize {
model.meshes.iter().map(|m| m.vertices.len()).sum()
}
#[allow(dead_code)]
pub fn threemf_triangle_count(model: &ThreeMfModel) -> usize {
model.meshes.iter().map(|m| m.triangles.len()).sum()
}
#[allow(dead_code)]
pub fn units_name_3mf(cfg: &ThreeMfConfig) -> &'static str {
match cfg.units {
ThreeMfUnits::Millimeter => "millimeter",
ThreeMfUnits::Centimeter => "centimeter",
ThreeMfUnits::Meter => "meter",
ThreeMfUnits::Inch => "inch",
ThreeMfUnits::Foot => "foot",
}
}
#[allow(dead_code)]
pub fn validate_3mf(result: &ThreeMfExportResult) -> bool {
!result.xml_string.is_empty()
&& result.xml_string.contains("model")
&& result.triangle_count > 0
&& result.vertex_count > 0
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_mesh() -> ThreeMfMesh {
new_threemf_mesh(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
vec![[0, 1, 2]],
)
}
#[test]
fn default_config_is_millimeter() {
let cfg = default_threemf_config();
assert_eq!(cfg.units, ThreeMfUnits::Millimeter);
assert!(!cfg.include_materials);
}
#[test]
fn new_model_is_empty() {
let model = new_threemf_model("test");
assert_eq!(model.name, "test");
assert!(model.meshes.is_empty());
}
#[test]
fn add_mesh_increases_count() {
let mut model = new_threemf_model("m");
add_mesh_to_model(&mut model, sample_mesh());
assert_eq!(model.meshes.len(), 1);
}
#[test]
fn vertex_and_triangle_counts() {
let mut model = new_threemf_model("m");
add_mesh_to_model(&mut model, sample_mesh());
assert_eq!(threemf_vertex_count(&model), 3);
assert_eq!(threemf_triangle_count(&model), 1);
}
#[test]
fn units_name_all_variants() {
let variants = [
(ThreeMfUnits::Millimeter, "millimeter"),
(ThreeMfUnits::Centimeter, "centimeter"),
(ThreeMfUnits::Meter, "meter"),
(ThreeMfUnits::Inch, "inch"),
(ThreeMfUnits::Foot, "foot"),
];
for (unit, expected) in &variants {
let cfg = ThreeMfConfig {
units: unit.clone(),
author: String::new(),
include_materials: false,
};
assert_eq!(units_name_3mf(&cfg), *expected);
}
}
#[test]
fn model_to_3mf_produces_valid_xml() {
let mut model = new_threemf_model("body");
add_mesh_to_model(&mut model, sample_mesh());
let cfg = default_threemf_config();
let result = model_to_3mf(&model, &cfg);
assert!(result.xml_string.contains("<?xml"));
assert!(result.xml_string.contains("millimeter"));
assert!(result.xml_string.contains("vertex"));
assert!(result.xml_string.contains("triangle"));
}
#[test]
fn validate_3mf_passes_for_valid_result() {
let mut model = new_threemf_model("v");
add_mesh_to_model(&mut model, sample_mesh());
let cfg = default_threemf_config();
let result = model_to_3mf(&model, &cfg);
assert!(validate_3mf(&result));
}
#[test]
fn validate_3mf_fails_for_empty_model() {
let result = ThreeMfExportResult {
xml_string: String::new(),
triangle_count: 0,
vertex_count: 0,
};
assert!(!validate_3mf(&result));
}
}