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