mod inflate;
mod model;
mod write;
mod xml;
mod zip;
pub use write::{write_3mf, ThreeMfSolid, ThreeMfWriteReport};
use serde::Serialize;
pub const CORE_NAMESPACE: &str = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
const RELATIONSHIP_NAMESPACE: &str = "http://schemas.openxmlformats.org/package/2006/relationships";
const MODEL_RELATIONSHIP: &str = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel";
const RELATIONSHIP_PART: &str = "_rels/.rels";
pub const MAX_PART_BYTES: usize = 256 * 1024 * 1024;
pub const MAX_MESH_VERTICES: usize = 8_000_000;
pub const MAX_MESH_TRIANGLES: usize = 16_000_000;
pub const MAX_INSTANCES: usize = 65_536;
pub const MAX_TOTAL_TRIANGLES: usize = 32_000_000;
pub const MAX_COMPONENT_DEPTH: usize = 32;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ThreeMfError {
Container(String),
Damaged { part: String, detail: String },
MissingPart { part: String, detail: String },
Unsupported { part: String, detail: String },
Bounds { part: String, detail: String },
Xml { part: String, detail: String },
Model { part: String, detail: String },
}
impl ThreeMfError {
pub fn part(&self) -> Option<&str> {
match self {
Self::Container(_) => None,
Self::Damaged { part, .. }
| Self::MissingPart { part, .. }
| Self::Unsupported { part, .. }
| Self::Bounds { part, .. }
| Self::Xml { part, .. }
| Self::Model { part, .. } => Some(part),
}
}
}
impl core::fmt::Display for ThreeMfError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Container(detail) => write!(f, "3MF package: {detail}"),
Self::Damaged { part, detail } => write!(f, "3MF '{part}' is damaged: {detail}"),
Self::MissingPart { part, detail } => write!(f, "3MF '{part}' is missing: {detail}"),
Self::Unsupported { part, detail } => write!(f, "3MF '{part}': {detail}"),
Self::Bounds { part, detail } => write!(f, "3MF '{part}' exceeds a bound: {detail}"),
Self::Xml { part, detail } => write!(f, "3MF '{part}' is not well-formed: {detail}"),
Self::Model { part, detail } => write!(f, "3MF '{part}': {detail}"),
}
}
}
impl std::error::Error for ThreeMfError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum ThreeMfUnit {
Micron,
Millimeter,
Centimeter,
Inch,
Foot,
Meter,
}
impl ThreeMfUnit {
pub fn millimetres(self) -> f64 {
match self {
Self::Micron => 0.001,
Self::Millimeter => 1.0,
Self::Centimeter => 10.0,
Self::Inch => 25.4,
Self::Foot => 304.8,
Self::Meter => 1000.0,
}
}
pub fn name(self) -> &'static str {
match self {
Self::Micron => "micron",
Self::Millimeter => "millimeter",
Self::Centimeter => "centimeter",
Self::Inch => "inch",
Self::Foot => "foot",
Self::Meter => "meter",
}
}
fn parse(text: &str) -> Option<Self> {
Some(match text {
"micron" => Self::Micron,
"millimeter" => Self::Millimeter,
"centimeter" => Self::Centimeter,
"inch" => Self::Inch,
"foot" => Self::Foot,
"meter" => Self::Meter,
_ => return None,
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct ThreeMfMesh {
pub object_id: u32,
pub positions: Vec<f64>,
pub indices: Vec<u32>,
pub triangle_colors: Vec<Option<[u8; 3]>>,
}
impl ThreeMfMesh {
pub fn vertex_count(&self) -> usize {
self.positions.len() / 3
}
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ThreeMfInstance {
pub object_id: u32,
pub mesh: usize,
pub transform: [f64; 12],
pub first_vertex: usize,
pub vertex_count: usize,
pub first_triangle: usize,
pub triangle_count: usize,
pub name: Option<String>,
}
impl ThreeMfInstance {
pub fn transform_point(&self, point: [f64; 3]) -> [f64; 3] {
transform_point(&self.transform, point)
}
}
#[derive(Clone, Debug, Serialize)]
pub struct ThreeMfReadResult {
pub model_part: String,
pub unit: ThreeMfUnit,
pub meshes: Vec<ThreeMfMesh>,
pub instances: Vec<ThreeMfInstance>,
pub positions: Vec<f64>,
pub indices: Vec<u32>,
pub triangle_colors: Vec<Option<[u8; 3]>>,
}
impl ThreeMfReadResult {
pub fn vertex_count(&self) -> usize {
self.positions.len() / 3
}
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
pub fn instance_mesh(&self, instance: usize) -> Option<(Vec<f64>, Vec<u32>)> {
let instance = self.instances.get(instance)?;
let positions = self
.positions
.get(instance.first_vertex * 3..(instance.first_vertex + instance.vertex_count) * 3)?
.to_vec();
let base = instance.first_vertex as u32;
let indices = self
.indices
.get(
instance.first_triangle * 3
..(instance.first_triangle + instance.triangle_count) * 3,
)?
.iter()
.map(|index| index - base)
.collect();
Some((positions, indices))
}
}
pub const IDENTITY: [f64; 12] = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0];
pub fn transform_point(transform: &[f64; 12], point: [f64; 3]) -> [f64; 3] {
let [x, y, z] = point;
[
x * transform[0] + y * transform[3] + z * transform[6] + transform[9],
x * transform[1] + y * transform[4] + z * transform[7] + transform[10],
x * transform[2] + y * transform[5] + z * transform[8] + transform[11],
]
}
pub fn compose(inner: &[f64; 12], outer: &[f64; 12]) -> [f64; 12] {
let mut out = [0.0; 12];
for row in 0..4 {
for column in 0..3 {
let mut sum = if row == 3 { outer[9 + column] } else { 0.0 };
for k in 0..3 {
sum += inner[row * 3 + k] * outer[k * 3 + column];
}
out[row * 3 + column] = sum;
}
}
out
}
fn determinant(transform: &[f64; 12]) -> f64 {
let m = transform;
m[0] * (m[4] * m[8] - m[5] * m[7]) - m[1] * (m[3] * m[8] - m[5] * m[6])
+ m[2] * (m[3] * m[7] - m[4] * m[6])
}
pub fn read_3mf(bytes: &[u8]) -> Result<ThreeMfReadResult, ThreeMfError> {
let archive = zip::ZipArchive::open(bytes).map_err(map_zip)?;
let relationships = archive.find(RELATIONSHIP_PART).ok_or_else(|| {
ThreeMfError::MissingPart {
part: RELATIONSHIP_PART.into(),
detail: "an OPC package must carry the package relationships part".into(),
}
})?;
let relationships_text = read_text(&archive, relationships, RELATIONSHIP_PART)?;
let model_part = start_part(&relationships_text)?;
let model_entry = archive.find(&model_part).ok_or_else(|| ThreeMfError::MissingPart {
part: model_part.clone(),
detail: format!("'{RELATIONSHIP_PART}' names it as the 3D model start part"),
})?;
let model_text = read_text(&archive, model_entry, &model_part)?;
let document = model::parse_model(&model_part, &model_text)?;
flatten(model_part, document)
}
fn map_zip(error: zip::ZipError) -> ThreeMfError {
let part = error.entry.clone().unwrap_or_else(|| "[package]".into());
match error.fault {
zip::ZipFault::Container => match &error.entry {
Some(entry) => ThreeMfError::Container(format!("{entry}: {}", error.message)),
None => ThreeMfError::Container(error.message),
},
zip::ZipFault::Damaged => ThreeMfError::Damaged { part, detail: error.message },
zip::ZipFault::Unsupported => ThreeMfError::Unsupported { part, detail: error.message },
zip::ZipFault::Bounds => ThreeMfError::Bounds { part, detail: error.message },
}
}
fn read_text(
archive: &zip::ZipArchive<'_>,
entry: &zip::ZipEntry,
part: &str,
) -> Result<String, ThreeMfError> {
let bytes = archive.read(entry, MAX_PART_BYTES).map_err(map_zip)?;
String::from_utf8(bytes).map_err(|_| ThreeMfError::Xml {
part: part.to_owned(),
detail: "the part is not UTF-8 text".into(),
})
}
fn start_part(text: &str) -> Result<String, ThreeMfError> {
let mut reader = xml::XmlReader::new(text);
loop {
let event = reader.next().map_err(|error| ThreeMfError::Xml {
part: RELATIONSHIP_PART.into(),
detail: error.to_string(),
})?;
match event {
xml::Event::Start(tag) => {
if tag.namespace != RELATIONSHIP_NAMESPACE {
reader.skip_subtree(&tag).map_err(|error| ThreeMfError::Xml {
part: RELATIONSHIP_PART.into(),
detail: error.to_string(),
})?;
continue;
}
if tag.name != "Relationship" {
continue;
}
if tag.attribute("Type") != Some(MODEL_RELATIONSHIP) {
continue;
}
let target = tag.attribute("Target").ok_or_else(|| ThreeMfError::Model {
part: RELATIONSHIP_PART.into(),
detail: "the 3D model relationship has no Target".into(),
})?;
let target = target.trim_start_matches('/');
if target.is_empty() {
return Err(ThreeMfError::Model {
part: RELATIONSHIP_PART.into(),
detail: "the 3D model relationship has an empty Target".into(),
});
}
return Ok(target.to_owned());
}
xml::Event::End { .. } => {}
xml::Event::Eof => break,
}
}
Err(ThreeMfError::MissingPart {
part: RELATIONSHIP_PART.into(),
detail: format!("no relationship of type '{MODEL_RELATIONSHIP}' names a 3D model part"),
})
}
fn flatten(
model_part: String,
document: model::ModelDocument,
) -> Result<ThreeMfReadResult, ThreeMfError> {
let scale = document.unit.millimetres();
let meshes = document
.meshes
.iter()
.map(|mesh| {
let object_color = document.color(mesh.object_property);
let mut triangle_colors: Vec<Option<[u8; 3]>> = mesh
.properties
.iter()
.map(|property| match property {
Some(property) => document.color(Some(*property)),
None => object_color,
})
.collect();
if triangle_colors.iter().all(Option::is_none) {
triangle_colors.clear();
}
ThreeMfMesh {
object_id: mesh.object_id,
positions: mesh.positions.iter().map(|value| value * scale).collect(),
indices: mesh.indices.clone(),
triangle_colors,
}
})
.collect::<Vec<_>>();
let mut instances = Vec::new();
for item in &document.items {
let mut transform = item.transform;
for value in &mut transform[9..12] {
*value *= scale;
}
place(&document, &model_part, item.object_id, transform, 0, None, &mut instances)?;
}
let mut positions = Vec::new();
let mut indices = Vec::new();
let mut triangle_colors: Vec<Option<[u8; 3]>> = Vec::new();
let colored = instances.iter().any(|instance| !meshes[instance.mesh].triangle_colors.is_empty());
let mut triangles = 0usize;
for instance in &mut instances {
let mesh = &meshes[instance.mesh];
triangles += mesh.triangle_count();
if triangles > MAX_TOTAL_TRIANGLES {
return Err(ThreeMfError::Bounds {
part: model_part,
detail: format!(
"the build places more than {MAX_TOTAL_TRIANGLES} triangles in total"
),
});
}
instance.first_vertex = positions.len() / 3;
instance.vertex_count = mesh.vertex_count();
instance.first_triangle = indices.len() / 3;
instance.triangle_count = mesh.triangle_count();
let base = instance.first_vertex as u32;
for point in mesh.positions.chunks_exact(3) {
let placed = instance.transform_point([point[0], point[1], point[2]]);
positions.extend(placed);
}
let mirrored = determinant(&instance.transform) < 0.0;
for triangle in mesh.indices.chunks_exact(3) {
if mirrored {
indices.extend([base + triangle[0], base + triangle[2], base + triangle[1]]);
} else {
indices.extend([base + triangle[0], base + triangle[1], base + triangle[2]]);
}
}
if colored {
if mesh.triangle_colors.is_empty() {
triangle_colors.extend(std::iter::repeat(None).take(mesh.triangle_count()));
} else {
triangle_colors.extend_from_slice(&mesh.triangle_colors);
}
}
}
Ok(ThreeMfReadResult {
model_part,
unit: document.unit,
meshes,
instances,
positions,
indices,
triangle_colors,
})
}
fn place(
document: &model::ModelDocument,
model_part: &str,
object_id: u32,
transform: [f64; 12],
depth: usize,
inherited_name: Option<&str>,
instances: &mut Vec<ThreeMfInstance>,
) -> Result<(), ThreeMfError> {
if depth > MAX_COMPONENT_DEPTH {
return Err(ThreeMfError::Bounds {
part: model_part.to_owned(),
detail: format!("components nest deeper than {MAX_COMPONENT_DEPTH} levels"),
});
}
let object = document.object(object_id).ok_or_else(|| ThreeMfError::Model {
part: model_part.to_owned(),
detail: format!("object {object_id} is placed but never defined"),
})?;
let name = object.name.as_deref().or(inherited_name);
match &object.content {
model::ObjectContent::Mesh(mesh) => {
if object.model_type {
if instances.len() + 1 > MAX_INSTANCES {
return Err(ThreeMfError::Bounds {
part: model_part.to_owned(),
detail: format!("the build places more than {MAX_INSTANCES} instances"),
});
}
instances.push(ThreeMfInstance {
object_id: object.id,
mesh: *mesh,
transform,
first_vertex: 0,
vertex_count: 0,
first_triangle: 0,
triangle_count: 0,
name: name.map(str::to_owned),
});
}
Ok(())
}
model::ObjectContent::Components(components) => {
for component in components {
let mut inner = component.transform;
let scale = document.unit.millimetres();
for value in &mut inner[9..12] {
*value *= scale;
}
let composed = compose(&inner, &transform);
place(
document,
model_part,
component.object_id,
composed,
depth + 1,
name,
instances,
)?;
}
Ok(())
}
}
}