use std::collections::{BTreeMap, HashMap};
use super::zip::crc32;
use super::CORE_NAMESPACE;
use crate::Mesh;
#[derive(Clone, Debug, Default)]
pub struct ThreeMfSolid {
pub name: String,
pub mesh: Mesh,
pub color: Option<[u8; 3]>,
pub face_colors: BTreeMap<u32, [u8; 3]>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ThreeMfWriteReport {
pub objects: usize,
pub triangles: usize,
pub vertices: usize,
pub degenerate_triangles: usize,
pub materials: usize,
pub colored_triangles: usize,
pub painted_triangles: usize,
pub first_paint_slot: usize,
pub paint_slots: usize,
pub unpainted_colors: usize,
pub bytes: usize,
}
const SLIC3RPE_NAMESPACE: &str = "http://schemas.slic3r.org/3mf/2017/06";
pub const MAX_PAINT_SLOTS: usize = 15;
fn painting_code(slot: usize) -> String {
debug_assert!((1..=MAX_PAINT_SLOTS).contains(&slot));
if slot < 3 {
format!("{:X}", slot << 2)
} else {
format!("{:X}C", slot - 3)
}
}
const CONTENT_TYPES_PART: &str = "[Content_Types].xml";
const RELATIONSHIPS_PART: &str = "_rels/.rels";
const MODEL_PART: &str = "3D/3dmodel.model";
const BASE_MATERIALS_ID: u32 = 1;
const FIRST_OBJECT_ID: u32 = 2;
const CONTENT_TYPES_XML: &str = "<?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";
const RELATIONSHIPS_XML: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n \
<Relationship Id=\"rel0\" Type=\"http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel\" Target=\"/3D/3dmodel.model\"/>\n\
</Relationships>\n";
fn xml_escape(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c if (c as u32) < 0x20 && c != '\t' && c != '\n' && c != '\r' => {}
c => out.push(c),
}
}
out
}
fn display_color(rgb: [u8; 3]) -> String {
format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2])
}
fn coordinate(value: f64) -> Result<String, String> {
let narrow = value as f32;
if !narrow.is_finite() {
return Err("write_3mf: non-finite vertex coordinate".into());
}
let narrow = if narrow == 0.0 { 0.0 } else { narrow };
Ok(format!("{narrow}"))
}
fn triangle_color(solid: &ThreeMfSolid, triangle: usize) -> Option<[u8; 3]> {
solid
.mesh
.face_ids
.get(triangle)
.and_then(|face| solid.face_colors.get(face))
.copied()
.or(solid.color)
}
#[derive(Default)]
struct Palette {
colors: Vec<[u8; 3]>,
index: HashMap<[u8; 3], u32>,
}
impl Palette {
fn intern(&mut self, rgb: [u8; 3]) -> u32 {
if let Some(index) = self.index.get(&rgb) {
return *index;
}
let index = self.colors.len() as u32;
self.colors.push(rgb);
self.index.insert(rgb, index);
index
}
}
struct WeldedObject {
id: u32,
name: String,
vertices: Vec<[String; 3]>,
triangles: Vec<([u32; 3], Option<u32>)>,
body: Option<u32>,
}
fn weld(
solid: &ThreeMfSolid,
id: u32,
palette: &mut Palette,
report: &mut ThreeMfWriteReport,
) -> Result<Option<WeldedObject>, String> {
solid.mesh.validate_geometry()?;
if solid.mesh.indices.is_empty() {
return Ok(None);
}
let body = solid.color.map(|rgb| palette.intern(rgb));
let source_vertices = solid.mesh.positions.len() / 3;
let mut by_key: HashMap<[String; 3], u32> = HashMap::new();
let mut remap: Vec<u32> = Vec::with_capacity(source_vertices);
let mut vertices: Vec<[String; 3]> = Vec::new();
for point in solid.mesh.positions.chunks_exact(3) {
let key = [coordinate(point[0])?, coordinate(point[1])?, coordinate(point[2])?];
let index = match by_key.get(&key) {
Some(index) => *index,
None => {
let index = vertices.len() as u32;
vertices.push(key.clone());
by_key.insert(key, index);
index
}
};
remap.push(index);
}
let mut triangles = Vec::with_capacity(solid.mesh.indices.len() / 3);
for (triangle, corners) in solid.mesh.indices.chunks_exact(3).enumerate() {
let a = remap[corners[0] as usize];
let b = remap[corners[1] as usize];
let c = remap[corners[2] as usize];
if a == b || b == c || a == c {
report.degenerate_triangles += 1;
continue;
}
let color = triangle_color(solid, triangle).map(|rgb| palette.intern(rgb));
triangles.push(([a, b, c], color));
}
if triangles.is_empty() {
return Ok(None);
}
Ok(Some(WeldedObject { id, name: solid.name.clone(), vertices, triangles, body }))
}
fn paint_slot(color: u32, first_slot: usize) -> Option<usize> {
let slot = color as usize + first_slot;
(slot <= MAX_PAINT_SLOTS).then_some(slot)
}
fn model_xml(objects: &[WeldedObject], palette: &Palette, document_name: &str, first_slot: usize) -> String {
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str(&format!(
"<model unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"{CORE_NAMESPACE}\" \
xmlns:slic3rpe=\"{SLIC3RPE_NAMESPACE}\">\n"
));
xml.push_str(&format!(
" <metadata name=\"Title\">{}</metadata>\n",
xml_escape(document_name)
));
xml.push_str(" <metadata name=\"Application\">next.BREP.io</metadata>\n");
xml.push_str(" <resources>\n");
if !palette.colors.is_empty() {
xml.push_str(&format!(" <basematerials id=\"{BASE_MATERIALS_ID}\">\n"));
for rgb in &palette.colors {
xml.push_str(&format!(
" <base name=\"color_{:02x}{:02x}{:02x}\" displaycolor=\"{}\"/>\n",
rgb[0],
rgb[1],
rgb[2],
display_color(*rgb)
));
}
xml.push_str(" </basematerials>\n");
}
for object in objects {
xml.push_str(&format!(
" <object id=\"{}\" name=\"{}\" type=\"model\"",
object.id,
xml_escape(&object.name)
));
if let Some(body) = object.body {
xml.push_str(&format!(" pid=\"{BASE_MATERIALS_ID}\" pindex=\"{body}\""));
}
xml.push_str(">\n <mesh>\n <vertices>\n");
for [x, y, z] in &object.vertices {
xml.push_str(&format!(" <vertex x=\"{x}\" y=\"{y}\" z=\"{z}\"/>\n"));
}
xml.push_str(" </vertices>\n <triangles>\n");
for ([a, b, c], color) in &object.triangles {
xml.push_str(&format!(" <triangle v1=\"{a}\" v2=\"{b}\" v3=\"{c}\""));
if let Some(color) = color {
if let Some(slot) = paint_slot(*color, first_slot) {
let code = painting_code(slot);
xml.push_str(&format!(
" slic3rpe:mmu_segmentation=\"{code}\" paint_color=\"{code}\""
));
}
if Some(*color) != object.body {
xml.push_str(&format!(" pid=\"{BASE_MATERIALS_ID}\" p1=\"{color}\""));
}
}
xml.push_str("/>\n");
}
xml.push_str(" </triangles>\n </mesh>\n </object>\n");
}
xml.push_str(" </resources>\n <build>\n");
for object in objects {
xml.push_str(&format!(" <item objectid=\"{}\"/>\n", object.id));
}
xml.push_str(" </build>\n</model>\n");
xml
}
struct ZipWriter {
bytes: Vec<u8>,
central: Vec<u8>,
entries: u16,
}
const DOS_DATE_1980_01_01: u16 = (1 << 5) | 1;
impl ZipWriter {
fn new() -> Self {
Self { bytes: Vec::new(), central: Vec::new(), entries: 0 }
}
fn add(&mut self, name: &str, data: &[u8]) -> Result<(), String> {
if data.len() > u32::MAX as usize {
return Err(format!("write_3mf: part '{name}' is larger than a ZIP entry can hold"));
}
let crc = crc32(data);
let size = data.len() as u32;
let offset = self.bytes.len() as u32;
let name_bytes = name.as_bytes();
let mut local = Vec::with_capacity(30 + name_bytes.len());
push_u32(&mut local, 0x0403_4b50);
push_u16(&mut local, 20); push_u16(&mut local, 0); push_u16(&mut local, 0); push_u16(&mut local, 0); push_u16(&mut local, DOS_DATE_1980_01_01);
push_u32(&mut local, crc);
push_u32(&mut local, size);
push_u32(&mut local, size);
push_u16(&mut local, name_bytes.len() as u16);
push_u16(&mut local, 0); local.extend_from_slice(name_bytes);
self.bytes.extend_from_slice(&local);
self.bytes.extend_from_slice(data);
push_u32(&mut self.central, 0x0201_4b50);
push_u16(&mut self.central, 20); push_u16(&mut self.central, 20); push_u16(&mut self.central, 0); push_u16(&mut self.central, 0); push_u16(&mut self.central, 0); push_u16(&mut self.central, DOS_DATE_1980_01_01);
push_u32(&mut self.central, crc);
push_u32(&mut self.central, size);
push_u32(&mut self.central, size);
push_u16(&mut self.central, name_bytes.len() as u16);
push_u16(&mut self.central, 0); push_u16(&mut self.central, 0); push_u16(&mut self.central, 0); push_u16(&mut self.central, 0); push_u32(&mut self.central, 0); push_u32(&mut self.central, offset);
self.central.extend_from_slice(name_bytes);
self.entries += 1;
Ok(())
}
fn finish(mut self) -> Result<Vec<u8>, String> {
let directory_offset = self.bytes.len();
let directory_size = self.central.len();
if directory_offset + directory_size + 22 > u32::MAX as usize {
return Err("write_3mf: the package is larger than a ZIP can hold".into());
}
self.bytes.extend_from_slice(&self.central);
push_u32(&mut self.bytes, 0x0605_4b50);
push_u16(&mut self.bytes, 0); push_u16(&mut self.bytes, 0); push_u16(&mut self.bytes, self.entries);
push_u16(&mut self.bytes, self.entries);
push_u32(&mut self.bytes, directory_size as u32);
push_u32(&mut self.bytes, directory_offset as u32);
push_u16(&mut self.bytes, 0); Ok(self.bytes)
}
}
fn push_u16(bytes: &mut Vec<u8>, value: u16) {
bytes.extend_from_slice(&value.to_le_bytes());
}
fn push_u32(bytes: &mut Vec<u8>, value: u32) {
bytes.extend_from_slice(&value.to_le_bytes());
}
pub fn write_3mf(
solids: &[ThreeMfSolid],
document_name: &str,
) -> Result<(Vec<u8>, ThreeMfWriteReport), String> {
let mut report = ThreeMfWriteReport::default();
let mut palette = Palette::default();
let mut objects = Vec::new();
for solid in solids {
let id = FIRST_OBJECT_ID + objects.len() as u32;
if let Some(object) = weld(solid, id, &mut palette, &mut report)? {
objects.push(object);
}
}
if objects.is_empty() {
return Err("write_3mf: nothing to export, no solid has a triangle".into());
}
report.objects = objects.len();
report.materials = palette.colors.len();
let any_uncolored = objects.iter().any(|o| o.triangles.iter().any(|(_, c)| c.is_none()));
let first_slot = if any_uncolored { 2 } else { 1 };
report.first_paint_slot = first_slot;
let slots = MAX_PAINT_SLOTS + 1 - first_slot;
report.paint_slots = palette.colors.len().min(slots);
report.unpainted_colors = palette.colors.len().saturating_sub(slots);
for object in &objects {
report.vertices += object.vertices.len();
report.triangles += object.triangles.len();
report.colored_triangles += object
.triangles
.iter()
.filter(|(_, color)| color.is_some() && *color != object.body)
.count();
report.painted_triangles += object
.triangles
.iter()
.filter(|(_, color)| color.is_some_and(|c| paint_slot(c, first_slot).is_some()))
.count();
}
let model = model_xml(&objects, &palette, document_name, first_slot);
let mut zip = ZipWriter::new();
zip.add(CONTENT_TYPES_PART, CONTENT_TYPES_XML.as_bytes())?;
zip.add(RELATIONSHIPS_PART, RELATIONSHIPS_XML.as_bytes())?;
zip.add(MODEL_PART, model.as_bytes())?;
let bytes = zip.finish()?;
report.bytes = bytes.len();
Ok((bytes, report))
}