use crate::zip_pack::{zip_bytes, ZipEntry};
use oxihuman_mesh::MeshBuffers;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum ThreeMfUnit {
Millimeter,
Centimeter,
Meter,
Inch,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ThreeMfOptions {
pub unit: ThreeMfUnit,
pub object_name: String,
pub scale: f32,
pub author: String,
}
impl Default for ThreeMfOptions {
fn default() -> Self {
ThreeMfOptions {
unit: ThreeMfUnit::Millimeter,
object_name: "OxiHuman".to_string(),
scale: 1000.0,
author: "COOLJAPAN OU (Team KitaSan)".to_string(),
}
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct ThreeMfExportResult {
pub zip_bytes: Vec<u8>,
pub vertex_count: usize,
pub triangle_count: usize,
pub model_xml_size: usize,
}
pub fn export_3mf(mesh: &MeshBuffers, opts: &ThreeMfOptions) -> ThreeMfExportResult {
let model_xml = build_3mf_model_xml(mesh, opts);
let model_xml_size = model_xml.len();
let content_types = build_content_types_xml();
let rels = build_rels_xml();
let entries = vec![
ZipEntry {
filename: "[Content_Types].xml".to_string(),
data: content_types.into_bytes(),
},
ZipEntry {
filename: "_rels/.rels".to_string(),
data: rels.into_bytes(),
},
ZipEntry {
filename: "3D/3dmodel.model".to_string(),
data: model_xml.into_bytes(),
},
];
let zip = zip_bytes(&entries);
ThreeMfExportResult {
zip_bytes: zip,
vertex_count: mesh.positions.len(),
triangle_count: mesh.indices.len() / 3,
model_xml_size,
}
}
pub fn build_3mf_model_xml(mesh: &MeshBuffers, opts: &ThreeMfOptions) -> String {
let unit_str = unit_string(&opts.unit);
let s = opts.scale;
let mut vertices_xml = String::new();
for p in &mesh.positions {
vertices_xml.push_str(&format!(
" <vertex x=\"{:.6}\" y=\"{:.6}\" z=\"{:.6}\"/>\n",
p[0] * s,
p[1] * s,
p[2] * s,
));
}
let mut triangles_xml = String::new();
let idx = &mesh.indices;
let tri_count = idx.len() / 3;
for t in 0..tri_count {
triangles_xml.push_str(&format!(
" <triangle v1=\"{}\" v2=\"{}\" v3=\"{}\"/>\n",
idx[t * 3],
idx[t * 3 + 1],
idx[t * 3 + 2],
));
}
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!-- Author: {} -->\n\
<model unit=\"{}\" xml:lang=\"en-US\" \
xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\">\n\
<resources>\n\
<object id=\"1\" name=\"{}\" type=\"model\">\n\
<mesh>\n\
<vertices>\n\
{}\
</vertices>\n\
<triangles>\n\
{}\
</triangles>\n\
</mesh>\n\
</object>\n\
</resources>\n\
<build>\n\
<item objectid=\"1\"/>\n\
</build>\n\
</model>\n",
opts.author, unit_str, opts.object_name, vertices_xml, triangles_xml,
)
}
pub fn build_content_types_xml() -> String {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\n\
<Default Extension=\"rels\" \
ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n\
<Default Extension=\"model\" \
ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n\
</Types>\n"
.to_string()
}
pub fn build_rels_xml() -> String {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<Relationships \
xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n\
<Relationship Id=\"rel0\" \
Target=\"/3D/3dmodel.model\" \
Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\"/>\n\
</Relationships>\n"
.to_string()
}
pub fn unit_string(u: &ThreeMfUnit) -> &'static str {
match u {
ThreeMfUnit::Millimeter => "millimeter",
ThreeMfUnit::Centimeter => "centimeter",
ThreeMfUnit::Meter => "meter",
ThreeMfUnit::Inch => "inch",
}
}
pub fn validate_3mf_zip(data: &[u8]) -> bool {
if data.len() < 4 {
return false;
}
if data[0..4] != [0x50, 0x4B, 0x03, 0x04] {
return false;
}
let needle = b"3dmodel.model";
data.windows(needle.len()).any(|w| w == needle)
}
pub fn mesh_is_printable(mesh: &MeshBuffers) -> bool {
if mesh.positions.is_empty() {
return false;
}
if !mesh.indices.len().is_multiple_of(3) {
return false;
}
let n = mesh.positions.len() as u32;
mesh.indices.iter().all(|&i| i < n)
}
#[cfg(test)]
mod tests {
use super::*;
use oxihuman_mesh::MeshBuffers;
use oxihuman_morph::engine::MeshBuffers as MB;
fn simple_mesh() -> MeshBuffers {
MeshBuffers::from_morph(MB {
positions: vec![[0.0, 0.0, 0.0], [0.001, 0.0, 0.0], [0.0, 0.001, 0.0]],
normals: vec![[0.0, 0.0, 1.0]; 3],
uvs: vec![[0.0, 0.0]; 3],
indices: vec![0, 1, 2],
has_suit: false,
})
}
fn empty_mesh() -> MeshBuffers {
MeshBuffers::from_morph(MB {
positions: vec![],
normals: vec![],
uvs: vec![],
indices: vec![],
has_suit: false,
})
}
#[test]
fn unit_string_millimeter() {
assert_eq!(unit_string(&ThreeMfUnit::Millimeter), "millimeter");
}
#[test]
fn unit_string_centimeter() {
assert_eq!(unit_string(&ThreeMfUnit::Centimeter), "centimeter");
}
#[test]
fn unit_string_meter() {
assert_eq!(unit_string(&ThreeMfUnit::Meter), "meter");
}
#[test]
fn unit_string_inch() {
assert_eq!(unit_string(&ThreeMfUnit::Inch), "inch");
}
#[test]
fn model_xml_contains_vertices_tag() {
let mesh = simple_mesh();
let xml = build_3mf_model_xml(&mesh, &ThreeMfOptions::default());
assert!(xml.contains("<vertices>"), "XML must contain <vertices>");
}
#[test]
fn model_xml_contains_triangles_tag() {
let mesh = simple_mesh();
let xml = build_3mf_model_xml(&mesh, &ThreeMfOptions::default());
assert!(xml.contains("<triangles>"), "XML must contain <triangles>");
}
#[test]
fn model_xml_vertex_count_matches() {
let mesh = simple_mesh();
let xml = build_3mf_model_xml(&mesh, &ThreeMfOptions::default());
let count = xml.matches("<vertex ").count();
assert_eq!(count, mesh.positions.len(), "XML vertex count mismatch");
}
#[test]
fn model_xml_triangle_count_matches() {
let mesh = simple_mesh();
let xml = build_3mf_model_xml(&mesh, &ThreeMfOptions::default());
let count = xml.matches("<triangle ").count();
assert_eq!(count, mesh.indices.len() / 3, "XML triangle count mismatch");
}
#[test]
fn model_xml_contains_unit() {
let opts = ThreeMfOptions::default();
let mesh = simple_mesh();
let xml = build_3mf_model_xml(&mesh, &opts);
assert!(
xml.contains("millimeter"),
"XML should contain unit attribute"
);
}
#[test]
fn content_types_contains_3dmodel() {
let ct = build_content_types_xml();
assert!(
ct.contains("3dmanufacturing-3dmodel"),
"content types should reference 3dmodel content type"
);
}
#[test]
fn content_types_is_xml() {
let ct = build_content_types_xml();
assert!(ct.starts_with("<?xml"), "should start with XML declaration");
}
#[test]
fn rels_contains_relationship() {
let rels = build_rels_xml();
assert!(
rels.contains("Relationship"),
"rels XML should contain Relationship element"
);
}
#[test]
fn rels_points_to_3dmodel() {
let rels = build_rels_xml();
assert!(
rels.contains("3dmodel.model"),
"rels XML should reference 3dmodel.model"
);
}
#[test]
fn export_3mf_zip_starts_with_pk_magic() {
let mesh = simple_mesh();
let result = export_3mf(&mesh, &ThreeMfOptions::default());
assert!(result.zip_bytes.len() >= 4, "ZIP bytes should be non-empty");
assert_eq!(
&result.zip_bytes[0..4],
&[0x50, 0x4B, 0x03, 0x04],
"ZIP must start with PK magic"
);
}
#[test]
fn export_3mf_result_vertex_count_matches() {
let mesh = simple_mesh();
let result = export_3mf(&mesh, &ThreeMfOptions::default());
assert_eq!(result.vertex_count, 3);
}
#[test]
fn export_3mf_result_triangle_count_matches() {
let mesh = simple_mesh();
let result = export_3mf(&mesh, &ThreeMfOptions::default());
assert_eq!(result.triangle_count, 1);
}
#[test]
fn export_3mf_model_xml_size_positive() {
let mesh = simple_mesh();
let result = export_3mf(&mesh, &ThreeMfOptions::default());
assert!(result.model_xml_size > 0);
}
#[test]
fn validate_3mf_zip_valid() {
let mesh = simple_mesh();
let result = export_3mf(&mesh, &ThreeMfOptions::default());
assert!(
validate_3mf_zip(&result.zip_bytes),
"exported 3MF should pass validation"
);
}
#[test]
fn validate_3mf_zip_invalid_data() {
let garbage = b"not a zip at all";
assert!(!validate_3mf_zip(garbage));
}
#[test]
fn validate_3mf_zip_empty_data() {
assert!(!validate_3mf_zip(&[]));
}
#[test]
fn mesh_is_printable_valid_mesh() {
assert!(mesh_is_printable(&simple_mesh()));
}
#[test]
fn mesh_is_printable_empty_mesh_false() {
assert!(!mesh_is_printable(&empty_mesh()));
}
#[test]
fn mesh_is_printable_out_of_range_index_false() {
let mut mesh = simple_mesh();
mesh.indices.push(999); assert!(!mesh_is_printable(&mesh));
}
}