Skip to main content

draco_io/
ply_reader.rs

1//! PLY format reader for meshes and point clouds.
2//!
3//! Reads positions, triangle/polygon faces, normals, colors, and per-vertex
4//! texture coordinates from ASCII and binary PLY files. Polygon faces are
5//! triangulated with a fan.
6
7use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
8use std::fs;
9use std::io::{self, Cursor, Write};
10use std::path::Path;
11
12use draco_core::draco_types::DataType;
13use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
14use draco_core::mesh::Mesh;
15
16pub use crate::ply_format::PlyFormat;
17use crate::traits::{PointCloudReader, ReadFromBytes, Reader};
18
19#[derive(Debug)]
20struct ParsedPlyColorData {
21    num_components: u8,
22    values: Vec<[u8; 4]>,
23}
24
25#[derive(Debug)]
26struct ParsedPlyData {
27    positions: ParsedPlyPositionData,
28    faces: Vec<[u32; 3]>,
29    normals: Option<Vec<[f32; 3]>>,
30    colors: Option<ParsedPlyColorData>,
31    texcoords: Option<Vec<[f32; 2]>>,
32}
33
34#[derive(Debug)]
35enum ParsedPlyPositionData {
36    Float32(Vec<[f32; 3]>),
37    Int32(Vec<[i32; 3]>),
38}
39
40impl ParsedPlyPositionData {
41    fn len(&self) -> usize {
42        match self {
43            ParsedPlyPositionData::Float32(values) => values.len(),
44            ParsedPlyPositionData::Int32(values) => values.len(),
45        }
46    }
47
48    fn to_f32_positions(&self) -> Vec<[f32; 3]> {
49        match self {
50            ParsedPlyPositionData::Float32(values) => values.clone(),
51            ParsedPlyPositionData::Int32(values) => values
52                .iter()
53                .map(|value| [value[0] as f32, value[1] as f32, value[2] as f32])
54                .collect(),
55        }
56    }
57}
58
59#[derive(Debug, Clone)]
60enum PlyPropertyKind {
61    Scalar(DataType),
62    List {
63        count_type: DataType,
64        item_type: DataType,
65    },
66}
67
68#[derive(Debug, Clone)]
69struct PlyPropertyDef {
70    name: String,
71    kind: PlyPropertyKind,
72}
73
74impl PlyPropertyDef {
75    fn scalar_type(&self) -> Option<DataType> {
76        match self.kind {
77            PlyPropertyKind::Scalar(data_type) => Some(data_type),
78            PlyPropertyKind::List { .. } => None,
79        }
80    }
81}
82
83#[derive(Debug, Clone)]
84struct PlyHeader {
85    format: PlyFormat,
86    vertex_count: usize,
87    face_count: usize,
88    elements: Vec<PlyElementDef>,
89    vertex_properties: Vec<PlyPropertyDef>,
90    face_properties: Vec<PlyPropertyDef>,
91}
92
93#[derive(Debug, Clone)]
94struct PlyElementDef {
95    name: String,
96    count: usize,
97    properties: Vec<PlyPropertyDef>,
98}
99
100#[derive(Debug, Clone, Copy)]
101struct PlyReadSchema {
102    position_data_type: DataType,
103    has_normals: bool,
104    color_components: u8,
105    texcoord_pair: Option<TexcoordPropertyPair>,
106}
107
108#[derive(Debug, Clone, Copy)]
109struct TexcoordPropertyPair {
110    u: &'static str,
111    v: &'static str,
112}
113
114fn parse_ply_scalar_type(token: &str) -> Option<DataType> {
115    match token {
116        "char" | "int8" => Some(DataType::Int8),
117        "uchar" | "uint8" => Some(DataType::Uint8),
118        "short" | "int16" => Some(DataType::Int16),
119        "ushort" | "uint16" => Some(DataType::Uint16),
120        "int" | "int32" => Some(DataType::Int32),
121        "uint" | "uint32" => Some(DataType::Uint32),
122        "float" | "float32" => Some(DataType::Float32),
123        "double" | "float64" => Some(DataType::Float64),
124        _ => None,
125    }
126}
127
128/// PLY format reader.
129///
130/// Reads vertex positions from ASCII and little-endian binary PLY files.
131#[derive(Debug)]
132pub struct PlyReader {
133    source: PlyReaderSource,
134}
135
136#[derive(Debug, Clone)]
137enum PlyReaderSource {
138    Path(std::path::PathBuf),
139    Bytes(Vec<u8>),
140}
141
142impl PlyReader {
143    /// Open a PLY file for reading.
144    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
145        let path = path.as_ref().to_path_buf();
146        if !path.exists() {
147            return Err(io::Error::new(
148                io::ErrorKind::NotFound,
149                format!("File not found: {}", path.display()),
150            ));
151        }
152        Ok(Self {
153            source: PlyReaderSource::Path(path),
154        })
155    }
156
157    /// Create a PLY reader from in-memory bytes.
158    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
159        Self {
160            source: PlyReaderSource::Bytes(bytes.into()),
161        }
162    }
163
164    /// Read a mesh directly from in-memory bytes.
165    pub fn read_from_bytes(bytes: &[u8]) -> io::Result<Mesh> {
166        let mut reader = Self::from_bytes(bytes.to_vec());
167        reader.read_mesh()
168    }
169
170    /// Read all positions from the PLY file.
171    pub fn read_positions(&mut self) -> io::Result<Vec<[f32; 3]>> {
172        Ok(read_ply_source(&self.source)?.positions.to_f32_positions())
173    }
174
175    /// Read a mesh with positions (and faces if present).
176    pub fn read_mesh(&mut self) -> io::Result<Mesh> {
177        let parsed = read_ply_source(&self.source)?;
178        let mut mesh = Mesh::new();
179
180        if parsed.positions.len() == 0 {
181            return Ok(mesh);
182        }
183
184        mesh.set_num_points(parsed.positions.len());
185        mesh.set_num_faces(parsed.faces.len());
186
187        // Create position attribute
188        match &parsed.positions {
189            ParsedPlyPositionData::Float32(values) => {
190                mesh.add_attribute(make_f32x3_attribute(
191                    GeometryAttributeType::Position,
192                    values,
193                ));
194            }
195            ParsedPlyPositionData::Int32(values) => {
196                mesh.add_attribute(make_i32x3_attribute(
197                    GeometryAttributeType::Position,
198                    values,
199                ));
200            }
201        }
202
203        if let Some(normals) = parsed.normals.as_ref() {
204            mesh.add_attribute(make_f32x3_attribute(GeometryAttributeType::Normal, normals));
205        }
206
207        if let Some(colors) = parsed.colors.as_ref() {
208            mesh.add_attribute(make_u8_attribute(
209                GeometryAttributeType::Color,
210                colors.num_components,
211                true,
212                &colors.values,
213            ));
214        }
215
216        if let Some(texcoords) = parsed.texcoords.as_ref() {
217            mesh.add_attribute(make_f32x2_attribute(
218                GeometryAttributeType::TexCoord,
219                texcoords,
220            ));
221        }
222
223        for (i, face) in parsed.faces.iter().enumerate() {
224            mesh.set_face(
225                draco_core::geometry_indices::FaceIndex(i as u32),
226                [
227                    draco_core::geometry_indices::PointIndex(face[0]),
228                    draco_core::geometry_indices::PointIndex(face[1]),
229                    draco_core::geometry_indices::PointIndex(face[2]),
230                ],
231            );
232        }
233
234        if mesh.num_faces() > 0 {
235            // Match C++ Draco behavior: deduplicate point IDs in face-traversal order.
236            // This ensures binary compatibility when encoding.
237            mesh.deduplicate_point_ids();
238        }
239
240        Ok(mesh)
241    }
242}
243
244impl Reader for PlyReader {
245    fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
246        PlyReader::open(path)
247    }
248
249    fn read_meshes(&mut self) -> io::Result<Vec<Mesh>> {
250        let m = self.read_mesh()?;
251        Ok(vec![m])
252    }
253}
254
255impl ReadFromBytes for PlyReader {
256    fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
257        Ok(Self::from_bytes(bytes.to_vec()))
258    }
259}
260
261impl PointCloudReader for PlyReader {
262    fn read_points(&mut self) -> io::Result<Vec<[f32; 3]>> {
263        self.read_positions()
264    }
265}
266
267// ============================================================================
268// Convenience Functions (for backward compatibility)
269// ============================================================================
270
271/// Parse point positions from an ASCII or binary little-endian PLY file.
272/// Returns a vec of [x, y, z] positions.
273pub fn read_ply_positions<P: AsRef<Path>>(path: P) -> io::Result<Vec<[f32; 3]>> {
274    Ok(read_ply(path)?.positions.to_f32_positions())
275}
276
277fn make_f32x3_attribute(
278    attribute_type: GeometryAttributeType,
279    values: &[[f32; 3]],
280) -> PointAttribute {
281    let mut attribute = PointAttribute::new();
282    attribute.init(attribute_type, 3, DataType::Float32, false, values.len());
283
284    let buffer = attribute.buffer_mut();
285    for (i, value) in values.iter().enumerate() {
286        let bytes: Vec<u8> = value
287            .iter()
288            .flat_map(|component| component.to_le_bytes())
289            .collect();
290        buffer.write(i * 12, &bytes);
291    }
292
293    attribute
294}
295
296fn make_f32x2_attribute(
297    attribute_type: GeometryAttributeType,
298    values: &[[f32; 2]],
299) -> PointAttribute {
300    let mut attribute = PointAttribute::new();
301    attribute.init(attribute_type, 2, DataType::Float32, false, values.len());
302
303    let buffer = attribute.buffer_mut();
304    for (i, value) in values.iter().enumerate() {
305        let bytes: Vec<u8> = value
306            .iter()
307            .flat_map(|component| component.to_le_bytes())
308            .collect();
309        buffer.write(i * 8, &bytes);
310    }
311
312    attribute
313}
314
315fn make_i32x3_attribute(
316    attribute_type: GeometryAttributeType,
317    values: &[[i32; 3]],
318) -> PointAttribute {
319    let mut attribute = PointAttribute::new();
320    attribute.init(attribute_type, 3, DataType::Int32, false, values.len());
321
322    let buffer = attribute.buffer_mut();
323    for (i, value) in values.iter().enumerate() {
324        let bytes: Vec<u8> = value
325            .iter()
326            .flat_map(|component| component.to_le_bytes())
327            .collect();
328        buffer.write(i * 12, &bytes);
329    }
330
331    attribute
332}
333
334fn make_u8_attribute(
335    attribute_type: GeometryAttributeType,
336    num_components: u8,
337    normalized: bool,
338    values: &[[u8; 4]],
339) -> PointAttribute {
340    let mut attribute = PointAttribute::new();
341    attribute.init(
342        attribute_type,
343        num_components,
344        DataType::Uint8,
345        normalized,
346        values.len(),
347    );
348
349    let buffer = attribute.buffer_mut();
350    for (i, value) in values.iter().enumerate() {
351        let end = num_components as usize;
352        buffer.write(i * end, &value[..end]);
353    }
354
355    attribute
356}
357
358fn invalid_ply(message: impl Into<String>) -> io::Error {
359    io::Error::new(io::ErrorKind::InvalidData, message.into())
360}
361
362fn parse_ply_property(parts: &[&str]) -> io::Result<PlyPropertyDef> {
363    if parts.len() < 3 {
364        return Err(invalid_ply("Malformed property declaration"));
365    }
366
367    if parts[1] == "list" {
368        if parts.len() < 5 {
369            return Err(invalid_ply("Malformed list property declaration"));
370        }
371        let count_type = parse_ply_scalar_type(parts[2])
372            .ok_or_else(|| invalid_ply(format!("Unsupported PLY scalar type: {}", parts[2])))?;
373        let item_type = parse_ply_scalar_type(parts[3])
374            .ok_or_else(|| invalid_ply(format!("Unsupported PLY scalar type: {}", parts[3])))?;
375        Ok(PlyPropertyDef {
376            name: parts[4].to_string(),
377            kind: PlyPropertyKind::List {
378                count_type,
379                item_type,
380            },
381        })
382    } else {
383        let data_type = parse_ply_scalar_type(parts[1])
384            .ok_or_else(|| invalid_ply(format!("Unsupported PLY scalar type: {}", parts[1])))?;
385        Ok(PlyPropertyDef {
386            name: parts[2].to_string(),
387            kind: PlyPropertyKind::Scalar(data_type),
388        })
389    }
390}
391
392fn parse_ply_header(bytes: &[u8]) -> io::Result<(PlyHeader, usize)> {
393    if bytes.is_empty() {
394        return Err(invalid_ply("Empty PLY file"));
395    }
396
397    let mut body_offset = None;
398    let mut offset = 0usize;
399    while offset < bytes.len() {
400        let line_end = bytes[offset..]
401            .iter()
402            .position(|byte| matches!(*byte, b'\n' | b'\r'))
403            .map(|idx| offset + idx);
404        match line_end {
405            Some(end) => {
406                let line_bytes = &bytes[offset..end];
407                let line = std::str::from_utf8(line_bytes)
408                    .map_err(|_| invalid_ply("PLY header must be valid UTF-8/ASCII"))?;
409                offset = end + 1;
410                if bytes[end] == b'\r' && bytes.get(offset) == Some(&b'\n') {
411                    offset += 1;
412                }
413                if line.trim() == "end_header" {
414                    body_offset = Some(offset);
415                    break;
416                }
417            }
418            None => {
419                let line = std::str::from_utf8(&bytes[offset..])
420                    .map_err(|_| invalid_ply("PLY header must be valid UTF-8/ASCII"))?;
421                if line.trim() == "end_header" {
422                    body_offset = Some(bytes.len());
423                    break;
424                }
425                break;
426            }
427        }
428    }
429
430    let body_offset = body_offset.ok_or_else(|| invalid_ply("No end_header found"))?;
431    let header_text = std::str::from_utf8(&bytes[..body_offset])
432        .map_err(|_| invalid_ply("PLY header must be valid UTF-8/ASCII"))?;
433
434    // PLY writers in the wild use LF, CRLF, and (notably Rhino) CR-only
435    // header lines. `str::lines` does not split CR-only input.
436    let mut lines = header_text.split(['\n', '\r']);
437    let first_line = lines.next().ok_or_else(|| invalid_ply("Empty PLY file"))?;
438    if first_line.trim() != "ply" {
439        return Err(invalid_ply("Missing PLY header"));
440    }
441
442    let mut format = None;
443    let mut vertex_count = 0usize;
444    let mut face_count = 0usize;
445    let mut elements: Vec<PlyElementDef> = Vec::new();
446
447    for line in lines {
448        let trimmed = line.trim();
449        if trimmed.is_empty() || trimmed == "end_header" {
450            continue;
451        }
452
453        let parts: Vec<&str> = trimmed.split_whitespace().collect();
454        if parts.is_empty() {
455            continue;
456        }
457
458        match parts[0] {
459            "comment" | "obj_info" => {}
460            "format" => {
461                if parts.len() < 2 {
462                    return Err(invalid_ply("Malformed format declaration"));
463                }
464                format = Some(match parts[1] {
465                    "ascii" => PlyFormat::Ascii,
466                    "binary_little_endian" => PlyFormat::BinaryLittleEndian,
467                    "binary_big_endian" => PlyFormat::BinaryBigEndian,
468                    other => {
469                        return Err(invalid_ply(format!("Unsupported PLY format: {other}")));
470                    }
471                });
472            }
473            "element" => {
474                if parts.len() < 3 {
475                    return Err(invalid_ply("Malformed element declaration"));
476                }
477                let count = parts[2]
478                    .parse()
479                    .map_err(|_| invalid_ply("Invalid element count"))?;
480                elements.push(PlyElementDef {
481                    name: parts[1].to_string(),
482                    count,
483                    properties: Vec::new(),
484                });
485                match parts[1] {
486                    "vertex" => {
487                        vertex_count = count;
488                    }
489                    "face" => {
490                        face_count = count;
491                    }
492                    _ => {}
493                }
494            }
495            "property" => {
496                let property = parse_ply_property(&parts)?;
497                let Some(element) = elements.last_mut() else {
498                    return Err(invalid_ply("Property declared before element"));
499                };
500                element.properties.push(property);
501            }
502            _ => {}
503        }
504    }
505
506    let mut vertex_properties = Vec::new();
507    let mut face_properties = Vec::new();
508    for element in &elements {
509        match element.name.as_str() {
510            "vertex" => vertex_properties = element.properties.clone(),
511            "face" => face_properties = element.properties.clone(),
512            _ => {}
513        }
514    }
515
516    Ok((
517        PlyHeader {
518            format: format.ok_or_else(|| invalid_ply("Missing PLY format declaration"))?,
519            vertex_count,
520            face_count,
521            elements,
522            vertex_properties,
523            face_properties,
524        },
525        body_offset,
526    ))
527}
528
529fn skip_ascii_element_lines<'a>(lines: &mut std::str::Lines<'a>, count: usize) {
530    for _ in 0..count {
531        let _ = lines.next();
532    }
533}
534
535fn ascii_scalar_token_count(data_type: DataType) -> usize {
536    if data_type == DataType::Invalid {
537        0
538    } else {
539        1
540    }
541}
542
543fn split_ascii_vertex_lines<'a>(
544    header: &PlyHeader,
545    body_text: &'a str,
546) -> io::Result<(Vec<&'a str>, Vec<&'a str>)> {
547    let mut lines = body_text.lines();
548    let mut vertex_lines = Vec::new();
549    let mut face_lines = Vec::new();
550
551    for element in &header.elements {
552        match element.name.as_str() {
553            "vertex" => {
554                for _ in 0..element.count {
555                    if let Some(line) = lines.next() {
556                        vertex_lines.push(line);
557                    }
558                }
559            }
560            "face" => {
561                for _ in 0..element.count {
562                    if let Some(line) = lines.next() {
563                        face_lines.push(line);
564                    }
565                }
566            }
567            _ => skip_ascii_element_lines(&mut lines, element.count),
568        }
569    }
570
571    Ok((vertex_lines, face_lines))
572}
573
574fn position_data_type_for_scalar(data_type: DataType) -> DataType {
575    match data_type {
576        DataType::Int32 => DataType::Int32,
577        _ => DataType::Float32,
578    }
579}
580
581fn scalar_property_type(header: &PlyHeader, name: &str) -> Option<DataType> {
582    header.vertex_properties.iter().find_map(|property| {
583        (property.name == name)
584            .then(|| property.scalar_type())
585            .flatten()
586    })
587}
588
589fn detect_texcoord_pair(header: &PlyHeader) -> io::Result<Option<TexcoordPropertyPair>> {
590    const PAIRS: [TexcoordPropertyPair; 3] = [
591        TexcoordPropertyPair {
592            u: "texture_u",
593            v: "texture_v",
594        },
595        TexcoordPropertyPair { u: "u", v: "v" },
596        TexcoordPropertyPair { u: "s", v: "t" },
597    ];
598
599    for pair in PAIRS {
600        let u_type = scalar_property_type(header, pair.u);
601        let v_type = scalar_property_type(header, pair.v);
602        if u_type.is_some() || v_type.is_some() {
603            if u_type == Some(DataType::Float32) && v_type == Some(DataType::Float32) {
604                return Ok(Some(pair));
605            }
606            return Err(invalid_ply(format!(
607                "Texture coordinate properties {} and {} must both be float",
608                pair.u, pair.v
609            )));
610        }
611    }
612
613    Ok(None)
614}
615
616fn build_read_schema(header: &PlyHeader) -> io::Result<PlyReadSchema> {
617    let mut has_x = false;
618    let mut has_y = false;
619    let mut has_z = false;
620    let mut position_data_type = DataType::Float32;
621    let mut prop_nx_type = None;
622    let mut prop_ny_type = None;
623    let mut prop_nz_type = None;
624    let mut prop_r_type = None;
625    let mut prop_g_type = None;
626    let mut prop_b_type = None;
627    let mut prop_a_type = None;
628
629    for property in &header.vertex_properties {
630        let Some(data_type) = property.scalar_type() else {
631            continue;
632        };
633
634        match property.name.as_str() {
635            "x" => {
636                has_x = true;
637                position_data_type = position_data_type_for_scalar(data_type);
638            }
639            "y" => {
640                has_y = true;
641                position_data_type = position_data_type_for_scalar(data_type);
642            }
643            "z" => {
644                has_z = true;
645                position_data_type = position_data_type_for_scalar(data_type);
646            }
647            "nx" => prop_nx_type = Some(data_type),
648            "ny" => prop_ny_type = Some(data_type),
649            "nz" => prop_nz_type = Some(data_type),
650            "red" => prop_r_type = Some(data_type),
651            "green" => prop_g_type = Some(data_type),
652            "blue" => prop_b_type = Some(data_type),
653            "alpha" => prop_a_type = Some(data_type),
654            _ => {}
655        }
656    }
657
658    if !has_x {
659        return Err(invalid_ply("No x property"));
660    }
661    if !has_y {
662        return Err(invalid_ply("No y property"));
663    }
664    if !has_z {
665        return Err(invalid_ply("No z property"));
666    }
667
668    let has_normals = prop_nx_type == Some(DataType::Float32)
669        && prop_ny_type == Some(DataType::Float32)
670        && prop_nz_type == Some(DataType::Float32);
671
672    let color_types = [prop_r_type, prop_g_type, prop_b_type, prop_a_type];
673    let color_components = color_types.iter().flatten().count() as u8;
674    if color_components > 0 {
675        for color_type in color_types.into_iter().flatten() {
676            if color_type != DataType::Uint8 {
677                return Err(invalid_ply("Color properties must be uint8"));
678            }
679        }
680    }
681
682    Ok(PlyReadSchema {
683        position_data_type,
684        has_normals,
685        color_components,
686        texcoord_pair: detect_texcoord_pair(header)?,
687    })
688}
689
690fn triangulate_vertex_indices(indices: &[u32], faces: &mut Vec<[u32; 3]>) {
691    if indices.len() < 3 {
692        return;
693    }
694
695    for j in 1..indices.len() - 1 {
696        faces.push([indices[0], indices[j], indices[j + 1]]);
697    }
698}
699
700fn parse_ascii_face_line(
701    header: &PlyHeader,
702    line: &str,
703    faces: &mut Vec<[u32; 3]>,
704) -> io::Result<()> {
705    let parts: Vec<&str> = line.split_whitespace().collect();
706    if parts.is_empty() {
707        return Ok(());
708    }
709
710    if header.face_properties.is_empty() {
711        let indices: Vec<u32> = parts
712            .iter()
713            .map(|part| {
714                part.parse::<u32>()
715                    .map_err(|_| invalid_ply("Bad face index value"))
716            })
717            .collect::<io::Result<Vec<u32>>>()?;
718
719        if indices.is_empty() {
720            return Ok(());
721        }
722
723        let polygon_size = indices[0] as usize;
724        if polygon_size < 3 || indices.len() < polygon_size + 1 {
725            return Ok(());
726        }
727
728        triangulate_vertex_indices(&indices[1..polygon_size + 1], faces);
729        return Ok(());
730    }
731
732    let mut cursor = 0usize;
733    let mut polygon_indices: Option<Vec<u32>> = None;
734
735    for property in &header.face_properties {
736        match property.kind {
737            PlyPropertyKind::Scalar(_) => {
738                if cursor >= parts.len() {
739                    return Ok(());
740                }
741                cursor += 1;
742            }
743            PlyPropertyKind::List { .. } => {
744                if cursor >= parts.len() {
745                    return Ok(());
746                }
747                let count: usize = parts[cursor]
748                    .parse()
749                    .map_err(|_| invalid_ply("Bad face list size"))?;
750                cursor += 1;
751                if parts.len() < cursor + count {
752                    return Ok(());
753                }
754
755                let values = parts[cursor..cursor + count]
756                    .iter()
757                    .map(|part| {
758                        part.parse::<u32>()
759                            .map_err(|_| invalid_ply("Bad face index value"))
760                    })
761                    .collect::<io::Result<Vec<u32>>>()?;
762                cursor += count;
763
764                if property.name == "vertex_indices" || polygon_indices.is_none() {
765                    polygon_indices = Some(values);
766                }
767            }
768        }
769    }
770
771    if let Some(indices) = polygon_indices {
772        triangulate_vertex_indices(&indices, faces);
773    }
774
775    Ok(())
776}
777
778fn parse_ascii_f32(token: &str, label: &str) -> io::Result<f32> {
779    token
780        .parse()
781        .map_err(|_| invalid_ply(format!("Bad {label} value")))
782}
783
784fn parse_ascii_i32(token: &str, label: &str) -> io::Result<i32> {
785    token
786        .parse()
787        .map_err(|_| invalid_ply(format!("Bad {label} value")))
788}
789
790fn parse_ascii_u8(token: &str) -> io::Result<u8> {
791    token
792        .parse()
793        .map_err(|_| invalid_ply("Bad color component value"))
794}
795
796fn read_ply_ascii_body(header: &PlyHeader, body: &[u8]) -> io::Result<ParsedPlyData> {
797    let schema = build_read_schema(header)?;
798    let body_text = std::str::from_utf8(body)
799        .map_err(|_| invalid_ply("ASCII PLY payload must be valid UTF-8/ASCII"))?;
800    let (vertex_lines, face_lines) = split_ascii_vertex_lines(header, body_text)?;
801
802    let mut float_positions = matches!(schema.position_data_type, DataType::Float32)
803        .then(|| Vec::with_capacity(header.vertex_count));
804    let mut int_positions = matches!(schema.position_data_type, DataType::Int32)
805        .then(|| Vec::with_capacity(header.vertex_count));
806    let mut normals = schema
807        .has_normals
808        .then(|| Vec::with_capacity(header.vertex_count));
809    let mut colors = (schema.color_components > 0).then(|| ParsedPlyColorData {
810        num_components: schema.color_components,
811        values: Vec::with_capacity(header.vertex_count),
812    });
813    let mut texcoords = schema
814        .texcoord_pair
815        .is_some()
816        .then(|| Vec::with_capacity(header.vertex_count));
817
818    for line in vertex_lines {
819        let trimmed = line.trim();
820        if trimmed.is_empty() {
821            continue;
822        }
823
824        let parts: Vec<&str> = trimmed.split_whitespace().collect();
825        let mut float_position = [0.0f32; 3];
826        let mut int_position = [0i32; 3];
827        let mut normal = [0.0f32; 3];
828        let mut color = [0u8; 4];
829        let mut texcoord = [0.0f32; 2];
830        let mut color_component = 0usize;
831        let mut cursor = 0usize;
832
833        for property in &header.vertex_properties {
834            let Some(data_type) = property.scalar_type() else {
835                if cursor >= parts.len() {
836                    break;
837                }
838                let count: usize = parts[cursor]
839                    .parse()
840                    .map_err(|_| invalid_ply("Bad vertex list size"))?;
841                cursor = cursor
842                    .checked_add(1 + count)
843                    .ok_or_else(|| invalid_ply("ASCII PLY line is too large"))?;
844                continue;
845            };
846            if cursor >= parts.len() {
847                break;
848            }
849            let token = parts[cursor];
850            cursor += ascii_scalar_token_count(data_type);
851
852            match property.name.as_str() {
853                "x" => match schema.position_data_type {
854                    DataType::Int32 => int_position[0] = parse_ascii_i32(token, "x")?,
855                    _ => float_position[0] = parse_ascii_f32(token, "x")?,
856                },
857                "y" => match schema.position_data_type {
858                    DataType::Int32 => int_position[1] = parse_ascii_i32(token, "y")?,
859                    _ => float_position[1] = parse_ascii_f32(token, "y")?,
860                },
861                "z" => match schema.position_data_type {
862                    DataType::Int32 => int_position[2] = parse_ascii_i32(token, "z")?,
863                    _ => float_position[2] = parse_ascii_f32(token, "z")?,
864                },
865                "nx" if schema.has_normals => normal[0] = parse_ascii_f32(token, "nx")?,
866                "ny" if schema.has_normals => normal[1] = parse_ascii_f32(token, "ny")?,
867                "nz" if schema.has_normals => normal[2] = parse_ascii_f32(token, "nz")?,
868                "red" | "green" | "blue" | "alpha" if schema.color_components > 0 => {
869                    color[color_component] = parse_ascii_u8(token)?;
870                    color_component += 1;
871                }
872                name if schema.texcoord_pair.is_some_and(|pair| name == pair.u) => {
873                    texcoord[0] = parse_ascii_f32(token, name)?;
874                }
875                name if schema.texcoord_pair.is_some_and(|pair| name == pair.v) => {
876                    texcoord[1] = parse_ascii_f32(token, name)?;
877                }
878                _ => {}
879            }
880        }
881
882        match schema.position_data_type {
883            DataType::Int32 => int_positions.as_mut().unwrap().push(int_position),
884            _ => float_positions.as_mut().unwrap().push(float_position),
885        }
886
887        if let Some(normals) = normals.as_mut() {
888            normals.push(normal);
889        }
890
891        if let Some(colors) = colors.as_mut() {
892            colors.values.push(color);
893        }
894
895        if let Some(texcoords) = texcoords.as_mut() {
896            texcoords.push(texcoord);
897        }
898    }
899
900    let mut faces = Vec::with_capacity(header.face_count);
901    for line in face_lines {
902        let trimmed = line.trim();
903        if trimmed.is_empty() {
904            continue;
905        }
906        parse_ascii_face_line(header, trimmed, &mut faces)?;
907    }
908
909    Ok(ParsedPlyData {
910        positions: match schema.position_data_type {
911            DataType::Int32 => ParsedPlyPositionData::Int32(int_positions.unwrap_or_default()),
912            _ => ParsedPlyPositionData::Float32(float_positions.unwrap_or_default()),
913        },
914        faces,
915        normals,
916        colors,
917        texcoords,
918    })
919}
920
921fn ensure_remaining(cursor: &Cursor<&[u8]>, bytes_needed: usize) -> io::Result<()> {
922    let position = cursor.position() as usize;
923    let end = position
924        .checked_add(bytes_needed)
925        .ok_or_else(|| invalid_ply("PLY payload is too large"))?;
926    if end > cursor.get_ref().len() {
927        return Err(io::Error::new(
928            io::ErrorKind::UnexpectedEof,
929            "Unexpected end of binary PLY payload",
930        ));
931    }
932    Ok(())
933}
934
935fn skip_binary_scalar(cursor: &mut Cursor<&[u8]>, data_type: DataType) -> io::Result<()> {
936    ensure_remaining(cursor, data_type.byte_length())?;
937    cursor.set_position(cursor.position() + data_type.byte_length() as u64);
938    Ok(())
939}
940
941#[derive(Debug, Clone, Copy)]
942enum BinaryEndian {
943    Little,
944    Big,
945}
946
947fn read_binary_scalar_as_f32(
948    cursor: &mut Cursor<&[u8]>,
949    data_type: DataType,
950    endian: BinaryEndian,
951) -> io::Result<f32> {
952    ensure_remaining(cursor, data_type.byte_length())?;
953    match data_type {
954        DataType::Int8 => cursor.read_i8().map(|value| value as f32),
955        DataType::Uint8 => cursor.read_u8().map(|value| value as f32),
956        DataType::Int16 => match endian {
957            BinaryEndian::Little => cursor.read_i16::<LittleEndian>().map(|value| value as f32),
958            BinaryEndian::Big => cursor.read_i16::<BigEndian>().map(|value| value as f32),
959        },
960        DataType::Uint16 => match endian {
961            BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as f32),
962            BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as f32),
963        },
964        DataType::Int32 => match endian {
965            BinaryEndian::Little => cursor.read_i32::<LittleEndian>().map(|value| value as f32),
966            BinaryEndian::Big => cursor.read_i32::<BigEndian>().map(|value| value as f32),
967        },
968        DataType::Uint32 => match endian {
969            BinaryEndian::Little => cursor.read_u32::<LittleEndian>().map(|value| value as f32),
970            BinaryEndian::Big => cursor.read_u32::<BigEndian>().map(|value| value as f32),
971        },
972        DataType::Int64 => match endian {
973            BinaryEndian::Little => cursor.read_i64::<LittleEndian>().map(|value| value as f32),
974            BinaryEndian::Big => cursor.read_i64::<BigEndian>().map(|value| value as f32),
975        },
976        DataType::Uint64 => match endian {
977            BinaryEndian::Little => cursor.read_u64::<LittleEndian>().map(|value| value as f32),
978            BinaryEndian::Big => cursor.read_u64::<BigEndian>().map(|value| value as f32),
979        },
980        DataType::Float32 => match endian {
981            BinaryEndian::Little => cursor.read_f32::<LittleEndian>(),
982            BinaryEndian::Big => cursor.read_f32::<BigEndian>(),
983        },
984        DataType::Float64 => match endian {
985            BinaryEndian::Little => cursor.read_f64::<LittleEndian>().map(|value| value as f32),
986            BinaryEndian::Big => cursor.read_f64::<BigEndian>().map(|value| value as f32),
987        },
988        _ => Err(invalid_ply("Unsupported binary scalar type")),
989    }
990}
991
992fn read_binary_scalar_as_i32(
993    cursor: &mut Cursor<&[u8]>,
994    data_type: DataType,
995    endian: BinaryEndian,
996) -> io::Result<i32> {
997    ensure_remaining(cursor, data_type.byte_length())?;
998    match data_type {
999        DataType::Int8 => cursor.read_i8().map(|value| value as i32),
1000        DataType::Uint8 => cursor.read_u8().map(|value| value as i32),
1001        DataType::Int16 => match endian {
1002            BinaryEndian::Little => cursor.read_i16::<LittleEndian>().map(|value| value as i32),
1003            BinaryEndian::Big => cursor.read_i16::<BigEndian>().map(|value| value as i32),
1004        },
1005        DataType::Uint16 => match endian {
1006            BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as i32),
1007            BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as i32),
1008        },
1009        DataType::Int32 => match endian {
1010            BinaryEndian::Little => cursor.read_i32::<LittleEndian>(),
1011            BinaryEndian::Big => cursor.read_i32::<BigEndian>(),
1012        },
1013        DataType::Uint32 => {
1014            let value = match endian {
1015                BinaryEndian::Little => cursor.read_u32::<LittleEndian>()?,
1016                BinaryEndian::Big => cursor.read_u32::<BigEndian>()?,
1017            };
1018            i32::try_from(value).map_err(|_| invalid_ply("Binary PLY value does not fit in int32"))
1019        }
1020        _ => Err(invalid_ply("Unsupported binary int32 scalar type")),
1021    }
1022}
1023
1024fn read_binary_scalar_as_u8(cursor: &mut Cursor<&[u8]>, data_type: DataType) -> io::Result<u8> {
1025    ensure_remaining(cursor, data_type.byte_length())?;
1026    match data_type {
1027        DataType::Uint8 => cursor.read_u8(),
1028        DataType::Int8 => {
1029            let value = cursor.read_i8()?;
1030            u8::try_from(value).map_err(|_| invalid_ply("Negative color component value"))
1031        }
1032        _ => Err(invalid_ply("Color properties must be uint8")),
1033    }
1034}
1035
1036fn read_binary_scalar_as_u32(
1037    cursor: &mut Cursor<&[u8]>,
1038    data_type: DataType,
1039    endian: BinaryEndian,
1040) -> io::Result<u32> {
1041    ensure_remaining(cursor, data_type.byte_length())?;
1042    match data_type {
1043        DataType::Uint8 => cursor.read_u8().map(|value| value as u32),
1044        DataType::Int8 => {
1045            let value = cursor.read_i8()?;
1046            u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
1047        }
1048        DataType::Uint16 => match endian {
1049            BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as u32),
1050            BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as u32),
1051        },
1052        DataType::Int16 => {
1053            let value = match endian {
1054                BinaryEndian::Little => cursor.read_i16::<LittleEndian>()?,
1055                BinaryEndian::Big => cursor.read_i16::<BigEndian>()?,
1056            };
1057            u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
1058        }
1059        DataType::Uint32 => match endian {
1060            BinaryEndian::Little => cursor.read_u32::<LittleEndian>(),
1061            BinaryEndian::Big => cursor.read_u32::<BigEndian>(),
1062        },
1063        DataType::Int32 => {
1064            let value = match endian {
1065                BinaryEndian::Little => cursor.read_i32::<LittleEndian>()?,
1066                BinaryEndian::Big => cursor.read_i32::<BigEndian>()?,
1067            };
1068            u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
1069        }
1070        _ => Err(invalid_ply("Unsupported face index scalar type")),
1071    }
1072}
1073
1074fn read_binary_scalar_as_usize(
1075    cursor: &mut Cursor<&[u8]>,
1076    data_type: DataType,
1077    endian: BinaryEndian,
1078) -> io::Result<usize> {
1079    let value = read_binary_scalar_as_u32(cursor, data_type, endian)?;
1080    usize::try_from(value).map_err(|_| invalid_ply("Binary list size is too large"))
1081}
1082
1083fn skip_binary_element(
1084    cursor: &mut Cursor<&[u8]>,
1085    element: &PlyElementDef,
1086    endian: BinaryEndian,
1087) -> io::Result<()> {
1088    for _ in 0..element.count {
1089        for property in &element.properties {
1090            match property.kind {
1091                PlyPropertyKind::Scalar(data_type) => skip_binary_scalar(cursor, data_type)?,
1092                PlyPropertyKind::List {
1093                    count_type,
1094                    item_type,
1095                } => {
1096                    let count = read_binary_scalar_as_usize(cursor, count_type, endian)?;
1097                    for _ in 0..count {
1098                        skip_binary_scalar(cursor, item_type)?;
1099                    }
1100                }
1101            }
1102        }
1103    }
1104    Ok(())
1105}
1106
1107fn read_ply_binary_body(
1108    header: &PlyHeader,
1109    body: &[u8],
1110    endian: BinaryEndian,
1111) -> io::Result<ParsedPlyData> {
1112    let schema = build_read_schema(header)?;
1113    let mut cursor = Cursor::new(body);
1114    let vertex_element_index = header
1115        .elements
1116        .iter()
1117        .position(|element| element.name == "vertex")
1118        .ok_or_else(|| invalid_ply("Missing vertex element"))?;
1119    for element in &header.elements[..vertex_element_index] {
1120        skip_binary_element(&mut cursor, element, endian)?;
1121    }
1122
1123    let mut float_positions = matches!(schema.position_data_type, DataType::Float32)
1124        .then(|| Vec::with_capacity(header.vertex_count));
1125    let mut int_positions = matches!(schema.position_data_type, DataType::Int32)
1126        .then(|| Vec::with_capacity(header.vertex_count));
1127    let mut normals = schema
1128        .has_normals
1129        .then(|| Vec::with_capacity(header.vertex_count));
1130    let mut colors = (schema.color_components > 0).then(|| ParsedPlyColorData {
1131        num_components: schema.color_components,
1132        values: Vec::with_capacity(header.vertex_count),
1133    });
1134    let mut texcoords = schema
1135        .texcoord_pair
1136        .is_some()
1137        .then(|| Vec::with_capacity(header.vertex_count));
1138
1139    for _ in 0..header.vertex_count {
1140        let mut float_position = [0.0f32; 3];
1141        let mut int_position = [0i32; 3];
1142        let mut normal = [0.0f32; 3];
1143        let mut color = [0u8; 4];
1144        let mut texcoord = [0.0f32; 2];
1145        let mut color_component = 0usize;
1146
1147        for property in &header.vertex_properties {
1148            match property.kind {
1149                PlyPropertyKind::Scalar(data_type) => match property.name.as_str() {
1150                    "x" => match schema.position_data_type {
1151                        DataType::Int32 => {
1152                            int_position[0] =
1153                                read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
1154                        }
1155                        _ => {
1156                            float_position[0] =
1157                                read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1158                        }
1159                    },
1160                    "y" => match schema.position_data_type {
1161                        DataType::Int32 => {
1162                            int_position[1] =
1163                                read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
1164                        }
1165                        _ => {
1166                            float_position[1] =
1167                                read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1168                        }
1169                    },
1170                    "z" => match schema.position_data_type {
1171                        DataType::Int32 => {
1172                            int_position[2] =
1173                                read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
1174                        }
1175                        _ => {
1176                            float_position[2] =
1177                                read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1178                        }
1179                    },
1180                    "nx" if schema.has_normals => {
1181                        normal[0] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1182                    }
1183                    "ny" if schema.has_normals => {
1184                        normal[1] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1185                    }
1186                    "nz" if schema.has_normals => {
1187                        normal[2] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1188                    }
1189                    "red" | "green" | "blue" | "alpha" if schema.color_components > 0 => {
1190                        color[color_component] = read_binary_scalar_as_u8(&mut cursor, data_type)?;
1191                        color_component += 1;
1192                    }
1193                    name if schema.texcoord_pair.is_some_and(|pair| name == pair.u) => {
1194                        texcoord[0] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1195                    }
1196                    name if schema.texcoord_pair.is_some_and(|pair| name == pair.v) => {
1197                        texcoord[1] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1198                    }
1199                    _ => skip_binary_scalar(&mut cursor, data_type)?,
1200                },
1201                PlyPropertyKind::List {
1202                    count_type,
1203                    item_type,
1204                } => {
1205                    let count = read_binary_scalar_as_usize(&mut cursor, count_type, endian)?;
1206                    for _ in 0..count {
1207                        skip_binary_scalar(&mut cursor, item_type)?;
1208                    }
1209                }
1210            }
1211        }
1212
1213        match schema.position_data_type {
1214            DataType::Int32 => int_positions.as_mut().unwrap().push(int_position),
1215            _ => float_positions.as_mut().unwrap().push(float_position),
1216        }
1217
1218        if let Some(normals) = normals.as_mut() {
1219            normals.push(normal);
1220        }
1221
1222        if let Some(colors) = colors.as_mut() {
1223            colors.values.push(color);
1224        }
1225
1226        if let Some(texcoords) = texcoords.as_mut() {
1227            texcoords.push(texcoord);
1228        }
1229    }
1230
1231    let face_element_index = header
1232        .elements
1233        .iter()
1234        .position(|element| element.name == "face");
1235    if let Some(face_element_index) = face_element_index {
1236        if face_element_index < vertex_element_index {
1237            return Err(invalid_ply(
1238                "PLY face element before vertex element is not supported",
1239            ));
1240        }
1241        for element in &header.elements[vertex_element_index + 1..face_element_index] {
1242            skip_binary_element(&mut cursor, element, endian)?;
1243        }
1244    }
1245
1246    if header.face_count > 0 && header.face_properties.is_empty() {
1247        return Err(invalid_ply(
1248            "Binary PLY faces require a face property declaration",
1249        ));
1250    }
1251
1252    let mut faces = Vec::with_capacity(header.face_count);
1253    for _ in 0..header.face_count {
1254        let mut polygon_indices: Option<Vec<u32>> = None;
1255
1256        for property in &header.face_properties {
1257            match property.kind {
1258                PlyPropertyKind::Scalar(data_type) => skip_binary_scalar(&mut cursor, data_type)?,
1259                PlyPropertyKind::List {
1260                    count_type,
1261                    item_type,
1262                } => {
1263                    let count = read_binary_scalar_as_usize(&mut cursor, count_type, endian)?;
1264                    let mut values = Vec::with_capacity(count);
1265                    for _ in 0..count {
1266                        values.push(read_binary_scalar_as_u32(&mut cursor, item_type, endian)?);
1267                    }
1268
1269                    if property.name == "vertex_indices" || polygon_indices.is_none() {
1270                        polygon_indices = Some(values);
1271                    }
1272                }
1273            }
1274        }
1275
1276        if let Some(indices) = polygon_indices {
1277            triangulate_vertex_indices(&indices, &mut faces);
1278        }
1279    }
1280
1281    Ok(ParsedPlyData {
1282        positions: match schema.position_data_type {
1283            DataType::Int32 => ParsedPlyPositionData::Int32(int_positions.unwrap_or_default()),
1284            _ => ParsedPlyPositionData::Float32(float_positions.unwrap_or_default()),
1285        },
1286        faces,
1287        normals,
1288        colors,
1289        texcoords,
1290    })
1291}
1292
1293fn read_ply<P: AsRef<Path>>(path: P) -> io::Result<ParsedPlyData> {
1294    let bytes = fs::read(path)?;
1295    read_ply_bytes(&bytes)
1296}
1297
1298fn read_ply_source(source: &PlyReaderSource) -> io::Result<ParsedPlyData> {
1299    match source {
1300        PlyReaderSource::Path(path) => read_ply(path),
1301        PlyReaderSource::Bytes(bytes) => read_ply_bytes(bytes),
1302    }
1303}
1304
1305fn read_ply_bytes(bytes: &[u8]) -> io::Result<ParsedPlyData> {
1306    let (header, body_offset) = parse_ply_header(bytes)?;
1307
1308    match header.format {
1309        PlyFormat::Ascii => read_ply_ascii_body(&header, &bytes[body_offset..]),
1310        PlyFormat::BinaryLittleEndian => {
1311            read_ply_binary_body(&header, &bytes[body_offset..], BinaryEndian::Little)
1312        }
1313        PlyFormat::BinaryBigEndian => {
1314            read_ply_binary_body(&header, &bytes[body_offset..], BinaryEndian::Big)
1315        }
1316    }
1317}
1318
1319/// Write point positions to an ASCII PLY file.
1320pub fn write_ply_positions<P: AsRef<Path>>(path: P, points: &[[f32; 3]]) -> io::Result<()> {
1321    let mut file = fs::File::create(path)?;
1322
1323    writeln!(file, "ply")?;
1324    writeln!(file, "format ascii 1.0")?;
1325    writeln!(file, "element vertex {}", points.len())?;
1326    writeln!(file, "property float x")?;
1327    writeln!(file, "property float y")?;
1328    writeln!(file, "property float z")?;
1329    writeln!(file, "end_header")?;
1330
1331    for p in points {
1332        writeln!(file, "{:.6} {:.6} {:.6}", p[0], p[1], p[2])?;
1333    }
1334
1335    Ok(())
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use super::*;
1341    use draco_core::geometry_attribute::GeometryAttributeType;
1342    use tempfile::NamedTempFile;
1343
1344    #[test]
1345    fn test_read_write_ply() {
1346        let expected = vec![
1347            [0.0, 0.0, 0.0],
1348            [1.0, 0.0, 0.0],
1349            [0.0, 1.0, 0.0],
1350            [0.0, 0.0, 1.0],
1351            [-1.0, -1.0, -1.0],
1352        ];
1353
1354        let file = NamedTempFile::new().unwrap();
1355        write_ply_positions(file.path(), &expected).unwrap();
1356
1357        let positions = read_ply_positions(file.path()).unwrap();
1358        assert_eq!(positions.len(), expected.len());
1359
1360        for (i, (a, b)) in positions.iter().zip(expected.iter()).enumerate() {
1361            let diff = (a[0] - b[0]).abs() + (a[1] - b[1]).abs() + (a[2] - b[2]).abs();
1362            assert!(
1363                diff < 1e-5,
1364                "Position mismatch at index {i}: {a:?} vs {b:?}"
1365            );
1366        }
1367    }
1368
1369    #[test]
1370    fn test_read_mesh_parses_and_triangulates_faces() {
1371        let file = NamedTempFile::new().unwrap();
1372        let ply = r#"ply
1373format ascii 1.0
1374element vertex 4
1375property float x
1376property float y
1377property float z
1378element face 2
1379property list uchar int vertex_indices
1380end_header
13810 0 0
13821 0 0
13831 1 0
13840 1 0
13853 0 1 2
13864 0 1 2 3
1387"#;
1388
1389        std::fs::write(file.path(), ply).unwrap();
1390
1391        let mut reader = PlyReader::open(file.path()).unwrap();
1392        let mesh = reader.read_mesh().unwrap();
1393
1394        assert_eq!(mesh.num_points(), 4);
1395        assert_eq!(mesh.num_faces(), 3);
1396        assert_eq!(
1397            mesh.face(draco_core::geometry_indices::FaceIndex(0)),
1398            [0u32.into(), 1u32.into(), 2u32.into()]
1399        );
1400        assert_eq!(
1401            mesh.face(draco_core::geometry_indices::FaceIndex(1)),
1402            [0u32.into(), 1u32.into(), 2u32.into()]
1403        );
1404        assert_eq!(
1405            mesh.face(draco_core::geometry_indices::FaceIndex(2)),
1406            [0u32.into(), 2u32.into(), 3u32.into()]
1407        );
1408    }
1409
1410    #[test]
1411    fn test_read_mesh_parses_normals_and_colors() {
1412        let file = NamedTempFile::new().unwrap();
1413        let ply = r#"ply
1414format ascii 1.0
1415element vertex 2
1416property float x
1417property float y
1418property float z
1419property float nx
1420property float ny
1421property float nz
1422property uchar red
1423property uchar green
1424property uchar blue
1425property uchar alpha
1426end_header
14270 0 0 0 0 1 10 20 30 40
14281 0 0 0 1 0 50 60 70 80
1429"#;
1430
1431        std::fs::write(file.path(), ply).unwrap();
1432
1433        let mut reader = PlyReader::open(file.path()).unwrap();
1434        let mesh = reader.read_mesh().unwrap();
1435
1436        assert_eq!(mesh.num_points(), 2);
1437        assert_eq!(mesh.num_faces(), 0);
1438        assert_eq!(mesh.num_attributes(), 3);
1439
1440        let normal_att = mesh.named_attribute(GeometryAttributeType::Normal).unwrap();
1441        assert_eq!(normal_att.data_type(), DataType::Float32);
1442        assert_eq!(normal_att.num_components(), 3);
1443        assert!(!normal_att.normalized());
1444
1445        let normal_data = normal_att.buffer().data();
1446        let first_normal = [
1447            f32::from_le_bytes(normal_data[0..4].try_into().unwrap()),
1448            f32::from_le_bytes(normal_data[4..8].try_into().unwrap()),
1449            f32::from_le_bytes(normal_data[8..12].try_into().unwrap()),
1450        ];
1451        assert_eq!(first_normal, [0.0, 0.0, 1.0]);
1452
1453        let color_att = mesh.named_attribute(GeometryAttributeType::Color).unwrap();
1454        assert_eq!(color_att.data_type(), DataType::Uint8);
1455        assert_eq!(color_att.num_components(), 4);
1456        assert!(color_att.normalized());
1457        assert_eq!(color_att.buffer().data(), &[10, 20, 30, 40, 50, 60, 70, 80]);
1458    }
1459
1460    #[test]
1461    fn test_read_mesh_preserves_int32_positions() {
1462        let file = NamedTempFile::new().unwrap();
1463        let ply = r#"ply
1464format ascii 1.0
1465element vertex 2
1466property int x
1467property int y
1468property int z
1469end_header
14701 2 3
14714 5 6
1472"#;
1473
1474        std::fs::write(file.path(), ply).unwrap();
1475
1476        let mut reader = PlyReader::open(file.path()).unwrap();
1477        let mesh = reader.read_mesh().unwrap();
1478
1479        let position_att = mesh
1480            .named_attribute(GeometryAttributeType::Position)
1481            .unwrap();
1482        assert_eq!(position_att.data_type(), DataType::Int32);
1483        assert_eq!(position_att.num_components(), 3);
1484        assert!(!position_att.normalized());
1485
1486        let position_data = position_att.buffer().data();
1487        let first_position = [
1488            i32::from_le_bytes(position_data[0..4].try_into().unwrap()),
1489            i32::from_le_bytes(position_data[4..8].try_into().unwrap()),
1490            i32::from_le_bytes(position_data[8..12].try_into().unwrap()),
1491        ];
1492        assert_eq!(first_position, [1, 2, 3]);
1493    }
1494
1495    #[test]
1496    fn test_read_mesh_ignores_non_float_normals() {
1497        let file = NamedTempFile::new().unwrap();
1498        let ply = r#"ply
1499format ascii 1.0
1500element vertex 1
1501property float x
1502property float y
1503property float z
1504property int nx
1505property int ny
1506property int nz
1507end_header
15080 0 0 0 0 1
1509"#;
1510
1511        std::fs::write(file.path(), ply).unwrap();
1512
1513        let mut reader = PlyReader::open(file.path()).unwrap();
1514        let mesh = reader.read_mesh().unwrap();
1515
1516        assert_eq!(mesh.named_attribute_id(GeometryAttributeType::Normal), -1);
1517    }
1518
1519    #[test]
1520    fn test_read_mesh_rejects_non_uint8_colors() {
1521        let file = NamedTempFile::new().unwrap();
1522        let ply = r#"ply
1523format ascii 1.0
1524element vertex 1
1525property float x
1526property float y
1527property float z
1528property int red
1529property int green
1530property int blue
1531end_header
15320 0 0 1 2 3
1533"#;
1534
1535        std::fs::write(file.path(), ply).unwrap();
1536
1537        let mut reader = PlyReader::open(file.path()).unwrap();
1538        let error = reader.read_mesh().unwrap_err();
1539        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
1540        assert!(error.to_string().contains("Color properties must be uint8"));
1541    }
1542
1543    #[test]
1544    fn test_read_binary_little_endian_mesh() {
1545        let file = NamedTempFile::new().unwrap();
1546        let mut ply = Vec::new();
1547        ply.extend_from_slice(
1548            br#"ply
1549format binary_little_endian 1.0
1550element vertex 4
1551property float x
1552property float y
1553property float z
1554element face 2
1555property list uchar int vertex_indices
1556end_header
1557"#,
1558        );
1559
1560        for vertex in [
1561            [0.0f32, 0.0, 0.0],
1562            [1.0, 0.0, 0.0],
1563            [1.0, 1.0, 0.0],
1564            [0.0, 1.0, 0.0],
1565        ] {
1566            for component in vertex {
1567                ply.extend_from_slice(&component.to_le_bytes());
1568            }
1569        }
1570
1571        ply.push(3);
1572        for index in [0i32, 1, 2] {
1573            ply.extend_from_slice(&index.to_le_bytes());
1574        }
1575
1576        ply.push(4);
1577        for index in [0i32, 1, 2, 3] {
1578            ply.extend_from_slice(&index.to_le_bytes());
1579        }
1580
1581        std::fs::write(file.path(), ply).unwrap();
1582
1583        let mut reader = PlyReader::open(file.path()).unwrap();
1584        let mesh = reader.read_mesh().unwrap();
1585
1586        assert_eq!(mesh.num_points(), 4);
1587        assert_eq!(mesh.num_faces(), 3);
1588        assert_eq!(
1589            mesh.face(draco_core::geometry_indices::FaceIndex(0)),
1590            [0u32.into(), 1u32.into(), 2u32.into()]
1591        );
1592        assert_eq!(
1593            mesh.face(draco_core::geometry_indices::FaceIndex(1)),
1594            [0u32.into(), 1u32.into(), 2u32.into()]
1595        );
1596        assert_eq!(
1597            mesh.face(draco_core::geometry_indices::FaceIndex(2)),
1598            [0u32.into(), 2u32.into(), 3u32.into()]
1599        );
1600    }
1601
1602    #[test]
1603    fn test_read_binary_little_endian_mesh_with_cr_only_header() {
1604        let mut ply = b"ply\rformat binary_little_endian 1.0\relement vertex 24\rproperty float x\rproperty float y\rproperty float z\relement face 1\rproperty list uchar int vertex_indices\rend_header\r".to_vec();
1605        for index in 0..24 {
1606            ply.extend_from_slice(&(index as f32).to_le_bytes());
1607            ply.extend_from_slice(&0.0f32.to_le_bytes());
1608            ply.extend_from_slice(&0.0f32.to_le_bytes());
1609        }
1610        ply.extend_from_slice(&[3]);
1611        for index in [0i32, 1, 2] {
1612            ply.extend_from_slice(&index.to_le_bytes());
1613        }
1614
1615        let mesh =
1616            PlyReader::read_from_bytes(&ply).expect("CR-only binary PLY header should parse");
1617
1618        assert_eq!(mesh.num_points(), 24);
1619        assert!(mesh.num_faces() > 0);
1620    }
1621
1622    #[test]
1623    fn test_read_binary_little_endian_attributes_and_int_positions() {
1624        let file = NamedTempFile::new().unwrap();
1625        let mut ply = Vec::new();
1626        ply.extend_from_slice(
1627            br#"ply
1628format binary_little_endian 1.0
1629element vertex 2
1630property int x
1631property int y
1632property int z
1633property float nx
1634property float ny
1635property float nz
1636property uchar red
1637property uchar green
1638property uchar blue
1639property uchar alpha
1640end_header
1641"#,
1642        );
1643
1644        for (position, normal, color) in [
1645            ([1i32, 2, 3], [0.0f32, 0.0, 1.0], [10u8, 20, 30, 40]),
1646            ([4i32, 5, 6], [0.0f32, 1.0, 0.0], [50u8, 60, 70, 80]),
1647        ] {
1648            for component in position {
1649                ply.extend_from_slice(&component.to_le_bytes());
1650            }
1651            for component in normal {
1652                ply.extend_from_slice(&component.to_le_bytes());
1653            }
1654            ply.extend_from_slice(&color);
1655        }
1656
1657        std::fs::write(file.path(), ply).unwrap();
1658
1659        let mut reader = PlyReader::open(file.path()).unwrap();
1660        let mesh = reader.read_mesh().unwrap();
1661
1662        let position_att = mesh
1663            .named_attribute(GeometryAttributeType::Position)
1664            .unwrap();
1665        assert_eq!(position_att.data_type(), DataType::Int32);
1666        assert_eq!(position_att.num_components(), 3);
1667
1668        let position_data = position_att.buffer().data();
1669        let first_position = [
1670            i32::from_le_bytes(position_data[0..4].try_into().unwrap()),
1671            i32::from_le_bytes(position_data[4..8].try_into().unwrap()),
1672            i32::from_le_bytes(position_data[8..12].try_into().unwrap()),
1673        ];
1674        assert_eq!(first_position, [1, 2, 3]);
1675
1676        let normal_att = mesh.named_attribute(GeometryAttributeType::Normal).unwrap();
1677        assert_eq!(normal_att.data_type(), DataType::Float32);
1678        assert_eq!(normal_att.num_components(), 3);
1679
1680        let normal_data = normal_att.buffer().data();
1681        let first_normal = [
1682            f32::from_le_bytes(normal_data[0..4].try_into().unwrap()),
1683            f32::from_le_bytes(normal_data[4..8].try_into().unwrap()),
1684            f32::from_le_bytes(normal_data[8..12].try_into().unwrap()),
1685        ];
1686        assert_eq!(first_normal, [0.0, 0.0, 1.0]);
1687
1688        let color_att = mesh.named_attribute(GeometryAttributeType::Color).unwrap();
1689        assert_eq!(color_att.data_type(), DataType::Uint8);
1690        assert_eq!(color_att.num_components(), 4);
1691        assert!(color_att.normalized());
1692        assert_eq!(color_att.buffer().data(), &[10, 20, 30, 40, 50, 60, 70, 80]);
1693    }
1694
1695    #[test]
1696    fn test_read_binary_big_endian_mesh() {
1697        let mut ply = Vec::new();
1698        ply.extend_from_slice(
1699            br#"ply
1700format binary_big_endian 1.0
1701element vertex 4
1702property float x
1703property float y
1704property float z
1705element face 1
1706property list uchar int vertex_indices
1707end_header
1708"#,
1709        );
1710
1711        for vertex in [
1712            [0.0f32, 0.0, 0.0],
1713            [1.0, 0.0, 0.0],
1714            [1.0, 1.0, 0.0],
1715            [0.0, 1.0, 0.0],
1716        ] {
1717            for component in vertex {
1718                ply.extend_from_slice(&component.to_be_bytes());
1719            }
1720        }
1721
1722        ply.push(4);
1723        for index in [0i32, 1, 2, 3] {
1724            ply.extend_from_slice(&index.to_be_bytes());
1725        }
1726
1727        let mesh = PlyReader::read_from_bytes(&ply).unwrap();
1728        assert_eq!(mesh.num_points(), 4);
1729        assert_eq!(mesh.num_faces(), 2);
1730        assert_eq!(
1731            mesh.face(draco_core::geometry_indices::FaceIndex(1)),
1732            [0u32.into(), 2u32.into(), 3u32.into()]
1733        );
1734    }
1735}