1use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
81use lib3mf_core::error::{Lib3mfError, Result};
82use lib3mf_core::model::resources::ResourceId;
83use lib3mf_core::model::{BuildItem, Mesh, Model, Triangle, Vertex};
84use std::collections::HashMap;
85use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum StlFormat {
90 Binary,
92 Ascii,
94}
95
96pub fn detect_stl_format<R: Read + Seek>(reader: &mut R) -> Result<StlFormat> {
113 let mut buf = [0u8; 84];
114 let n = reader.read(&mut buf).map_err(Lib3mfError::Io)?;
115 reader.seek(SeekFrom::Start(0)).map_err(Lib3mfError::Io)?;
116
117 if n < 5 {
118 return Ok(StlFormat::Ascii);
120 }
121
122 if !buf[..5].eq_ignore_ascii_case(b"solid") {
123 return Ok(StlFormat::Binary);
124 }
125
126 if n >= 84 {
129 let tri_count = u32::from_le_bytes([buf[80], buf[81], buf[82], buf[83]]);
130 let expected_binary_size = 84u64 + tri_count as u64 * 50;
131 let file_size = reader.seek(SeekFrom::End(0)).map_err(Lib3mfError::Io)?;
132 reader.seek(SeekFrom::Start(0)).map_err(Lib3mfError::Io)?;
133 if file_size == expected_binary_size {
134 return Ok(StlFormat::Binary);
135 }
136 }
137
138 Ok(StlFormat::Ascii)
139}
140
141pub struct StlImporter;
157
158impl Default for StlImporter {
159 fn default() -> Self {
160 Self::new()
161 }
162}
163
164impl StlImporter {
165 pub fn new() -> Self {
167 Self
168 }
169
170 pub fn read<R: Read + Seek>(mut reader: R) -> Result<Model> {
192 let format = detect_stl_format(&mut reader)?;
193 match format {
194 StlFormat::Binary => Self::read_binary(reader),
195 StlFormat::Ascii => Self::read_ascii(reader),
196 }
197 }
198
199 pub fn read_binary<R: Read>(mut reader: R) -> Result<Model> {
254 let mut header = [0u8; 80];
260 reader.read_exact(&mut header).map_err(Lib3mfError::Io)?;
261
262 let triangle_count = reader.read_u32::<LittleEndian>().map_err(|_| {
263 Lib3mfError::Validation("Failed to read STL triangle count".to_string())
264 })?;
265
266 let mut mesh = Mesh::default();
267 let mut vert_map: HashMap<[u32; 3], u32> = HashMap::new();
268
269 for _ in 0..triangle_count {
270 let _nx = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
272 let _ny = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
273 let _nz = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
274
275 let mut indices = [0u32; 3];
276
277 for index in &mut indices {
278 let x = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
279 let y = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
280 let z = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
281
282 let key = [x.to_bits(), y.to_bits(), z.to_bits()];
283
284 let idx = *vert_map.entry(key).or_insert_with(|| {
285 let new_idx = mesh.vertices.len() as u32;
286 mesh.vertices.push(Vertex { x, y, z });
287 new_idx
288 });
289 *index = idx;
290 }
291
292 let _attr_byte_count = reader.read_u16::<LittleEndian>().map_err(Lib3mfError::Io)?;
293
294 mesh.triangles.push(Triangle {
295 v1: indices[0],
296 v2: indices[1],
297 v3: indices[2],
298 ..Default::default()
299 });
300 }
301
302 let mut model = Model::default();
303 let resource_id = ResourceId(1); let object = lib3mf_core::model::Object {
306 id: resource_id,
307 object_type: lib3mf_core::model::ObjectType::Model,
308 name: Some("STL Import".to_string()),
309 part_number: None,
310 uuid: None,
311 pid: None,
312 pindex: None,
313 thumbnail: None,
314 geometry: lib3mf_core::model::Geometry::Mesh(mesh),
315 };
316
317 let _ = model.resources.add_object(object);
318
319 model.build.items.push(BuildItem {
320 object_id: resource_id,
321 transform: glam::Mat4::IDENTITY,
322 part_number: None,
323 uuid: None,
324 path: None,
325 printable: None,
326 });
327
328 Ok(model)
329 }
330
331 pub fn read_ascii<R: Read>(reader: R) -> Result<Model> {
366 let buf_reader = BufReader::new(reader);
367 let mut model = Model::default();
368 let mut next_id = 1u32;
369
370 let mut current_mesh: Option<(Mesh, String)> = None;
371 let mut vert_map: HashMap<[u32; 3], u32> = HashMap::new();
372 let mut facet_verts: Vec<(f32, f32, f32)> = Vec::with_capacity(3);
374
375 for line_res in buf_reader.lines() {
376 let line = line_res.map_err(Lib3mfError::Io)?;
377 let trimmed = line.trim();
378 if trimmed.is_empty() {
379 continue;
380 }
381
382 let parts: Vec<&str> = trimmed.split_whitespace().collect();
383 if parts.is_empty() {
384 continue;
385 }
386
387 match parts[0].to_ascii_lowercase().as_str() {
388 "solid" => {
389 let name = if parts.len() > 1 {
391 parts[1..].join(" ")
392 } else {
393 String::new()
394 };
395 current_mesh = Some((Mesh::default(), name));
396 vert_map.clear();
397 facet_verts.clear();
398 }
399 "facet" => {
400 facet_verts.clear();
402 }
403 "vertex" => {
404 if parts.len() >= 4 {
405 let x = parts[1].parse::<f32>().map_err(|_| {
406 Lib3mfError::InvalidStructure("Invalid STL vertex x coordinate".into())
407 })?;
408 let y = parts[2].parse::<f32>().map_err(|_| {
409 Lib3mfError::InvalidStructure("Invalid STL vertex y coordinate".into())
410 })?;
411 let z = parts[3].parse::<f32>().map_err(|_| {
412 Lib3mfError::InvalidStructure("Invalid STL vertex z coordinate".into())
413 })?;
414 facet_verts.push((x, y, z));
415 }
416 }
417 "endfacet" => {
418 if facet_verts.len() != 3 {
419 return Err(Lib3mfError::InvalidStructure(format!(
420 "STL facet must have exactly 3 vertices, found {}",
421 facet_verts.len()
422 )));
423 }
424 if let Some((ref mut mesh, _)) = current_mesh {
425 let mut indices = [0u32; 3];
426 for (i, &(x, y, z)) in facet_verts.iter().enumerate() {
427 let key = [x.to_bits(), y.to_bits(), z.to_bits()];
428 let idx = *vert_map.entry(key).or_insert_with(|| {
429 let new_idx = mesh.vertices.len() as u32;
430 mesh.vertices.push(Vertex { x, y, z });
431 new_idx
432 });
433 indices[i] = idx;
434 }
435 mesh.triangles.push(Triangle {
436 v1: indices[0],
437 v2: indices[1],
438 v3: indices[2],
439 ..Default::default()
440 });
441 }
442 facet_verts.clear();
443 }
444 "endsolid" => {
445 if let Some((mesh, name)) = current_mesh.take() {
446 finalize_solid(&mut model, mesh, name, next_id);
447 next_id += 1;
448 }
449 }
450 _ => {}
452 }
453 }
454
455 if let Some((mesh, name)) = current_mesh.take() {
457 finalize_solid(&mut model, mesh, name, next_id);
458 }
459
460 Ok(model)
461 }
462}
463
464fn finalize_solid(model: &mut Model, mesh: Mesh, name: String, id: u32) {
466 let resource_id = ResourceId(id);
467 let object_name = if name.is_empty() { None } else { Some(name) };
468
469 let object = lib3mf_core::model::Object {
470 id: resource_id,
471 object_type: lib3mf_core::model::ObjectType::Model,
472 name: object_name,
473 part_number: None,
474 uuid: None,
475 pid: None,
476 pindex: None,
477 thumbnail: None,
478 geometry: lib3mf_core::model::Geometry::Mesh(mesh),
479 };
480
481 let _ = model.resources.add_object(object);
482
483 model.build.items.push(BuildItem {
484 object_id: resource_id,
485 transform: glam::Mat4::IDENTITY,
486 part_number: None,
487 uuid: None,
488 path: None,
489 printable: None,
490 });
491}
492
493fn compute_face_normal(v1: glam::Vec3, v2: glam::Vec3, v3: glam::Vec3) -> glam::Vec3 {
497 let edge1 = v2 - v1;
498 let edge2 = v3 - v1;
499 edge1.cross(edge2).normalize_or_zero()
500}
501
502pub struct BinaryStlExporter;
512
513impl BinaryStlExporter {
514 pub fn write<W: Write>(model: &Model, mut writer: W) -> Result<()> {
562 let mut triangles: Vec<(glam::Vec3, glam::Vec3, glam::Vec3)> = Vec::new(); for item in &model.build.items {
566 #[allow(clippy::collapsible_if)]
567 if let Some(object) = model.resources.get_object(item.object_id) {
568 if let lib3mf_core::model::Geometry::Mesh(mesh) = &object.geometry {
569 let transform = item.transform;
570
571 for tri in &mesh.triangles {
572 let v1_local = mesh.vertices[tri.v1 as usize];
573 let v2_local = mesh.vertices[tri.v2 as usize];
574 let v3_local = mesh.vertices[tri.v3 as usize];
575
576 let v1 = transform
577 .transform_point3(glam::Vec3::new(v1_local.x, v1_local.y, v1_local.z));
578 let v2 = transform
579 .transform_point3(glam::Vec3::new(v2_local.x, v2_local.y, v2_local.z));
580 let v3 = transform
581 .transform_point3(glam::Vec3::new(v3_local.x, v3_local.y, v3_local.z));
582
583 triangles.push((v1, v2, v3));
584 }
585 }
586 }
587 }
588
589 let header = [0u8; 80];
591 writer.write_all(&header).map_err(Lib3mfError::Io)?;
592
593 writer
595 .write_u32::<LittleEndian>(triangles.len() as u32)
596 .map_err(Lib3mfError::Io)?;
597
598 for (v1, v2, v3) in triangles {
600 writer
602 .write_f32::<LittleEndian>(0.0)
603 .map_err(Lib3mfError::Io)?;
604 writer
605 .write_f32::<LittleEndian>(0.0)
606 .map_err(Lib3mfError::Io)?;
607 writer
608 .write_f32::<LittleEndian>(0.0)
609 .map_err(Lib3mfError::Io)?;
610
611 writer
613 .write_f32::<LittleEndian>(v1.x)
614 .map_err(Lib3mfError::Io)?;
615 writer
616 .write_f32::<LittleEndian>(v1.y)
617 .map_err(Lib3mfError::Io)?;
618 writer
619 .write_f32::<LittleEndian>(v1.z)
620 .map_err(Lib3mfError::Io)?;
621
622 writer
624 .write_f32::<LittleEndian>(v2.x)
625 .map_err(Lib3mfError::Io)?;
626 writer
627 .write_f32::<LittleEndian>(v2.y)
628 .map_err(Lib3mfError::Io)?;
629 writer
630 .write_f32::<LittleEndian>(v2.z)
631 .map_err(Lib3mfError::Io)?;
632
633 writer
635 .write_f32::<LittleEndian>(v3.x)
636 .map_err(Lib3mfError::Io)?;
637 writer
638 .write_f32::<LittleEndian>(v3.y)
639 .map_err(Lib3mfError::Io)?;
640 writer
641 .write_f32::<LittleEndian>(v3.z)
642 .map_err(Lib3mfError::Io)?;
643
644 writer
646 .write_u16::<LittleEndian>(0)
647 .map_err(Lib3mfError::Io)?;
648 }
649
650 Ok(())
651 }
652
653 pub fn write_with_resolver<W: Write, A: lib3mf_core::archive::ArchiveReader>(
707 model: &Model,
708 mut resolver: lib3mf_core::model::resolver::PartResolver<A>,
709 mut writer: W,
710 ) -> Result<()> {
711 let mut triangles: Vec<(glam::Vec3, glam::Vec3, glam::Vec3)> = Vec::new();
713
714 for item in &model.build.items {
715 collect_triangles(
716 &mut resolver,
717 item.object_id,
718 item.transform,
719 None, &mut triangles,
721 )?;
722 }
723
724 let header = [0u8; 80];
726 writer.write_all(&header).map_err(Lib3mfError::Io)?;
727
728 writer
730 .write_u32::<LittleEndian>(triangles.len() as u32)
731 .map_err(Lib3mfError::Io)?;
732
733 for (v1, v2, v3) in triangles {
735 writer
737 .write_f32::<LittleEndian>(0.0)
738 .map_err(Lib3mfError::Io)?;
739 writer
740 .write_f32::<LittleEndian>(0.0)
741 .map_err(Lib3mfError::Io)?;
742 writer
743 .write_f32::<LittleEndian>(0.0)
744 .map_err(Lib3mfError::Io)?;
745
746 writer
748 .write_f32::<LittleEndian>(v1.x)
749 .map_err(Lib3mfError::Io)?;
750 writer
751 .write_f32::<LittleEndian>(v1.y)
752 .map_err(Lib3mfError::Io)?;
753 writer
754 .write_f32::<LittleEndian>(v1.z)
755 .map_err(Lib3mfError::Io)?;
756
757 writer
759 .write_f32::<LittleEndian>(v2.x)
760 .map_err(Lib3mfError::Io)?;
761 writer
762 .write_f32::<LittleEndian>(v2.y)
763 .map_err(Lib3mfError::Io)?;
764 writer
765 .write_f32::<LittleEndian>(v2.z)
766 .map_err(Lib3mfError::Io)?;
767
768 writer
770 .write_f32::<LittleEndian>(v3.x)
771 .map_err(Lib3mfError::Io)?;
772 writer
773 .write_f32::<LittleEndian>(v3.y)
774 .map_err(Lib3mfError::Io)?;
775 writer
776 .write_f32::<LittleEndian>(v3.z)
777 .map_err(Lib3mfError::Io)?;
778
779 writer
781 .write_u16::<LittleEndian>(0)
782 .map_err(Lib3mfError::Io)?;
783 }
784
785 Ok(())
786 }
787}
788
789pub struct AsciiStlExporter;
796
797impl AsciiStlExporter {
798 pub fn write<W: Write>(model: &Model, mut writer: W) -> Result<()> {
844 for item in &model.build.items {
845 #[allow(clippy::collapsible_if)]
846 if let Some(object) = model.resources.get_object(item.object_id) {
847 if let lib3mf_core::model::Geometry::Mesh(mesh) = &object.geometry {
848 let name = object.name.as_deref().unwrap_or("");
849 let transform = item.transform;
850
851 writeln!(writer, "solid {name}").map_err(Lib3mfError::Io)?;
852
853 for tri in &mesh.triangles {
854 let v1_local = mesh.vertices[tri.v1 as usize];
855 let v2_local = mesh.vertices[tri.v2 as usize];
856 let v3_local = mesh.vertices[tri.v3 as usize];
857
858 let v1 = transform
859 .transform_point3(glam::Vec3::new(v1_local.x, v1_local.y, v1_local.z));
860 let v2 = transform
861 .transform_point3(glam::Vec3::new(v2_local.x, v2_local.y, v2_local.z));
862 let v3 = transform
863 .transform_point3(glam::Vec3::new(v3_local.x, v3_local.y, v3_local.z));
864
865 let normal = compute_face_normal(v1, v2, v3);
866
867 writeln!(
868 writer,
869 " facet normal {:.6e} {:.6e} {:.6e}",
870 normal.x, normal.y, normal.z
871 )
872 .map_err(Lib3mfError::Io)?;
873 writeln!(writer, " outer loop").map_err(Lib3mfError::Io)?;
874 writeln!(writer, " vertex {:.6} {:.6} {:.6}", v1.x, v1.y, v1.z)
875 .map_err(Lib3mfError::Io)?;
876 writeln!(writer, " vertex {:.6} {:.6} {:.6}", v2.x, v2.y, v2.z)
877 .map_err(Lib3mfError::Io)?;
878 writeln!(writer, " vertex {:.6} {:.6} {:.6}", v3.x, v3.y, v3.z)
879 .map_err(Lib3mfError::Io)?;
880 writeln!(writer, " endloop").map_err(Lib3mfError::Io)?;
881 writeln!(writer, " endfacet").map_err(Lib3mfError::Io)?;
882 }
883
884 writeln!(writer, "endsolid {name}").map_err(Lib3mfError::Io)?;
885 }
886 }
887 }
888 Ok(())
889 }
890
891 pub fn write_with_resolver<W: Write, A: lib3mf_core::archive::ArchiveReader>(
914 model: &Model,
915 mut resolver: lib3mf_core::model::resolver::PartResolver<A>,
916 mut writer: W,
917 ) -> Result<()> {
918 let mut triangles: Vec<(glam::Vec3, glam::Vec3, glam::Vec3)> = Vec::new();
920
921 for item in &model.build.items {
922 collect_triangles(
923 &mut resolver,
924 item.object_id,
925 item.transform,
926 None,
927 &mut triangles,
928 )?;
929 }
930
931 writeln!(writer, "solid ").map_err(Lib3mfError::Io)?;
933
934 for (v1, v2, v3) in triangles {
935 let normal = compute_face_normal(v1, v2, v3);
936
937 writeln!(
938 writer,
939 " facet normal {:.6e} {:.6e} {:.6e}",
940 normal.x, normal.y, normal.z
941 )
942 .map_err(Lib3mfError::Io)?;
943 writeln!(writer, " outer loop").map_err(Lib3mfError::Io)?;
944 writeln!(writer, " vertex {:.6} {:.6} {:.6}", v1.x, v1.y, v1.z)
945 .map_err(Lib3mfError::Io)?;
946 writeln!(writer, " vertex {:.6} {:.6} {:.6}", v2.x, v2.y, v2.z)
947 .map_err(Lib3mfError::Io)?;
948 writeln!(writer, " vertex {:.6} {:.6} {:.6}", v3.x, v3.y, v3.z)
949 .map_err(Lib3mfError::Io)?;
950 writeln!(writer, " endloop").map_err(Lib3mfError::Io)?;
951 writeln!(writer, " endfacet").map_err(Lib3mfError::Io)?;
952 }
953
954 writeln!(writer, "endsolid ").map_err(Lib3mfError::Io)?;
955
956 Ok(())
957 }
958}
959
960fn collect_triangles<A: lib3mf_core::archive::ArchiveReader>(
961 resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
962 object_id: ResourceId,
963 transform: glam::Mat4,
964 path: Option<&str>,
965 triangles: &mut Vec<(glam::Vec3, glam::Vec3, glam::Vec3)>,
966) -> Result<()> {
967 let geometry = {
976 let res = resolver.resolve_object(object_id, path)?;
977 if let Some((_, obj)) = res {
978 Some(obj.geometry.clone()) } else {
980 None
981 }
982 };
983
984 if let Some(geo) = geometry {
985 match geo {
986 lib3mf_core::model::Geometry::Mesh(mesh) => {
987 for tri in &mesh.triangles {
988 let v1_local = mesh.vertices[tri.v1 as usize];
989 let v2_local = mesh.vertices[tri.v2 as usize];
990 let v3_local = mesh.vertices[tri.v3 as usize];
991
992 let v1 = transform
993 .transform_point3(glam::Vec3::new(v1_local.x, v1_local.y, v1_local.z));
994 let v2 = transform
995 .transform_point3(glam::Vec3::new(v2_local.x, v2_local.y, v2_local.z));
996 let v3 = transform
997 .transform_point3(glam::Vec3::new(v3_local.x, v3_local.y, v3_local.z));
998
999 triangles.push((v1, v2, v3));
1000 }
1001 }
1002 lib3mf_core::model::Geometry::Components(comps) => {
1003 for comp in comps.components {
1004 let new_transform = transform * comp.transform;
1005 let next_path_store = comp.path.clone();
1006 let next_path = next_path_store.as_deref().or(path);
1007
1008 collect_triangles(
1009 resolver,
1010 comp.object_id,
1011 new_transform,
1012 next_path,
1013 triangles,
1014 )?;
1015 }
1016 }
1017 _ => {}
1018 }
1019 }
1020
1021 Ok(())
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026 use super::*;
1027 use std::io::Cursor;
1028
1029 fn make_binary_stl(
1033 header: &[u8; 80],
1034 triangles: &[(f32, f32, f32, f32, f32, f32, f32, f32, f32)],
1035 ) -> Vec<u8> {
1036 use byteorder::{LittleEndian, WriteBytesExt};
1038 let mut buf = Vec::new();
1039 buf.extend_from_slice(header);
1040 buf.write_u32::<LittleEndian>(triangles.len() as u32)
1041 .unwrap();
1042 for &(v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z) in triangles {
1043 buf.write_f32::<LittleEndian>(0.0).unwrap();
1045 buf.write_f32::<LittleEndian>(0.0).unwrap();
1046 buf.write_f32::<LittleEndian>(0.0).unwrap();
1047 buf.write_f32::<LittleEndian>(v1x).unwrap();
1049 buf.write_f32::<LittleEndian>(v1y).unwrap();
1050 buf.write_f32::<LittleEndian>(v1z).unwrap();
1051 buf.write_f32::<LittleEndian>(v2x).unwrap();
1053 buf.write_f32::<LittleEndian>(v2y).unwrap();
1054 buf.write_f32::<LittleEndian>(v2z).unwrap();
1055 buf.write_f32::<LittleEndian>(v3x).unwrap();
1057 buf.write_f32::<LittleEndian>(v3y).unwrap();
1058 buf.write_f32::<LittleEndian>(v3z).unwrap();
1059 buf.write_u16::<LittleEndian>(0).unwrap();
1061 }
1062 buf
1063 }
1064
1065 fn make_simple_model(
1067 vertices: Vec<(f32, f32, f32)>,
1068 triangles: Vec<(u32, u32, u32)>,
1069 name: Option<&str>,
1070 ) -> Model {
1071 use lib3mf_core::model::{Geometry, Object, ObjectType};
1072
1073 let mut mesh = Mesh::default();
1074 for (x, y, z) in vertices {
1075 mesh.vertices.push(Vertex { x, y, z });
1076 }
1077 for (v1, v2, v3) in triangles {
1078 mesh.triangles.push(Triangle {
1079 v1,
1080 v2,
1081 v3,
1082 ..Default::default()
1083 });
1084 }
1085
1086 let resource_id = ResourceId(1);
1087 let object = Object {
1088 id: resource_id,
1089 object_type: ObjectType::Model,
1090 name: name.map(|s| s.to_string()),
1091 part_number: None,
1092 uuid: None,
1093 pid: None,
1094 pindex: None,
1095 thumbnail: None,
1096 geometry: Geometry::Mesh(mesh),
1097 };
1098
1099 let mut model = Model::default();
1100 let _ = model.resources.add_object(object);
1101 model.build.items.push(BuildItem {
1102 object_id: resource_id,
1103 transform: glam::Mat4::IDENTITY,
1104 part_number: None,
1105 uuid: None,
1106 path: None,
1107 printable: None,
1108 });
1109 model
1110 }
1111
1112 #[test]
1115 fn test_detect_binary_format() {
1116 let header = [0u8; 80];
1117 let tris = vec![(0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0)];
1118 let data = make_binary_stl(&header, &tris);
1119 let mut cursor = Cursor::new(data);
1120 let fmt = detect_stl_format(&mut cursor).expect("detect should succeed");
1121 assert_eq!(fmt, StlFormat::Binary);
1122 }
1123
1124 #[test]
1127 fn test_detect_ascii_format() {
1128 let ascii = "solid test\nendsolid test\n";
1129 let mut cursor = Cursor::new(ascii.as_bytes().to_vec());
1130 let fmt = detect_stl_format(&mut cursor).expect("detect should succeed");
1131 assert_eq!(fmt, StlFormat::Ascii);
1132 }
1133
1134 #[test]
1137 fn test_detect_binary_with_solid_header() {
1138 let mut header = [0u8; 80];
1140 let solid_bytes = b"solid binary_test_header";
1141 header[..solid_bytes.len()].copy_from_slice(solid_bytes);
1142
1143 let tris = vec![(0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0)];
1144 let data = make_binary_stl(&header, &tris);
1145
1146 assert_eq!(data.len(), 134);
1148
1149 let mut cursor = Cursor::new(data);
1150 let fmt = detect_stl_format(&mut cursor).expect("detect should succeed");
1151 assert_eq!(
1152 fmt,
1153 StlFormat::Binary,
1154 "Binary STL with 'solid' in header must be detected as Binary"
1155 );
1156 }
1157
1158 #[test]
1161 fn test_read_ascii_simple_triangle() {
1162 let ascii = "\
1163solid triangle
1164 facet normal 0 0 1
1165 outer loop
1166 vertex 0 0 0
1167 vertex 1 0 0
1168 vertex 0 1 0
1169 endloop
1170 endfacet
1171endsolid triangle
1172";
1173 let model = StlImporter::read_ascii(Cursor::new(ascii)).expect("parse should succeed");
1174
1175 assert_eq!(model.build.items.len(), 1);
1176 let obj = model
1177 .resources
1178 .get_object(ResourceId(1))
1179 .expect("object 1 should exist");
1180 if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
1181 assert_eq!(mesh.vertices.len(), 3, "should have 3 unique vertices");
1182 assert_eq!(mesh.triangles.len(), 1, "should have 1 triangle");
1183 assert!((mesh.vertices[0].x - 0.0).abs() < 1e-6);
1185 assert!((mesh.vertices[1].x - 1.0).abs() < 1e-6);
1186 assert!((mesh.vertices[2].y - 1.0).abs() < 1e-6);
1187 } else {
1188 panic!("expected Mesh geometry");
1189 }
1190 }
1191
1192 #[test]
1195 fn test_read_ascii_case_insensitive() {
1196 let ascii = "\
1197SOLID uppercase
1198 FACET NORMAL 0 0 1
1199 OUTER LOOP
1200 VERTEX 0 0 0
1201 VERTEX 1 0 0
1202 VERTEX 0 1 0
1203 ENDLOOP
1204 ENDFACET
1205ENDSOLID uppercase
1206";
1207 let model = StlImporter::read_ascii(Cursor::new(ascii)).expect("parse should succeed");
1208 assert_eq!(model.build.items.len(), 1);
1209 let obj = model.resources.get_object(ResourceId(1)).unwrap();
1210 if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
1211 assert_eq!(mesh.triangles.len(), 1);
1212 } else {
1213 panic!("expected Mesh geometry");
1214 }
1215 }
1216
1217 #[test]
1220 fn test_read_ascii_multi_solid() {
1221 let ascii = "\
1222solid part_a
1223 facet normal 0 0 1
1224 outer loop
1225 vertex 0 0 0
1226 vertex 1 0 0
1227 vertex 0 1 0
1228 endloop
1229 endfacet
1230endsolid part_a
1231solid part_b
1232 facet normal 0 0 -1
1233 outer loop
1234 vertex 0 0 1
1235 vertex 1 0 1
1236 vertex 0 1 1
1237 endloop
1238 endfacet
1239endsolid part_b
1240";
1241 let model = StlImporter::read_ascii(Cursor::new(ascii)).expect("parse should succeed");
1242 assert_eq!(model.build.items.len(), 2, "should have 2 build items");
1243
1244 let obj1 = model.resources.get_object(ResourceId(1)).expect("object 1");
1245 let obj2 = model.resources.get_object(ResourceId(2)).expect("object 2");
1246
1247 if let lib3mf_core::model::Geometry::Mesh(m) = &obj1.geometry {
1248 assert_eq!(m.triangles.len(), 1);
1249 }
1250 if let lib3mf_core::model::Geometry::Mesh(m) = &obj2.geometry {
1251 assert_eq!(m.triangles.len(), 1);
1252 }
1253 }
1254
1255 #[test]
1258 fn test_read_ascii_solid_name_with_spaces() {
1259 let ascii = "\
1260solid My Cool Part
1261 facet normal 0 0 1
1262 outer loop
1263 vertex 0 0 0
1264 vertex 1 0 0
1265 vertex 0 1 0
1266 endloop
1267 endfacet
1268endsolid My Cool Part
1269";
1270 let model = StlImporter::read_ascii(Cursor::new(ascii)).expect("parse should succeed");
1271 let obj = model.resources.get_object(ResourceId(1)).expect("object 1");
1272 assert_eq!(obj.name, Some("My Cool Part".to_string()));
1273 }
1274
1275 #[test]
1278 fn test_read_ascii_vertex_dedup() {
1279 let ascii = "\
1281solid dedup
1282 facet normal 0 0 1
1283 outer loop
1284 vertex 0 0 0
1285 vertex 1 0 0
1286 vertex 0 1 0
1287 endloop
1288 endfacet
1289 facet normal 0 0 1
1290 outer loop
1291 vertex 1 0 0
1292 vertex 1 1 0
1293 vertex 0 1 0
1294 endloop
1295 endfacet
1296endsolid dedup
1297";
1298 let model = StlImporter::read_ascii(Cursor::new(ascii)).expect("parse should succeed");
1299 let obj = model.resources.get_object(ResourceId(1)).expect("object 1");
1300 if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
1301 assert_eq!(mesh.triangles.len(), 2);
1302 assert_eq!(
1304 mesh.vertices.len(),
1305 4,
1306 "shared vertices should be deduplicated"
1307 );
1308 } else {
1309 panic!("expected Mesh");
1310 }
1311 }
1312
1313 #[test]
1316 fn test_read_ascii_mismatched_endsolid() {
1317 let ascii = "\
1318solid foo
1319 facet normal 0 0 1
1320 outer loop
1321 vertex 0 0 0
1322 vertex 1 0 0
1323 vertex 0 1 0
1324 endloop
1325 endfacet
1326endsolid bar
1327";
1328 let model = StlImporter::read_ascii(Cursor::new(ascii)).expect("parse should succeed");
1329 assert_eq!(
1330 model.build.items.len(),
1331 1,
1332 "mismatched endsolid name should not cause error"
1333 );
1334 }
1335
1336 #[test]
1339 fn test_read_ascii_no_endsolid() {
1340 let ascii = "\
1341solid truncated
1342 facet normal 0 0 1
1343 outer loop
1344 vertex 0 0 0
1345 vertex 1 0 0
1346 vertex 0 1 0
1347 endloop
1348 endfacet
1349";
1350 let model = StlImporter::read_ascii(Cursor::new(ascii)).expect("parse should succeed");
1351 assert_eq!(
1352 model.build.items.len(),
1353 1,
1354 "truncated file should still produce one object"
1355 );
1356 let obj = model.resources.get_object(ResourceId(1)).expect("object 1");
1357 if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
1358 assert_eq!(mesh.triangles.len(), 1);
1359 }
1360 }
1361
1362 #[test]
1365 fn test_write_ascii_simple() {
1366 let model = make_simple_model(
1368 vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)],
1369 vec![(0, 1, 2)],
1370 Some("test"),
1371 );
1372
1373 let mut output = Vec::new();
1374 AsciiStlExporter::write(&model, &mut output).expect("write should succeed");
1375 let text = String::from_utf8(output).expect("valid UTF-8");
1376
1377 assert!(
1378 text.contains("solid test"),
1379 "should contain solid keyword with name"
1380 );
1381 assert!(text.contains("facet normal"), "should contain facet normal");
1382 assert!(text.contains("outer loop"), "should contain outer loop");
1383 assert!(text.contains("vertex"), "should contain vertex lines");
1384 assert!(text.contains("endloop"), "should contain endloop");
1385 assert!(text.contains("endfacet"), "should contain endfacet");
1386 assert!(
1387 text.contains("endsolid test"),
1388 "should contain endsolid keyword with name"
1389 );
1390
1391 let has_nonzero_normal = text
1394 .lines()
1395 .filter(|l| l.trim().starts_with("facet normal"))
1396 .any(|l| {
1397 let parts: Vec<&str> = l.split_whitespace().collect();
1398 if parts.len() >= 5 {
1399 let nz: f64 = parts[4].parse().unwrap_or(0.0);
1400 nz.abs() > 0.5
1401 } else {
1402 false
1403 }
1404 });
1405 assert!(
1406 has_nonzero_normal,
1407 "normal should be non-zero for valid triangle"
1408 );
1409 }
1410
1411 #[test]
1414 fn test_write_ascii_degenerate_normal() {
1415 let model = make_simple_model(
1417 vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (2.0, 0.0, 0.0)],
1418 vec![(0, 1, 2)],
1419 None,
1420 );
1421
1422 let mut output = Vec::new();
1423 AsciiStlExporter::write(&model, &mut output).expect("write should succeed");
1424 let text = String::from_utf8(output).expect("valid UTF-8");
1425
1426 let normal_line = text
1427 .lines()
1428 .find(|l| l.trim().starts_with("facet normal"))
1429 .expect("should have facet normal line");
1430
1431 let parts: Vec<&str> = normal_line.split_whitespace().collect();
1432 assert!(parts.len() >= 5, "facet normal line should have 5 parts");
1433 let nx: f64 = parts[2].parse().unwrap_or(f64::NAN);
1434 let ny: f64 = parts[3].parse().unwrap_or(f64::NAN);
1435 let nz: f64 = parts[4].parse().unwrap_or(f64::NAN);
1436 assert!(
1437 nx.abs() < 1e-6 && ny.abs() < 1e-6 && nz.abs() < 1e-6,
1438 "degenerate triangle normal should be (0,0,0), got ({nx}, {ny}, {nz})"
1439 );
1440 }
1441
1442 #[test]
1445 fn test_write_ascii_object_name() {
1446 let model = make_simple_model(
1447 vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)],
1448 vec![(0, 1, 2)],
1449 Some("MyPart"),
1450 );
1451
1452 let mut output = Vec::new();
1453 AsciiStlExporter::write(&model, &mut output).expect("write should succeed");
1454 let text = String::from_utf8(output).expect("valid UTF-8");
1455
1456 let first_line = text.lines().next().expect("should have lines");
1457 assert_eq!(
1458 first_line, "solid MyPart",
1459 "first line should be 'solid MyPart'"
1460 );
1461
1462 let last_line = text
1463 .lines()
1464 .filter(|l| !l.is_empty())
1465 .last()
1466 .expect("should have lines");
1467 assert_eq!(
1468 last_line, "endsolid MyPart",
1469 "last line should be 'endsolid MyPart'"
1470 );
1471 }
1472
1473 #[test]
1476 fn test_roundtrip_ascii() {
1477 let model = make_simple_model(
1479 vec![
1480 (0.0, 0.0, 0.0),
1481 (1.0, 0.0, 0.0),
1482 (1.0, 1.0, 0.0),
1483 (0.0, 1.0, 0.0),
1484 ],
1485 vec![(0, 1, 2), (0, 2, 3)],
1486 Some("RoundtripTest"),
1487 );
1488
1489 let mut buf1 = Vec::new();
1491 AsciiStlExporter::write(&model, &mut buf1).expect("first write should succeed");
1492
1493 let model2 =
1495 StlImporter::read_ascii(Cursor::new(&buf1)).expect("first re-read should succeed");
1496
1497 let mut buf2 = Vec::new();
1499 AsciiStlExporter::write(&model2, &mut buf2).expect("second write should succeed");
1500
1501 let model3 =
1503 StlImporter::read_ascii(Cursor::new(&buf2)).expect("second re-read should succeed");
1504
1505 let get_mesh_info = |m: &Model| -> (usize, usize, Vec<(f32, f32, f32)>) {
1507 let obj = m.resources.get_object(ResourceId(1)).expect("object 1");
1508 if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
1509 let verts: Vec<(f32, f32, f32)> =
1510 mesh.vertices.iter().map(|v| (v.x, v.y, v.z)).collect();
1511 (mesh.vertices.len(), mesh.triangles.len(), verts)
1512 } else {
1513 panic!("expected Mesh");
1514 }
1515 };
1516
1517 let (v_count2, t_count2, verts2) = get_mesh_info(&model2);
1518 let (v_count3, t_count3, verts3) = get_mesh_info(&model3);
1519
1520 assert_eq!(
1521 v_count2, v_count3,
1522 "vertex count must be stable across roundtrips"
1523 );
1524 assert_eq!(
1525 t_count2, t_count3,
1526 "triangle count must be stable across roundtrips"
1527 );
1528
1529 for (i, (&(x2, y2, z2), &(x3, y3, z3))) in verts2.iter().zip(verts3.iter()).enumerate() {
1531 assert!(
1532 (x2 - x3).abs() < 1e-5 && (y2 - y3).abs() < 1e-5 && (z2 - z3).abs() < 1e-5,
1533 "vertex {i} position mismatch: ({x2},{y2},{z2}) vs ({x3},{y3},{z3})"
1534 );
1535 }
1536
1537 assert_eq!(v_count2, 4, "should have 4 vertices");
1539 assert_eq!(t_count2, 2, "should have 2 triangles");
1540 }
1541
1542 #[test]
1545 fn test_write_binary_simple() {
1546 use byteorder::{LittleEndian, ReadBytesExt};
1547
1548 let model = make_simple_model(
1550 vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)],
1551 vec![(0, 1, 2)],
1552 None,
1553 );
1554
1555 let mut buf = Vec::new();
1556 BinaryStlExporter::write(&model, Cursor::new(&mut buf)).expect("write should succeed");
1557
1558 assert_eq!(
1560 buf.len(),
1561 134,
1562 "binary STL size should be 134 bytes for 1 triangle"
1563 );
1564
1565 let mut count_bytes = Cursor::new(&buf[80..84]);
1567 let tri_count = count_bytes.read_u32::<LittleEndian>().unwrap();
1568 assert_eq!(tri_count, 1, "triangle count should be 1");
1569
1570 let mut tri_cursor = Cursor::new(&buf[96..]);
1577
1578 let v1x = tri_cursor.read_f32::<LittleEndian>().unwrap();
1580 let v1y = tri_cursor.read_f32::<LittleEndian>().unwrap();
1581 let v1z = tri_cursor.read_f32::<LittleEndian>().unwrap();
1582 assert!((v1x - 0.0).abs() < 1e-6, "v1.x should be 0.0, got {v1x}");
1583 assert!((v1y - 0.0).abs() < 1e-6, "v1.y should be 0.0, got {v1y}");
1584 assert!((v1z - 0.0).abs() < 1e-6, "v1.z should be 0.0, got {v1z}");
1585
1586 let v2x = tri_cursor.read_f32::<LittleEndian>().unwrap();
1588 let v2y = tri_cursor.read_f32::<LittleEndian>().unwrap();
1589 let v2z = tri_cursor.read_f32::<LittleEndian>().unwrap();
1590 assert!((v2x - 1.0).abs() < 1e-6, "v2.x should be 1.0, got {v2x}");
1591 assert!((v2y - 0.0).abs() < 1e-6, "v2.y should be 0.0, got {v2y}");
1592 assert!((v2z - 0.0).abs() < 1e-6, "v2.z should be 0.0, got {v2z}");
1593
1594 let v3x = tri_cursor.read_f32::<LittleEndian>().unwrap();
1596 let v3y = tri_cursor.read_f32::<LittleEndian>().unwrap();
1597 let v3z = tri_cursor.read_f32::<LittleEndian>().unwrap();
1598 assert!((v3x - 0.0).abs() < 1e-6, "v3.x should be 0.0, got {v3x}");
1599 assert!((v3y - 1.0).abs() < 1e-6, "v3.y should be 1.0, got {v3y}");
1600 assert!((v3z - 0.0).abs() < 1e-6, "v3.z should be 0.0, got {v3z}");
1601 }
1602
1603 #[test]
1606 fn test_roundtrip_binary() {
1607 let model = make_simple_model(
1609 vec![
1610 (0.0, 0.0, 0.0),
1611 (1.0, 0.0, 0.0),
1612 (1.0, 1.0, 0.0),
1613 (0.0, 1.0, 0.0),
1614 ],
1615 vec![(0, 1, 2), (0, 2, 3)],
1616 None,
1617 );
1618
1619 let mut buf = Vec::new();
1621 BinaryStlExporter::write(&model, Cursor::new(&mut buf)).expect("write should succeed");
1622
1623 let model2 =
1625 StlImporter::read(Cursor::new(buf)).expect("binary roundtrip read should succeed");
1626
1627 let obj2 = model2
1629 .resources
1630 .get_object(ResourceId(1))
1631 .expect("object 1");
1632 if let lib3mf_core::model::Geometry::Mesh(mesh2) = &obj2.geometry {
1633 assert_eq!(
1634 mesh2.triangles.len(),
1635 2,
1636 "read-back should have 2 triangles"
1637 );
1638 } else {
1639 panic!("expected Mesh geometry");
1640 }
1641 }
1642
1643 #[test]
1646 fn test_write_binary_multi_object() {
1647 use byteorder::{LittleEndian, ReadBytesExt};
1648 use lib3mf_core::model::{Geometry, Object, ObjectType};
1649
1650 let mut mesh1 = Mesh::default();
1652 mesh1.vertices.push(Vertex {
1653 x: 0.0,
1654 y: 0.0,
1655 z: 0.0,
1656 });
1657 mesh1.vertices.push(Vertex {
1658 x: 1.0,
1659 y: 0.0,
1660 z: 0.0,
1661 });
1662 mesh1.vertices.push(Vertex {
1663 x: 0.0,
1664 y: 1.0,
1665 z: 0.0,
1666 });
1667 mesh1.triangles.push(Triangle {
1668 v1: 0,
1669 v2: 1,
1670 v3: 2,
1671 ..Default::default()
1672 });
1673
1674 let obj1 = Object {
1675 id: ResourceId(1),
1676 object_type: ObjectType::Model,
1677 name: None,
1678 part_number: None,
1679 uuid: None,
1680 pid: None,
1681 pindex: None,
1682 thumbnail: None,
1683 geometry: Geometry::Mesh(mesh1),
1684 };
1685
1686 let mut mesh2 = Mesh::default();
1688 mesh2.vertices.push(Vertex {
1689 x: 0.0,
1690 y: 0.0,
1691 z: 1.0,
1692 });
1693 mesh2.vertices.push(Vertex {
1694 x: 1.0,
1695 y: 0.0,
1696 z: 1.0,
1697 });
1698 mesh2.vertices.push(Vertex {
1699 x: 1.0,
1700 y: 1.0,
1701 z: 1.0,
1702 });
1703 mesh2.vertices.push(Vertex {
1704 x: 0.0,
1705 y: 1.0,
1706 z: 1.0,
1707 });
1708 mesh2.triangles.push(Triangle {
1709 v1: 0,
1710 v2: 1,
1711 v3: 2,
1712 ..Default::default()
1713 });
1714 mesh2.triangles.push(Triangle {
1715 v1: 0,
1716 v2: 2,
1717 v3: 3,
1718 ..Default::default()
1719 });
1720
1721 let obj2 = Object {
1722 id: ResourceId(2),
1723 object_type: ObjectType::Model,
1724 name: None,
1725 part_number: None,
1726 uuid: None,
1727 pid: None,
1728 pindex: None,
1729 thumbnail: None,
1730 geometry: Geometry::Mesh(mesh2),
1731 };
1732
1733 let mut model = Model::default();
1734 let _ = model.resources.add_object(obj1);
1735 let _ = model.resources.add_object(obj2);
1736 model.build.items.push(BuildItem {
1737 object_id: ResourceId(1),
1738 transform: glam::Mat4::IDENTITY,
1739 part_number: None,
1740 uuid: None,
1741 path: None,
1742 printable: None,
1743 });
1744 model.build.items.push(BuildItem {
1745 object_id: ResourceId(2),
1746 transform: glam::Mat4::IDENTITY,
1747 part_number: None,
1748 uuid: None,
1749 path: None,
1750 printable: None,
1751 });
1752
1753 let mut buf = Vec::new();
1754 BinaryStlExporter::write(&model, Cursor::new(&mut buf)).expect("write should succeed");
1755
1756 assert_eq!(
1758 buf.len(),
1759 234,
1760 "binary STL size should be 234 bytes for 3 triangles"
1761 );
1762
1763 let mut count_cursor = Cursor::new(&buf[80..84]);
1765 let tri_count = count_cursor.read_u32::<LittleEndian>().unwrap();
1766 assert_eq!(tri_count, 3, "combined triangle count should be 3 (1 + 2)");
1767 }
1768
1769 #[test]
1772 fn test_auto_detect_read_binary() {
1773 let header = [0u8; 80];
1774 let tris = vec![(0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0)];
1775 let data = make_binary_stl(&header, &tris);
1776 let cursor = Cursor::new(data);
1777 let model = StlImporter::read(cursor).expect("auto-detect binary should succeed");
1778
1779 assert_eq!(model.build.items.len(), 1);
1780 let obj = model.resources.get_object(ResourceId(1)).expect("object 1");
1781 if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
1782 assert_eq!(mesh.triangles.len(), 1, "should have 1 triangle");
1783 } else {
1784 panic!("expected Mesh");
1785 }
1786 }
1787
1788 #[test]
1791 fn test_auto_detect_read_ascii() {
1792 let ascii = "\
1793solid autotest
1794 facet normal 0 0 1
1795 outer loop
1796 vertex 0 0 0
1797 vertex 1 0 0
1798 vertex 0 1 0
1799 endloop
1800 endfacet
1801endsolid autotest
1802";
1803 let cursor = Cursor::new(ascii.as_bytes().to_vec());
1804 let model = StlImporter::read(cursor).expect("auto-detect ASCII should succeed");
1805
1806 assert_eq!(model.build.items.len(), 1);
1807 let obj = model.resources.get_object(ResourceId(1)).expect("object 1");
1808 assert_eq!(obj.name, Some("autotest".to_string()));
1809 if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
1810 assert_eq!(mesh.triangles.len(), 1, "should have 1 triangle");
1811 } else {
1812 panic!("expected Mesh");
1813 }
1814 }
1815}