1use std::fs::File;
27use std::io::{self, BufWriter, Write};
28use std::path::Path;
29
30use draco_core::draco_types::DataType;
31use draco_core::geometry_attribute::GeometryAttributeType;
32use draco_core::geometry_indices::FaceIndex;
33use draco_core::mesh::Mesh;
34
35use crate::traits::{PointCloudWriter, WriteToBytes, Writer};
36
37#[derive(Debug, Clone, Default)]
52pub struct ObjWriter {
53 positions: Vec<[f32; 3]>,
55 normals: Vec<[f32; 3]>,
57 texcoords: Vec<[f32; 2]>,
59 faces: Vec<ObjFace>,
61 groups: Vec<(String, usize)>,
63}
64
65#[derive(Debug, Clone)]
66struct ObjFace {
67 positions: [u32; 3],
68 texcoords: Option<[u32; 3]>,
69 normals: Option<[u32; 3]>,
70}
71
72impl ObjWriter {
73 pub fn new() -> Self {
75 Self::default()
76 }
77
78 pub fn add_points(&mut self, points: &[[f32; 3]]) {
80 self.positions.extend_from_slice(points);
81 }
82
83 pub fn add_point(&mut self, point: [f32; 3]) {
85 self.positions.push(point);
86 }
87
88 pub fn vertex_count(&self) -> usize {
90 self.positions.len()
91 }
92
93 pub fn face_count(&self) -> usize {
95 self.faces.len()
96 }
97
98 pub fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
100 let file = File::create(path)?;
101 let mut writer = BufWriter::new(file);
102 self.write_to(&mut writer)
103 }
104
105 pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
107 writeln!(writer, "# OBJ file generated by draco-io")?;
109 writeln!(writer, "# Vertices: {}", self.positions.len())?;
110 writeln!(writer, "# Faces: {}", self.faces.len())?;
111 writeln!(writer)?;
112
113 for [x, y, z] in &self.positions {
115 writeln!(writer, "v {:.6} {:.6} {:.6}", x, y, z)?;
116 }
117
118 if !self.texcoords.is_empty() {
120 writeln!(writer)?;
121 for [u, v] in &self.texcoords {
122 writeln!(writer, "vt {:.6} {:.6}", u, v)?;
123 }
124 }
125
126 if !self.normals.is_empty() {
128 writeln!(writer)?;
129 for [x, y, z] in &self.normals {
130 writeln!(writer, "vn {:.6} {:.6} {:.6}", x, y, z)?;
131 }
132 }
133
134 if !self.faces.is_empty() {
136 writeln!(writer)?;
137
138 let mut group_iter = self.groups.iter().peekable();
139 let has_texcoords = !self.texcoords.is_empty();
140 let has_normals = !self.normals.is_empty();
141
142 for (i, face) in self.faces.iter().enumerate() {
143 if let Some((name, start_idx)) = group_iter.peek() {
145 if i == *start_idx {
146 writeln!(writer, "o {}", name)?;
147 group_iter.next();
148 }
149 }
150
151 match (
152 has_texcoords.then_some(face.texcoords).flatten(),
153 has_normals.then_some(face.normals).flatten(),
154 ) {
155 (Some(texcoords), Some(normals)) => {
156 writeln!(
157 writer,
158 "f {}/{}/{} {}/{}/{} {}/{}/{}",
159 face.positions[0],
160 texcoords[0],
161 normals[0],
162 face.positions[1],
163 texcoords[1],
164 normals[1],
165 face.positions[2],
166 texcoords[2],
167 normals[2]
168 )?;
169 }
170 (Some(texcoords), None) => {
171 writeln!(
172 writer,
173 "f {}/{} {}/{} {}/{}",
174 face.positions[0],
175 texcoords[0],
176 face.positions[1],
177 texcoords[1],
178 face.positions[2],
179 texcoords[2]
180 )?;
181 }
182 (None, Some(normals)) => {
183 writeln!(
184 writer,
185 "f {}//{} {}//{} {}//{}",
186 face.positions[0],
187 normals[0],
188 face.positions[1],
189 normals[1],
190 face.positions[2],
191 normals[2]
192 )?;
193 }
194 (None, None) => {
195 writeln!(
196 writer,
197 "f {} {} {}",
198 face.positions[0], face.positions[1], face.positions[2]
199 )?;
200 }
201 }
202 }
203 }
204
205 Ok(())
206 }
207
208 pub fn write_to_vec(&self) -> io::Result<Vec<u8>> {
210 let mut out = Vec::new();
211 self.write_to(&mut out)?;
212 Ok(out)
213 }
214}
215
216fn read_float3(mesh: &Mesh, att_id: i32, point_idx: usize) -> [f32; 3] {
218 let att = mesh.attribute(att_id);
219 let byte_stride = att.byte_stride() as usize;
220 let buffer = att.buffer();
221 let mut bytes = [0u8; 12];
222 buffer.read(point_idx * byte_stride, &mut bytes);
223 [
224 f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
225 f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
226 f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
227 ]
228}
229
230fn read_float2(mesh: &Mesh, att_id: i32, point_idx: usize) -> [f32; 2] {
232 let att = mesh.attribute(att_id);
233 let byte_stride = att.byte_stride() as usize;
234 let buffer = att.buffer();
235 let mut bytes = [0u8; 8];
236 buffer.read(point_idx * byte_stride, &mut bytes);
237 [
238 f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
239 f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
240 ]
241}
242
243fn require_f32_components(mesh: &Mesh, att_id: i32, components: u8, label: &str) -> io::Result<()> {
244 let att = mesh.attribute(att_id);
245 if att.data_type() != DataType::Float32 || att.num_components() != components {
246 return Err(io::Error::new(
247 io::ErrorKind::InvalidInput,
248 format!("OBJ writer requires {label} attributes to be Float32x{components}"),
249 ));
250 }
251 Ok(())
252}
253
254impl Writer for ObjWriter {
259 fn new() -> Self {
260 Self::default()
261 }
262
263 fn add_mesh(&mut self, mesh: &Mesh, name: Option<&str>) -> io::Result<()> {
264 let vertex_offset = self.positions.len() as u32;
265 let normal_offset = self.normals.len() as u32;
266 let texcoord_offset = self.texcoords.len() as u32;
267 let face_start = self.faces.len();
268
269 if let Some(n) = name {
271 self.groups.push((n.to_string(), face_start));
272 }
273
274 let pos_att_id = mesh.named_attribute_id(GeometryAttributeType::Position);
276 if pos_att_id >= 0 {
277 require_f32_components(mesh, pos_att_id, 3, "position")?;
278 for i in 0..mesh.num_points() {
279 self.positions.push(read_float3(mesh, pos_att_id, i));
280 }
281 }
282
283 let normal_att_id = mesh.named_attribute_id(GeometryAttributeType::Normal);
285 let has_normals = normal_att_id >= 0;
286 if has_normals {
287 require_f32_components(mesh, normal_att_id, 3, "normal")?;
288 for i in 0..mesh.num_points() {
289 self.normals.push(read_float3(mesh, normal_att_id, i));
290 }
291 }
292
293 let texcoord_att_id = mesh.named_attribute_id(GeometryAttributeType::TexCoord);
295 let has_texcoords = texcoord_att_id >= 0;
296 if has_texcoords {
297 require_f32_components(mesh, texcoord_att_id, 2, "texcoord")?;
298 for i in 0..mesh.num_points() {
299 self.texcoords.push(read_float2(mesh, texcoord_att_id, i));
300 }
301 }
302
303 for i in 0..mesh.num_faces() as u32 {
305 let face = mesh.face(FaceIndex(i));
306 let positions = [
307 face[0].0 + vertex_offset + 1,
308 face[1].0 + vertex_offset + 1,
309 face[2].0 + vertex_offset + 1,
310 ];
311 let normals = has_normals.then(|| {
312 [
313 face[0].0 + normal_offset + 1,
314 face[1].0 + normal_offset + 1,
315 face[2].0 + normal_offset + 1,
316 ]
317 });
318 let texcoords = has_texcoords.then(|| {
319 [
320 face[0].0 + texcoord_offset + 1,
321 face[1].0 + texcoord_offset + 1,
322 face[2].0 + texcoord_offset + 1,
323 ]
324 });
325 self.faces.push(ObjFace {
326 positions,
327 texcoords,
328 normals,
329 });
330 }
331 Ok(())
332 }
333
334 fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
335 self.write(path)
336 }
337
338 fn vertex_count(&self) -> usize {
339 self.vertex_count()
340 }
341
342 fn face_count(&self) -> usize {
343 self.face_count()
344 }
345}
346
347impl PointCloudWriter for ObjWriter {
348 fn add_points(&mut self, points: &[[f32; 3]]) {
349 self.positions.extend_from_slice(points);
350 }
351
352 fn add_point(&mut self, point: [f32; 3]) {
353 self.positions.push(point);
354 }
355}
356
357impl WriteToBytes for ObjWriter {
358 fn write_to_vec(&self) -> io::Result<Vec<u8>> {
359 ObjWriter::write_to_vec(self)
360 }
361
362 fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
363 ObjWriter::write_to(self, writer)
364 }
365}
366
367pub fn write_obj_mesh<P: AsRef<Path>>(path: P, mesh: &Mesh) -> io::Result<()> {
375 let mut writer = ObjWriter::new();
376 Writer::add_mesh(&mut writer, mesh, None)?;
377 writer.write(path)
378}
379
380pub fn write_obj_positions<P: AsRef<Path>>(path: P, points: &[[f32; 3]]) -> io::Result<()> {
384 let mut writer = ObjWriter::new();
385 writer.add_points(points);
386 writer.write(path)
387}
388
389#[cfg(test)]
394mod tests {
395 use super::*;
396 use draco_core::draco_types::DataType;
397 use draco_core::geometry_attribute::PointAttribute;
398 use draco_core::geometry_indices::PointIndex;
399 use std::fs;
400 use std::io::{BufRead, BufReader};
401 use tempfile::NamedTempFile;
402
403 fn create_triangle_mesh() -> Mesh {
404 let mut mesh = Mesh::new();
405 let mut pos_att = PointAttribute::new();
406
407 pos_att.init(
408 GeometryAttributeType::Position,
409 3,
410 DataType::Float32,
411 false,
412 3,
413 );
414 let buffer = pos_att.buffer_mut();
415 let positions: [[f32; 3]; 3] = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
416 for (i, pos) in positions.iter().enumerate() {
417 let bytes: Vec<u8> = pos.iter().flat_map(|v| v.to_le_bytes()).collect();
418 buffer.write(i * 12, &bytes);
419 }
420 mesh.add_attribute(pos_att);
421
422 mesh.set_num_faces(1);
423 mesh.set_face(FaceIndex(0), [PointIndex(0), PointIndex(1), PointIndex(2)]);
424
425 mesh
426 }
427
428 fn add_f32_attribute(
429 mesh: &mut Mesh,
430 attribute_type: GeometryAttributeType,
431 components: u8,
432 values: &[f32],
433 ) {
434 let mut att = PointAttribute::new();
435 att.init(
436 attribute_type,
437 components,
438 DataType::Float32,
439 false,
440 values.len() / components as usize,
441 );
442 let bytes: Vec<u8> = values
443 .iter()
444 .flat_map(|component| component.to_le_bytes())
445 .collect();
446 att.buffer_mut().write(0, &bytes);
447 mesh.add_attribute(att);
448 }
449
450 #[test]
451 fn test_obj_writer_new() {
452 let writer = ObjWriter::new();
453 assert_eq!(writer.vertex_count(), 0);
454 assert_eq!(writer.face_count(), 0);
455 }
456
457 #[test]
458 fn test_obj_writer_add_mesh() {
459 let mesh = create_triangle_mesh();
460 let mut writer = ObjWriter::new();
461 Writer::add_mesh(&mut writer, &mesh, Some("Triangle")).unwrap();
462 assert_eq!(writer.vertex_count(), 3);
463 assert_eq!(writer.face_count(), 1);
464 }
465
466 #[test]
467 fn test_obj_writer_add_points() {
468 let mut writer = ObjWriter::new();
469 writer.add_points(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
470 assert_eq!(writer.vertex_count(), 2);
471 assert_eq!(writer.face_count(), 0);
472 }
473
474 #[test]
475 fn test_write_obj_positions() {
476 let points = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
477
478 let file = NamedTempFile::new().unwrap();
479 write_obj_positions(file.path(), &points).unwrap();
480
481 let content = fs::read_to_string(file.path()).unwrap();
482 assert!(content.contains("v 0.000000 0.000000 0.000000"));
483 assert!(content.contains("v 1.000000 0.000000 0.000000"));
484 assert!(content.contains("v 0.000000 1.000000 0.000000"));
485 }
486
487 #[test]
488 fn test_write_obj_mesh() {
489 let mesh = create_triangle_mesh();
490 let file = NamedTempFile::new().unwrap();
491 write_obj_mesh(file.path(), &mesh).unwrap();
492
493 let reader = BufReader::new(fs::File::open(file.path()).unwrap());
494 let lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
495
496 assert!(lines.iter().any(|l| l.starts_with("v ")));
498 assert!(lines.iter().any(|l| l == "f 1 2 3"));
499 }
500
501 #[test]
502 fn test_multiple_meshes() {
503 let mesh1 = create_triangle_mesh();
504 let mesh2 = create_triangle_mesh();
505
506 let mut writer = ObjWriter::new();
507 Writer::add_mesh(&mut writer, &mesh1, Some("Mesh1")).unwrap();
508 Writer::add_mesh(&mut writer, &mesh2, Some("Mesh2")).unwrap();
509
510 assert_eq!(writer.vertex_count(), 6);
511 assert_eq!(writer.face_count(), 2);
512
513 let file = NamedTempFile::new().unwrap();
514 writer.write(file.path()).unwrap();
515
516 let content = fs::read_to_string(file.path()).unwrap();
517 assert!(content.contains("o Mesh1"));
518 assert!(content.contains("o Mesh2"));
519 assert!(content.contains("f 4 5 6"));
521 }
522
523 #[test]
524 fn test_write_obj_mesh_with_normals_and_texcoords() {
525 let mut mesh = create_triangle_mesh();
526 add_f32_attribute(
527 &mut mesh,
528 GeometryAttributeType::Normal,
529 3,
530 &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
531 );
532 add_f32_attribute(
533 &mut mesh,
534 GeometryAttributeType::TexCoord,
535 2,
536 &[0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
537 );
538
539 let mut writer = ObjWriter::new();
540 Writer::add_mesh(&mut writer, &mesh, Some("Triangle")).unwrap();
541
542 let content = String::from_utf8(writer.write_to_vec().unwrap()).unwrap();
543 assert!(content.contains("vt 1.000000 0.000000"));
544 assert!(content.contains("vn 0.000000 0.000000 1.000000"));
545 assert!(content.contains("f 1/1/1 2/2/2 3/3/3"));
546 }
547}