use std::io::{Cursor, Write as _};
use brepkit_operations::tessellate::{self, TriangleMesh};
use brepkit_topology::Topology;
use brepkit_topology::solid::SolidId;
use quick_xml::Writer;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
use crate::IoError;
const NS_3MF: &str = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
pub fn write_threemf(
topo: &Topology,
solids: &[SolidId],
deflection: f64,
) -> Result<Vec<u8>, IoError> {
if solids.is_empty() {
return Err(IoError::InvalidTopology {
reason: "no solids to export".to_string(),
});
}
if !deflection.is_finite() || deflection <= 0.0 {
return Err(IoError::InvalidTopology {
reason: format!("deflection must be positive and finite, got {deflection}"),
});
}
let meshes: Vec<TriangleMesh> = solids
.iter()
.map(|&solid_id| tessellate_solid(topo, solid_id, deflection))
.collect::<Result<_, _>>()?;
for (i, mesh) in meshes.iter().enumerate() {
if mesh.indices.len() < 3 {
return Err(IoError::InvalidTopology {
reason: format!("solid {i} tessellated to zero triangles"),
});
}
if mesh.indices.len() % 3 != 0 {
return Err(IoError::InvalidTopology {
reason: format!(
"solid {i} has {} indices (not a multiple of 3)",
mesh.indices.len()
),
});
}
}
let model_xml = write_model_xml(&meshes)?;
write_zip(&model_xml)
}
fn tessellate_solid(
topo: &Topology,
solid_id: SolidId,
deflection: f64,
) -> Result<TriangleMesh, IoError> {
tessellate::tessellate_solid(topo, solid_id, deflection).map_err(IoError::Operations)
}
fn write_model_xml(meshes: &[TriangleMesh]) -> Result<Vec<u8>, IoError> {
let mut buf = Vec::new();
let mut writer = Writer::new_with_indent(&mut buf, b' ', 1);
writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
let mut model = BytesStart::new("model");
model.push_attribute(("xmlns", NS_3MF));
model.push_attribute(("unit", "millimeter"));
writer.write_event(Event::Start(model))?;
writer.write_event(Event::Start(BytesStart::new("resources")))?;
for (i, mesh) in meshes.iter().enumerate() {
write_object(&mut writer, i, mesh)?;
}
writer.write_event(Event::End(BytesEnd::new("resources")))?;
writer.write_event(Event::Start(BytesStart::new("build")))?;
for i in 0..meshes.len() {
let mut item = BytesStart::new("item");
let id_str = (i + 1).to_string();
item.push_attribute(("objectid", id_str.as_str()));
writer.write_event(Event::Empty(item))?;
}
writer.write_event(Event::End(BytesEnd::new("build")))?;
writer.write_event(Event::End(BytesEnd::new("model")))?;
Ok(buf)
}
fn write_object(
writer: &mut Writer<&mut Vec<u8>>,
index: usize,
mesh: &TriangleMesh,
) -> Result<(), IoError> {
let id_str = (index + 1).to_string();
let mut object = BytesStart::new("object");
object.push_attribute(("id", id_str.as_str()));
object.push_attribute(("type", "model"));
writer.write_event(Event::Start(object))?;
writer.write_event(Event::Start(BytesStart::new("mesh")))?;
writer.write_event(Event::Start(BytesStart::new("vertices")))?;
for pos in &mesh.positions {
let mut vertex = BytesStart::new("vertex");
vertex.push_attribute(("x", format_f64(pos.x()).as_str()));
vertex.push_attribute(("y", format_f64(pos.y()).as_str()));
vertex.push_attribute(("z", format_f64(pos.z()).as_str()));
writer.write_event(Event::Empty(vertex))?;
}
writer.write_event(Event::End(BytesEnd::new("vertices")))?;
writer.write_event(Event::Start(BytesStart::new("triangles")))?;
for tri in mesh.indices.chunks_exact(3) {
let mut triangle = BytesStart::new("triangle");
triangle.push_attribute(("v1", tri[0].to_string().as_str()));
triangle.push_attribute(("v2", tri[1].to_string().as_str()));
triangle.push_attribute(("v3", tri[2].to_string().as_str()));
writer.write_event(Event::Empty(triangle))?;
}
writer.write_event(Event::End(BytesEnd::new("triangles")))?;
writer.write_event(Event::End(BytesEnd::new("mesh")))?;
writer.write_event(Event::End(BytesEnd::new("object")))?;
Ok(())
}
fn format_f64(v: f64) -> String {
format!("{v:.6}")
}
const CONTENT_TYPES_XML: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
</Types>"#;
const RELS_XML: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
</Relationships>"#;
fn write_zip(model_xml: &[u8]) -> Result<Vec<u8>, IoError> {
let buf = Cursor::new(Vec::new());
let mut zip = ZipWriter::new(buf);
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
zip.start_file("[Content_Types].xml", options)?;
zip.write_all(CONTENT_TYPES_XML)?;
zip.start_file("_rels/.rels", options)?;
zip.write_all(RELS_XML)?;
zip.start_file("3D/3dmodel.model", options)?;
zip.write_all(model_xml)?;
let cursor = zip.finish()?;
Ok(cursor.into_inner())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use brepkit_math::vec::{Point3, Vec3};
use brepkit_operations::extrude::extrude;
use brepkit_operations::revolve::revolve;
use brepkit_topology::Topology;
use brepkit_topology::test_utils::{make_unit_cube_non_manifold, make_unit_square_face};
use super::*;
fn make_extruded_box(topo: &mut Topology) -> SolidId {
let face = make_unit_square_face(topo);
extrude(topo, face, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap()
}
#[test]
fn export_extruded_box() {
let mut topo = Topology::new();
let solid = make_extruded_box(&mut topo);
let bytes = write_threemf(&topo, &[solid], 0.1).unwrap();
let reader = zip::ZipArchive::new(Cursor::new(&bytes)).unwrap();
assert_eq!(reader.len(), 3);
assert!(reader.file_names().any(|n| n == "[Content_Types].xml"));
assert!(reader.file_names().any(|n| n == "_rels/.rels"));
assert!(reader.file_names().any(|n| n == "3D/3dmodel.model"));
}
#[test]
fn export_unit_cube() {
let mut topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut topo);
let bytes = write_threemf(&topo, &[solid], 0.1).unwrap();
let mut archive = zip::ZipArchive::new(Cursor::new(&bytes)).unwrap();
let mut model_file = archive.by_name("3D/3dmodel.model").unwrap();
let mut xml_str = String::new();
std::io::Read::read_to_string(&mut model_file, &mut xml_str).unwrap();
assert!(xml_str.contains("<vertex"));
assert!(xml_str.contains("<triangle"));
assert!(xml_str.contains("<object"));
assert!(xml_str.contains("<item"));
}
#[test]
fn export_revolved_solid() {
let mut topo = Topology::new();
let face = make_unit_square_face(&mut topo);
let solid = revolve(
&mut topo,
face,
Point3::new(2.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
std::f64::consts::FRAC_PI_2,
)
.unwrap();
let bytes = write_threemf(&topo, &[solid], 0.25).unwrap();
let reader = zip::ZipArchive::new(Cursor::new(&bytes)).unwrap();
assert_eq!(reader.len(), 3);
}
#[test]
fn export_multiple_solids() {
let mut topo = Topology::new();
let s1 = make_extruded_box(&mut topo);
let s2 = make_unit_cube_non_manifold(&mut topo);
let bytes = write_threemf(&topo, &[s1, s2], 0.1).unwrap();
let mut archive = zip::ZipArchive::new(Cursor::new(&bytes)).unwrap();
let mut model_file = archive.by_name("3D/3dmodel.model").unwrap();
let mut xml_str = String::new();
std::io::Read::read_to_string(&mut model_file, &mut xml_str).unwrap();
assert_eq!(xml_str.matches("<object").count(), 2);
assert_eq!(xml_str.matches("<item").count(), 2);
}
#[test]
fn export_empty_solids_error() {
let topo = Topology::new();
let result = write_threemf(&topo, &[], 0.1);
assert!(result.is_err());
}
#[test]
fn export_invalid_deflection_error() {
let mut topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut topo);
assert!(write_threemf(&topo, &[solid], 0.0).is_err());
assert!(write_threemf(&topo, &[solid], -1.0).is_err());
assert!(write_threemf(&topo, &[solid], f64::NAN).is_err());
assert!(write_threemf(&topo, &[solid], f64::INFINITY).is_err());
}
fn count_elements(xml: &str, tag: &str) -> usize {
xml.matches(&format!("<{tag} ")).count()
}
fn extract_model_xml(bytes: &[u8]) -> String {
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
let mut model_file = archive.by_name("3D/3dmodel.model").unwrap();
let mut xml_str = String::new();
std::io::Read::read_to_string(&mut model_file, &mut xml_str).unwrap();
xml_str
}
#[test]
fn unit_cube_vertex_and_triangle_counts() {
let mut topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut topo);
let bytes = write_threemf(&topo, &[solid], 0.1).unwrap();
let xml = extract_model_xml(&bytes);
assert_eq!(count_elements(&xml, "vertex"), 8);
assert_eq!(count_elements(&xml, "triangle"), 12);
}
#[test]
fn xml_has_correct_namespace() {
let mut topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut topo);
let bytes = write_threemf(&topo, &[solid], 0.5).unwrap();
let xml = extract_model_xml(&bytes);
assert!(xml.contains(NS_3MF));
assert!(xml.contains("unit=\"millimeter\""));
}
#[test]
fn vertex_attributes_are_finite() {
let mut topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut topo);
let bytes = write_threemf(&topo, &[solid], 0.5).unwrap();
let xml = extract_model_xml(&bytes);
assert!(!xml.contains("NaN"));
assert!(!xml.contains("Infinity"));
assert!(!xml.contains("inf"));
}
#[test]
fn roundtrip_zip_contains_expected_entries() {
let mut topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut topo);
let bytes = write_threemf(&topo, &[solid], 0.5).unwrap();
let reader = zip::ZipArchive::new(Cursor::new(&bytes)).unwrap();
let names: Vec<&str> = reader.file_names().collect();
assert!(names.contains(&"[Content_Types].xml"));
assert!(names.contains(&"_rels/.rels"));
assert!(names.contains(&"3D/3dmodel.model"));
}
}