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