Skip to main content

draco_io/
ply_writer.rs

1//! PLY format writer for meshes and point clouds.
2//!
3//! Supports writing:
4//! - ASCII PLY format
5//! - Binary little-endian PLY format
6//! - Binary big-endian PLY format
7//! - Vertex positions
8//! - Vertex normals (if present)
9//! - Vertex colors (if present)
10//! - Per-vertex texture coordinates (if present)
11//! - Triangle faces (for meshes)
12//!
13//! # Example
14//!
15//! ```ignore
16//! use draco_io::PlyWriter;
17//! use draco_core::mesh::Mesh;
18//!
19//! let mesh: Mesh = /* ... */;
20//! let mut writer = PlyWriter::new();
21//! writer.add_mesh(&mesh);
22//! writer.write("output.ply")?;
23//!
24//! // Or write point cloud
25//! let mut writer = PlyWriter::new();
26//! writer.add_points(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
27//! writer.write("points.ply")?;
28//! ```
29
30use std::fs::File;
31use std::io::{self, BufWriter, Write};
32use std::path::Path;
33
34use draco_core::draco_types::DataType;
35use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
36use draco_core::geometry_indices::FaceIndex;
37use draco_core::mesh::Mesh;
38
39pub use crate::ply_format::PlyFormat;
40use crate::traits::{PointCloudWriter, WriteToBytes, Writer};
41
42/// PLY format writer.
43///
44/// This struct provides a builder-style API for writing PLY files.
45/// Meshes or points are added, then written with `write()`.
46///
47/// # Example
48///
49/// ```ignore
50/// use draco_io::PlyWriter;
51///
52/// let mut writer = PlyWriter::new();
53/// writer.add_mesh(&mesh);
54/// writer.write("cube.ply")?;
55/// ```
56#[derive(Debug, Clone, Default)]
57pub struct PlyWriter {
58    /// Output format
59    format: PlyFormat,
60    /// Collected vertex positions
61    positions: PlyPositionData,
62    /// Collected vertex normals
63    normals: Vec<[f32; 3]>,
64    /// Collected vertex colors (RGBA 0-255)
65    colors: Vec<[u8; 4]>,
66    color_components: u8,
67    /// Collected vertex texture coordinates
68    texcoords: Vec<[f32; 2]>,
69    /// Collected faces (0-based indices)
70    faces: Vec<[u32; 3]>,
71}
72
73#[derive(Debug, Clone)]
74enum PlyPositionData {
75    Float32(Vec<[f32; 3]>),
76    Float64(Vec<[f64; 3]>),
77    Int32(Vec<[i32; 3]>),
78    Uint32(Vec<[u32; 3]>),
79}
80
81impl Default for PlyPositionData {
82    fn default() -> Self {
83        PlyPositionData::Float32(Vec::new())
84    }
85}
86
87impl PlyPositionData {
88    fn len(&self) -> usize {
89        match self {
90            PlyPositionData::Float32(values) => values.len(),
91            PlyPositionData::Float64(values) => values.len(),
92            PlyPositionData::Int32(values) => values.len(),
93            PlyPositionData::Uint32(values) => values.len(),
94        }
95    }
96
97    fn data_type(&self) -> draco_core::draco_types::DataType {
98        match self {
99            PlyPositionData::Float32(_) => draco_core::draco_types::DataType::Float32,
100            PlyPositionData::Float64(_) => draco_core::draco_types::DataType::Float64,
101            PlyPositionData::Int32(_) => draco_core::draco_types::DataType::Int32,
102            PlyPositionData::Uint32(_) => draco_core::draco_types::DataType::Uint32,
103        }
104    }
105
106    fn type_name(&self) -> &'static str {
107        match self.data_type() {
108            draco_core::draco_types::DataType::Float64 => "double",
109            draco_core::draco_types::DataType::Int32 => "int",
110            draco_core::draco_types::DataType::Uint32 => "uint",
111            _ => "float",
112        }
113    }
114
115    fn push_f32_slice(&mut self, points: &[[f32; 3]]) {
116        self.ensure_float32();
117        if let PlyPositionData::Float32(values) = self {
118            values.extend_from_slice(points);
119        }
120    }
121
122    fn ensure_float32(&mut self) {
123        if matches!(self, PlyPositionData::Float32(_)) {
124            return;
125        }
126        let converted = self.iter_as_f32().collect();
127        *self = PlyPositionData::Float32(converted);
128    }
129
130    fn iter_as_f32(&self) -> Box<dyn Iterator<Item = [f32; 3]> + '_> {
131        match self {
132            PlyPositionData::Float32(values) => Box::new(values.iter().copied()),
133            PlyPositionData::Float64(values) => Box::new(
134                values
135                    .iter()
136                    .map(|v| [v[0] as f32, v[1] as f32, v[2] as f32]),
137            ),
138            PlyPositionData::Int32(values) => Box::new(
139                values
140                    .iter()
141                    .map(|v| [v[0] as f32, v[1] as f32, v[2] as f32]),
142            ),
143            PlyPositionData::Uint32(values) => Box::new(
144                values
145                    .iter()
146                    .map(|v| [v[0] as f32, v[1] as f32, v[2] as f32]),
147            ),
148        }
149    }
150}
151
152impl PlyWriter {
153    /// Create a new PLY writer.
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Configure the writer to emit binary little-endian PLY.
159    pub fn with_binary_little_endian(mut self) -> Self {
160        self.format = PlyFormat::BinaryLittleEndian;
161        self
162    }
163
164    /// Configure the PLY storage format.
165    pub fn with_format(mut self, format: PlyFormat) -> Self {
166        self.format = format;
167        self
168    }
169
170    /// Set the PLY storage format.
171    pub fn set_format(&mut self, format: PlyFormat) -> &mut Self {
172        self.format = format;
173        self
174    }
175
176    /// Get the configured PLY storage format.
177    pub fn format(&self) -> PlyFormat {
178        self.format
179    }
180
181    /// Enable or disable binary little-endian output.
182    pub fn set_binary_little_endian(&mut self, enabled: bool) -> &mut Self {
183        self.format = if enabled {
184            PlyFormat::BinaryLittleEndian
185        } else {
186            PlyFormat::Ascii
187        };
188        self
189    }
190
191    /// Returns true when the writer is configured for binary little-endian output.
192    pub fn is_binary_little_endian(&self) -> bool {
193        self.format == PlyFormat::BinaryLittleEndian
194    }
195
196    /// Add raw point positions (for point cloud output).
197    pub fn add_points(&mut self, points: &[[f32; 3]]) {
198        self.positions.push_f32_slice(points);
199    }
200
201    /// Add a single point.
202    pub fn add_point(&mut self, point: [f32; 3]) {
203        self.add_points(&[point]);
204    }
205
206    /// Add points with colors.
207    pub fn add_points_with_colors(&mut self, points: &[[f32; 3]], colors: &[[u8; 4]]) {
208        // Pad colors if needed
209        while self.colors.len() < self.positions.len() {
210            self.colors.push([255, 255, 255, 255]);
211        }
212        self.positions.push_f32_slice(points);
213        self.color_components = self.color_components.max(4);
214        self.colors.extend_from_slice(colors);
215    }
216
217    /// Get the number of vertices added.
218    pub fn vertex_count(&self) -> usize {
219        self.positions.len()
220    }
221
222    /// Get the number of faces added.
223    pub fn face_count(&self) -> usize {
224        self.faces.len()
225    }
226
227    /// Check if the writer has normals.
228    pub fn has_normals(&self) -> bool {
229        !self.normals.is_empty()
230    }
231
232    /// Check if the writer has colors.
233    pub fn has_colors(&self) -> bool {
234        !self.colors.is_empty()
235    }
236
237    /// Write the PLY file to the given path.
238    pub fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
239        let file = File::create(path)?;
240        let mut writer = BufWriter::new(file);
241        self.write_to(&mut writer)
242    }
243
244    /// Write the PLY data into a byte vector.
245    pub fn write_to_vec(&self) -> io::Result<Vec<u8>> {
246        let mut out = Vec::new();
247        self.write_to(&mut out)?;
248        Ok(out)
249    }
250
251    /// Write the PLY data to a writer.
252    pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
253        let has_normals = self.normals.len() == self.positions.len();
254        let has_colors = self.colors.len() == self.positions.len() && self.color_components > 0;
255
256        let has_texcoords = self.texcoords.len() == self.positions.len();
257        self.write_header(writer, has_normals, has_colors, has_texcoords)?;
258
259        match self.format {
260            PlyFormat::Ascii => {
261                self.write_ascii_body(writer, has_normals, has_colors, has_texcoords)
262            }
263            PlyFormat::BinaryLittleEndian => {
264                self.write_binary_body(writer, has_normals, has_colors, has_texcoords, false)
265            }
266            PlyFormat::BinaryBigEndian => {
267                self.write_binary_body(writer, has_normals, has_colors, has_texcoords, true)
268            }
269        }
270    }
271
272    fn write_header<W: Write>(
273        &self,
274        writer: &mut W,
275        has_normals: bool,
276        has_colors: bool,
277        has_texcoords: bool,
278    ) -> io::Result<()> {
279        writeln!(writer, "ply")?;
280        match self.format {
281            PlyFormat::Ascii => writeln!(writer, "format ascii 1.0")?,
282            PlyFormat::BinaryLittleEndian => writeln!(writer, "format binary_little_endian 1.0")?,
283            PlyFormat::BinaryBigEndian => writeln!(writer, "format binary_big_endian 1.0")?,
284        }
285        writeln!(writer, "comment Generated by draco-io")?;
286        writeln!(writer, "element vertex {}", self.positions.len())?;
287        writeln!(writer, "property {} x", self.positions.type_name())?;
288        writeln!(writer, "property {} y", self.positions.type_name())?;
289        writeln!(writer, "property {} z", self.positions.type_name())?;
290
291        if has_normals {
292            writeln!(writer, "property float nx")?;
293            writeln!(writer, "property float ny")?;
294            writeln!(writer, "property float nz")?;
295        }
296
297        if has_colors {
298            writeln!(writer, "property uchar red")?;
299            writeln!(writer, "property uchar green")?;
300            writeln!(writer, "property uchar blue")?;
301            if self.color_components > 3 {
302                writeln!(writer, "property uchar alpha")?;
303            }
304        }
305
306        if has_texcoords {
307            writeln!(writer, "property float texture_u")?;
308            writeln!(writer, "property float texture_v")?;
309        }
310
311        if !self.faces.is_empty() {
312            writeln!(writer, "element face {}", self.faces.len())?;
313            writeln!(writer, "property list uchar int vertex_indices")?;
314        }
315
316        writeln!(writer, "end_header")?;
317        Ok(())
318    }
319
320    fn write_ascii_body<W: Write>(
321        &self,
322        writer: &mut W,
323        has_normals: bool,
324        has_colors: bool,
325        has_texcoords: bool,
326    ) -> io::Result<()> {
327        for i in 0..self.positions.len() {
328            match &self.positions {
329                PlyPositionData::Float32(values) => {
330                    let [x, y, z] = values[i];
331                    write!(writer, "{:.6} {:.6} {:.6}", x, y, z)?;
332                }
333                PlyPositionData::Float64(values) => {
334                    let [x, y, z] = values[i];
335                    write!(writer, "{:.6} {:.6} {:.6}", x, y, z)?;
336                }
337                PlyPositionData::Int32(values) => {
338                    let [x, y, z] = values[i];
339                    write!(writer, "{} {} {}", x, y, z)?;
340                }
341                PlyPositionData::Uint32(values) => {
342                    let [x, y, z] = values[i];
343                    write!(writer, "{} {} {}", x, y, z)?;
344                }
345            }
346
347            if has_normals {
348                let [nx, ny, nz] = self.normals[i];
349                write!(writer, " {:.6} {:.6} {:.6}", nx, ny, nz)?;
350            }
351
352            if has_colors {
353                let [r, g, b, a] = self.colors[i];
354                write!(writer, " {} {} {}", r, g, b)?;
355                if self.color_components > 3 {
356                    write!(writer, " {}", a)?;
357                }
358            }
359
360            if has_texcoords {
361                let [u, v] = self.texcoords[i];
362                write!(writer, " {:.6} {:.6}", u, v)?;
363            }
364
365            writeln!(writer)?;
366        }
367
368        // Write faces
369        for face in &self.faces {
370            write!(writer, "3 {} {} {}", face[0], face[1], face[2])?;
371            writeln!(writer)?;
372        }
373
374        Ok(())
375    }
376
377    fn write_binary_body<W: Write>(
378        &self,
379        writer: &mut W,
380        has_normals: bool,
381        has_colors: bool,
382        has_texcoords: bool,
383        big_endian: bool,
384    ) -> io::Result<()> {
385        for i in 0..self.positions.len() {
386            match &self.positions {
387                PlyPositionData::Float32(values) => {
388                    for component in values[i] {
389                        writer.write_all(&if big_endian {
390                            component.to_be_bytes()
391                        } else {
392                            component.to_le_bytes()
393                        })?;
394                    }
395                }
396                PlyPositionData::Float64(values) => {
397                    for component in values[i] {
398                        writer.write_all(&if big_endian {
399                            component.to_be_bytes()
400                        } else {
401                            component.to_le_bytes()
402                        })?;
403                    }
404                }
405                PlyPositionData::Int32(values) => {
406                    for component in values[i] {
407                        writer.write_all(&if big_endian {
408                            component.to_be_bytes()
409                        } else {
410                            component.to_le_bytes()
411                        })?;
412                    }
413                }
414                PlyPositionData::Uint32(values) => {
415                    for component in values[i] {
416                        writer.write_all(&if big_endian {
417                            component.to_be_bytes()
418                        } else {
419                            component.to_le_bytes()
420                        })?;
421                    }
422                }
423            }
424
425            if has_normals {
426                let [nx, ny, nz] = self.normals[i];
427                writer.write_all(&if big_endian {
428                    nx.to_be_bytes()
429                } else {
430                    nx.to_le_bytes()
431                })?;
432                writer.write_all(&if big_endian {
433                    ny.to_be_bytes()
434                } else {
435                    ny.to_le_bytes()
436                })?;
437                writer.write_all(&if big_endian {
438                    nz.to_be_bytes()
439                } else {
440                    nz.to_le_bytes()
441                })?;
442            }
443
444            if has_colors {
445                writer.write_all(&self.colors[i][..self.color_components as usize])?;
446            }
447
448            if has_texcoords {
449                let [u, v] = self.texcoords[i];
450                writer.write_all(&if big_endian {
451                    u.to_be_bytes()
452                } else {
453                    u.to_le_bytes()
454                })?;
455                writer.write_all(&if big_endian {
456                    v.to_be_bytes()
457                } else {
458                    v.to_le_bytes()
459                })?;
460            }
461        }
462
463        for face in &self.faces {
464            writer.write_all(&[3u8])?;
465            for index in face {
466                let index = i32::try_from(*index).map_err(|_| {
467                    io::Error::new(
468                        io::ErrorKind::InvalidInput,
469                        "PLY binary writer only supports face indices up to i32::MAX",
470                    )
471                })?;
472                writer.write_all(&if big_endian {
473                    index.to_be_bytes()
474                } else {
475                    index.to_le_bytes()
476                })?;
477            }
478        }
479
480        Ok(())
481    }
482}
483
484/// Read a float3 from an attribute at a given point index.
485fn read_float3(mesh: &Mesh, att_id: i32, point_idx: usize) -> [f32; 3] {
486    let att = mesh.attribute(att_id);
487    let byte_stride = att.byte_stride() as usize;
488    let buffer = att.buffer();
489    let mut bytes = [0u8; 12];
490    buffer.read(point_idx * byte_stride, &mut bytes);
491    [
492        f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
493        f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
494        f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
495    ]
496}
497
498/// Read a color from an attribute at a given point index.
499fn read_color(mesh: &Mesh, att_id: i32, point_idx: usize) -> [u8; 4] {
500    let att = mesh.attribute(att_id);
501    let byte_stride = att.byte_stride() as usize;
502    let buffer = att.buffer();
503
504    // Colors can be stored in different formats
505    let num_components = att.num_components() as usize;
506    let component_size = byte_stride / num_components;
507
508    if component_size == 1 {
509        // u8 colors
510        let mut bytes = [255u8; 4];
511        let read_len = num_components.min(4);
512        buffer.read(point_idx * byte_stride, &mut bytes[..read_len]);
513        bytes
514    } else if component_size == 4 {
515        // f32 colors (0.0-1.0) - convert to u8
516        let mut float_bytes = [0u8; 16];
517        let read_len = (num_components * 4).min(16);
518        buffer.read(point_idx * byte_stride, &mut float_bytes[..read_len]);
519
520        let mut result = [255u8; 4];
521        for i in 0..num_components.min(4) {
522            let f = f32::from_le_bytes([
523                float_bytes[i * 4],
524                float_bytes[i * 4 + 1],
525                float_bytes[i * 4 + 2],
526                float_bytes[i * 4 + 3],
527            ]);
528            result[i] = (f.clamp(0.0, 1.0) * 255.0) as u8;
529        }
530        result
531    } else {
532        [255, 255, 255, 255] // Default white
533    }
534}
535
536// ============================================================================
537// Trait Implementations
538// ============================================================================
539
540impl Writer for PlyWriter {
541    fn new() -> Self {
542        Self::default()
543    }
544
545    fn add_mesh(&mut self, mesh: &Mesh, _name: Option<&str>) -> io::Result<()> {
546        // PLY format doesn't support mesh names
547        let vertex_offset = self.positions.len() as u32;
548
549        // Extract positions
550        let pos_att_id = mesh.named_attribute_id(GeometryAttributeType::Position);
551        if pos_att_id >= 0 {
552            let att = mesh.attribute(pos_att_id);
553            append_positions_from_attribute(&mut self.positions, att, mesh.num_points());
554        }
555
556        // Extract normals if present
557        let normal_att_id = mesh.named_attribute_id(GeometryAttributeType::Normal);
558        if normal_att_id >= 0 {
559            // Pad normals if we've added vertices without normals before
560            while self.normals.len() < self.positions.len() - mesh.num_points() {
561                self.normals.push([0.0, 0.0, 0.0]);
562            }
563            for i in 0..mesh.num_points() {
564                self.normals.push(read_float3(mesh, normal_att_id, i));
565            }
566        }
567
568        // Extract colors if present
569        let color_att_id = mesh.named_attribute_id(GeometryAttributeType::Color);
570        if color_att_id >= 0 {
571            let color_att = mesh.attribute(color_att_id);
572            let components = color_att.num_components().clamp(1, 4);
573            self.color_components = self.color_components.max(components);
574            // Pad colors if we've added vertices without colors before
575            while self.colors.len() < self.positions.len() - mesh.num_points() {
576                self.colors.push([255, 255, 255, 255]);
577            }
578            for i in 0..mesh.num_points() {
579                self.colors.push(read_color(mesh, color_att_id, i));
580            }
581        }
582
583        let texcoord_att_id = mesh.named_attribute_id(GeometryAttributeType::TexCoord);
584        if texcoord_att_id >= 0 {
585            let texcoord_att = mesh.attribute(texcoord_att_id);
586            if texcoord_att.num_components() == 2 && texcoord_att.data_type() == DataType::Float32 {
587                while self.texcoords.len() < self.positions.len() - mesh.num_points() {
588                    self.texcoords.push([0.0, 0.0]);
589                }
590                for i in 0..mesh.num_points() {
591                    self.texcoords.push(read_float2(mesh, texcoord_att_id, i));
592                }
593            }
594        }
595
596        // Extract faces (0-based indices with offset)
597        for i in 0..mesh.num_faces() as u32 {
598            let face = mesh.face(FaceIndex(i));
599            self.faces.push([
600                face[0].0 + vertex_offset,
601                face[1].0 + vertex_offset,
602                face[2].0 + vertex_offset,
603            ]);
604        }
605        Ok(())
606    }
607
608    fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
609        self.write(path)
610    }
611
612    fn vertex_count(&self) -> usize {
613        self.vertex_count()
614    }
615
616    fn face_count(&self) -> usize {
617        self.face_count()
618    }
619}
620
621impl PointCloudWriter for PlyWriter {
622    fn add_points(&mut self, points: &[[f32; 3]]) {
623        self.add_points(points);
624    }
625
626    fn add_point(&mut self, point: [f32; 3]) {
627        self.add_point(point);
628    }
629}
630
631impl WriteToBytes for PlyWriter {
632    fn write_to_vec(&self) -> io::Result<Vec<u8>> {
633        PlyWriter::write_to_vec(self)
634    }
635
636    fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
637        PlyWriter::write_to(self, writer)
638    }
639}
640
641// ============================================================================
642// Convenience Functions (for backward compatibility)
643// ============================================================================
644
645/// Write a mesh to a PLY file.
646///
647/// This is a convenience function. For more control, use `PlyWriter` directly.
648pub fn write_ply_mesh<P: AsRef<Path>>(path: P, mesh: &Mesh) -> io::Result<()> {
649    let mut writer = PlyWriter::new();
650    Writer::add_mesh(&mut writer, mesh, None)?;
651    writer.write(path)
652}
653
654/// Write point positions to a PLY file (point cloud, no faces).
655///
656/// This is a convenience function. For more control, use `PlyWriter` directly.
657pub fn write_ply_positions<P: AsRef<Path>>(path: P, points: &[[f32; 3]]) -> io::Result<()> {
658    let mut writer = PlyWriter::new();
659    writer.add_points(points);
660    writer.write(path)
661}
662
663// ============================================================================
664// Tests
665// ============================================================================
666
667#[cfg(test)]
668mod tests {
669    use super::*;
670    #[cfg(feature = "ply-reader")]
671    use crate::ply_reader::PlyReader;
672    use draco_core::draco_types::DataType;
673    use draco_core::geometry_attribute::PointAttribute;
674    use draco_core::geometry_indices::PointIndex;
675    use std::fs;
676    use tempfile::NamedTempFile;
677
678    fn create_triangle_mesh() -> Mesh {
679        let mut mesh = Mesh::new();
680        let mut pos_att = PointAttribute::new();
681
682        pos_att.init(
683            GeometryAttributeType::Position,
684            3,
685            DataType::Float32,
686            false,
687            3,
688        );
689        let buffer = pos_att.buffer_mut();
690        let positions: [[f32; 3]; 3] = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
691        for (i, pos) in positions.iter().enumerate() {
692            let bytes: Vec<u8> = pos.iter().flat_map(|v| v.to_le_bytes()).collect();
693            buffer.write(i * 12, &bytes);
694        }
695        mesh.add_attribute(pos_att);
696
697        mesh.set_num_faces(1);
698        mesh.set_face(FaceIndex(0), [PointIndex(0), PointIndex(1), PointIndex(2)]);
699
700        mesh
701    }
702
703    #[test]
704    fn test_ply_writer_new() {
705        let writer = PlyWriter::new();
706        assert_eq!(writer.vertex_count(), 0);
707        assert_eq!(writer.face_count(), 0);
708        assert!(!writer.has_normals());
709        assert!(!writer.has_colors());
710        assert!(!writer.is_binary_little_endian());
711    }
712
713    #[test]
714    fn test_ply_writer_add_mesh() {
715        let mesh = create_triangle_mesh();
716        let mut writer = PlyWriter::new();
717        Writer::add_mesh(&mut writer, &mesh, None).unwrap();
718        assert_eq!(writer.vertex_count(), 3);
719        assert_eq!(writer.face_count(), 1);
720    }
721
722    #[test]
723    fn test_ply_writer_add_points() {
724        let mut writer = PlyWriter::new();
725        writer.add_points(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
726        assert_eq!(writer.vertex_count(), 2);
727        assert_eq!(writer.face_count(), 0);
728    }
729
730    #[test]
731    fn test_ply_writer_add_points_with_colors() {
732        let mut writer = PlyWriter::new();
733        writer.add_points_with_colors(
734            &[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
735            &[[255, 0, 0, 255], [0, 255, 0, 255]],
736        );
737        assert_eq!(writer.vertex_count(), 2);
738        assert!(writer.has_colors());
739    }
740
741    #[test]
742    fn test_write_ply_positions() {
743        let points = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
744
745        let file = NamedTempFile::new().unwrap();
746        write_ply_positions(file.path(), &points).unwrap();
747
748        let content = fs::read_to_string(file.path()).unwrap();
749        assert!(content.contains("ply"));
750        assert!(content.contains("format ascii 1.0"));
751        assert!(content.contains("element vertex 3"));
752        assert!(content.contains("property float x"));
753        assert!(content.contains("end_header"));
754        assert!(content.contains("0.000000 0.000000 0.000000"));
755        assert!(content.contains("1.000000 0.000000 0.000000"));
756    }
757
758    #[test]
759    fn test_write_ply_mesh() {
760        let mesh = create_triangle_mesh();
761        let file = NamedTempFile::new().unwrap();
762        write_ply_mesh(file.path(), &mesh).unwrap();
763
764        let content = fs::read_to_string(file.path()).unwrap();
765        assert!(content.contains("ply"));
766        assert!(content.contains("element vertex 3"));
767        assert!(content.contains("element face 1"));
768        assert!(content.contains("property list uchar int vertex_indices"));
769        assert!(content.contains("3 0 1 2")); // face with 0-based indices
770    }
771
772    #[test]
773    fn test_multiple_meshes() {
774        let mesh1 = create_triangle_mesh();
775        let mesh2 = create_triangle_mesh();
776
777        let mut writer = PlyWriter::new();
778        Writer::add_mesh(&mut writer, &mesh1, None).unwrap();
779        Writer::add_mesh(&mut writer, &mesh2, None).unwrap();
780
781        assert_eq!(writer.vertex_count(), 6);
782        assert_eq!(writer.face_count(), 2);
783
784        let file = NamedTempFile::new().unwrap();
785        writer.write(file.path()).unwrap();
786
787        let content = fs::read_to_string(file.path()).unwrap();
788        assert!(content.contains("element vertex 6"));
789        assert!(content.contains("element face 2"));
790        // Second mesh should have offset indices
791        assert!(content.contains("3 3 4 5"));
792    }
793
794    #[test]
795    fn test_ply_with_colors() {
796        let mut writer = PlyWriter::new();
797        writer.add_points_with_colors(
798            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
799            &[[255, 0, 0, 255], [0, 255, 0, 255]],
800        );
801
802        let file = NamedTempFile::new().unwrap();
803        writer.write(file.path()).unwrap();
804
805        let content = fs::read_to_string(file.path()).unwrap();
806        assert!(content.contains("property uchar red"));
807        assert!(content.contains("property uchar green"));
808        assert!(content.contains("property uchar blue"));
809        assert!(content.contains("property uchar alpha"));
810        assert!(content.contains("255 0 0 255"));
811        assert!(content.contains("0 255 0 255"));
812    }
813
814    #[test]
815    fn test_ply_writer_can_switch_to_binary_little_endian() {
816        let writer = PlyWriter::new().with_binary_little_endian();
817        assert!(writer.is_binary_little_endian());
818
819        let mut writer = PlyWriter::new();
820        writer.set_binary_little_endian(true);
821        assert!(writer.is_binary_little_endian());
822        writer.set_binary_little_endian(false);
823        assert!(!writer.is_binary_little_endian());
824    }
825
826    #[cfg(feature = "ply-reader")]
827    #[test]
828    fn test_write_binary_little_endian_positions_roundtrip() {
829        let points = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
830
831        let file = NamedTempFile::new().unwrap();
832        let mut writer = PlyWriter::new().with_binary_little_endian();
833        writer.add_points(&points);
834        writer.write(file.path()).unwrap();
835
836        let content = fs::read(file.path()).unwrap();
837        let header_end = content
838            .windows(b"end_header\n".len())
839            .position(|window| window == b"end_header\n")
840            .map(|idx| idx + b"end_header\n".len())
841            .unwrap();
842        let header = std::str::from_utf8(&content[..header_end]).unwrap();
843        assert!(header.contains("format binary_little_endian 1.0"));
844
845        let mut reader = PlyReader::open(file.path()).unwrap();
846        let positions = reader.read_positions().unwrap();
847        assert_eq!(positions, points);
848    }
849
850    #[cfg(feature = "ply-reader")]
851    #[test]
852    fn test_write_binary_little_endian_mesh_roundtrip() {
853        let mesh = create_triangle_mesh();
854        let file = NamedTempFile::new().unwrap();
855
856        let mut writer = PlyWriter::new().with_binary_little_endian();
857        Writer::add_mesh(&mut writer, &mesh, None).unwrap();
858        writer.write(file.path()).unwrap();
859
860        let bytes = fs::read(file.path()).unwrap();
861        let header_end = bytes
862            .windows(b"end_header\n".len())
863            .position(|window| window == b"end_header\n")
864            .map(|idx| idx + b"end_header\n".len())
865            .unwrap();
866        let header = std::str::from_utf8(&bytes[..header_end]).unwrap();
867        assert!(header.contains("format binary_little_endian 1.0"));
868        assert!(header.contains("element vertex 3"));
869        assert!(header.contains("element face 1"));
870
871        let mut reader = PlyReader::open(file.path()).unwrap();
872        let mesh = reader.read_mesh().unwrap();
873        assert_eq!(mesh.num_points(), 3);
874        assert_eq!(mesh.num_faces(), 1);
875        assert_eq!(
876            mesh.face(FaceIndex(0)),
877            [PointIndex(0), PointIndex(1), PointIndex(2)]
878        );
879    }
880
881    #[cfg(feature = "ply-reader")]
882    #[test]
883    fn test_write_binary_big_endian_mesh_roundtrip() {
884        let mesh = create_triangle_mesh();
885        let mut writer = PlyWriter::new().with_format(PlyFormat::BinaryBigEndian);
886        Writer::add_mesh(&mut writer, &mesh, None).unwrap();
887        let bytes = writer.write_to_vec().unwrap();
888        let header_end = bytes
889            .windows(b"end_header\n".len())
890            .position(|window| window == b"end_header\n")
891            .map(|idx| idx + b"end_header\n".len())
892            .unwrap();
893        let header = std::str::from_utf8(&bytes[..header_end]).unwrap();
894        assert!(header.contains("format binary_big_endian 1.0"));
895
896        let mesh = PlyReader::read_from_bytes(&bytes).unwrap();
897        assert_eq!(mesh.num_points(), 3);
898        assert_eq!(mesh.num_faces(), 1);
899    }
900
901    #[test]
902    fn test_write_preserves_int32_positions() {
903        let mut mesh = Mesh::new();
904        let mut pos_att = PointAttribute::new();
905        pos_att.init(
906            GeometryAttributeType::Position,
907            3,
908            DataType::Int32,
909            false,
910            2,
911        );
912        pos_att
913            .buffer_mut()
914            .write(0, &[1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
915        pos_att
916            .buffer_mut()
917            .write(12, &[4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0]);
918        mesh.add_attribute(pos_att);
919
920        let mut writer = PlyWriter::new();
921        Writer::add_mesh(&mut writer, &mesh, None).unwrap();
922        let output = String::from_utf8(writer.write_to_vec().unwrap()).unwrap();
923        assert!(output.contains("property int x"));
924        assert!(output.contains("1 2 3"));
925    }
926}
927
928fn read_float2(mesh: &Mesh, att_id: i32, point_idx: usize) -> [f32; 2] {
929    let att = mesh.attribute(att_id);
930    let byte_stride = att.byte_stride() as usize;
931    let buffer = att.buffer();
932    let mut bytes = [0u8; 8];
933    buffer.read(point_idx * byte_stride, &mut bytes);
934    [
935        f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
936        f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
937    ]
938}
939
940fn read_f64x3(att: &PointAttribute, point_idx: usize) -> [f64; 3] {
941    let byte_stride = att.byte_stride() as usize;
942    let buffer = att.buffer();
943    let mut bytes = [0u8; 24];
944    buffer.read(point_idx * byte_stride, &mut bytes);
945    [
946        f64::from_le_bytes(bytes[0..8].try_into().unwrap()),
947        f64::from_le_bytes(bytes[8..16].try_into().unwrap()),
948        f64::from_le_bytes(bytes[16..24].try_into().unwrap()),
949    ]
950}
951
952fn read_i32x3(att: &PointAttribute, point_idx: usize) -> [i32; 3] {
953    let byte_stride = att.byte_stride() as usize;
954    let buffer = att.buffer();
955    let mut bytes = [0u8; 12];
956    buffer.read(point_idx * byte_stride, &mut bytes);
957    [
958        i32::from_le_bytes(bytes[0..4].try_into().unwrap()),
959        i32::from_le_bytes(bytes[4..8].try_into().unwrap()),
960        i32::from_le_bytes(bytes[8..12].try_into().unwrap()),
961    ]
962}
963
964fn read_u32x3(att: &PointAttribute, point_idx: usize) -> [u32; 3] {
965    let byte_stride = att.byte_stride() as usize;
966    let buffer = att.buffer();
967    let mut bytes = [0u8; 12];
968    buffer.read(point_idx * byte_stride, &mut bytes);
969    [
970        u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
971        u32::from_le_bytes(bytes[4..8].try_into().unwrap()),
972        u32::from_le_bytes(bytes[8..12].try_into().unwrap()),
973    ]
974}
975
976fn append_positions_from_attribute(
977    positions: &mut PlyPositionData,
978    att: &PointAttribute,
979    num_points: usize,
980) {
981    if att.num_components() != 3 {
982        return;
983    }
984
985    match att.data_type() {
986        DataType::Float32 => {
987            let values: Vec<[f32; 3]> = (0..num_points)
988                .map(|i| {
989                    let byte_stride = att.byte_stride() as usize;
990                    let mut bytes = [0u8; 12];
991                    att.buffer().read(i * byte_stride, &mut bytes);
992                    [
993                        f32::from_le_bytes(bytes[0..4].try_into().unwrap()),
994                        f32::from_le_bytes(bytes[4..8].try_into().unwrap()),
995                        f32::from_le_bytes(bytes[8..12].try_into().unwrap()),
996                    ]
997                })
998                .collect();
999            match positions {
1000                PlyPositionData::Float32(existing) => existing.extend(values),
1001                _ => {
1002                    positions.ensure_float32();
1003                    if let PlyPositionData::Float32(existing) = positions {
1004                        existing.extend(values);
1005                    }
1006                }
1007            }
1008        }
1009        DataType::Float64
1010            if positions.len() == 0 || matches!(positions, PlyPositionData::Float64(_)) =>
1011        {
1012            let values: Vec<[f64; 3]> = (0..num_points).map(|i| read_f64x3(att, i)).collect();
1013            match positions {
1014                PlyPositionData::Float32(existing) if existing.is_empty() => {
1015                    *positions = PlyPositionData::Float64(values);
1016                }
1017                PlyPositionData::Float64(existing) => existing.extend(values),
1018                _ => unreachable!(),
1019            }
1020        }
1021        DataType::Int32
1022            if positions.len() == 0 || matches!(positions, PlyPositionData::Int32(_)) =>
1023        {
1024            let values: Vec<[i32; 3]> = (0..num_points).map(|i| read_i32x3(att, i)).collect();
1025            match positions {
1026                PlyPositionData::Float32(existing) if existing.is_empty() => {
1027                    *positions = PlyPositionData::Int32(values);
1028                }
1029                PlyPositionData::Int32(existing) => existing.extend(values),
1030                _ => unreachable!(),
1031            }
1032        }
1033        DataType::Uint32
1034            if positions.len() == 0 || matches!(positions, PlyPositionData::Uint32(_)) =>
1035        {
1036            let values: Vec<[u32; 3]> = (0..num_points).map(|i| read_u32x3(att, i)).collect();
1037            match positions {
1038                PlyPositionData::Float32(existing) if existing.is_empty() => {
1039                    *positions = PlyPositionData::Uint32(values);
1040                }
1041                PlyPositionData::Uint32(existing) => existing.extend(values),
1042                _ => unreachable!(),
1043            }
1044        }
1045        _ => {
1046            let converted: Vec<[f32; 3]> = (0..num_points)
1047                .map(|i| read_numeric3_as_f32(att, i))
1048                .collect();
1049            positions.push_f32_slice(&converted);
1050        }
1051    }
1052}
1053
1054fn read_numeric3_as_f32(att: &PointAttribute, point_idx: usize) -> [f32; 3] {
1055    match att.data_type() {
1056        DataType::Float64 => {
1057            let v = read_f64x3(att, point_idx);
1058            [v[0] as f32, v[1] as f32, v[2] as f32]
1059        }
1060        DataType::Int32 => {
1061            let v = read_i32x3(att, point_idx);
1062            [v[0] as f32, v[1] as f32, v[2] as f32]
1063        }
1064        DataType::Uint32 => {
1065            let v = read_u32x3(att, point_idx);
1066            [v[0] as f32, v[1] as f32, v[2] as f32]
1067        }
1068        _ => {
1069            let mut bytes = [0u8; 12];
1070            att.buffer()
1071                .read(point_idx * att.byte_stride() as usize, &mut bytes);
1072            [
1073                f32::from_le_bytes(bytes[0..4].try_into().unwrap()),
1074                f32::from_le_bytes(bytes[4..8].try_into().unwrap()),
1075                f32::from_le_bytes(bytes[8..12].try_into().unwrap()),
1076            ]
1077        }
1078    }
1079}