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
700/// Which face property carries the polygon's corner indices.
701///
702/// `vertex_indices` when the file names it, and otherwise the first list on the
703/// element, which is what a file spelling it `vertex_index` leaves behind. Every
704/// other list there describes the face rather than being it — per-corner
705/// texture coordinates, most often, which Draco's own encoder writes next to the
706/// indices — so it is skipped whatever scalar type it declares. Choosing the
707/// property before the values are read is the point: reading each list in turn
708/// and keeping the first one made the choice depend on the declaration order,
709/// and made a float list next to the indices a parse error.
710fn face_index_property(properties: &[PlyPropertyDef]) -> Option<usize> {
711    let lists = || {
712        properties
713            .iter()
714            .enumerate()
715            .filter(|(_, property)| matches!(property.kind, PlyPropertyKind::List { .. }))
716    };
717    lists()
718        .find(|(_, property)| property.name == "vertex_indices")
719        .or_else(|| lists().next())
720        .map(|(index, _)| index)
721}
722
723fn parse_ascii_face_line(
724    header: &PlyHeader,
725    line: &str,
726    faces: &mut Vec<[u32; 3]>,
727) -> io::Result<()> {
728    let parts: Vec<&str> = line.split_whitespace().collect();
729    if parts.is_empty() {
730        return Ok(());
731    }
732
733    if header.face_properties.is_empty() {
734        let indices: Vec<u32> = parts
735            .iter()
736            .map(|part| {
737                part.parse::<u32>()
738                    .map_err(|_| invalid_ply("Bad face index value"))
739            })
740            .collect::<io::Result<Vec<u32>>>()?;
741
742        if indices.is_empty() {
743            return Ok(());
744        }
745
746        let polygon_size = indices[0] as usize;
747        if polygon_size < 3 || indices.len() < polygon_size + 1 {
748            return Ok(());
749        }
750
751        triangulate_vertex_indices(&indices[1..polygon_size + 1], faces);
752        return Ok(());
753    }
754
755    let index_property = face_index_property(&header.face_properties);
756    let mut cursor = 0usize;
757    let mut polygon_indices: Option<Vec<u32>> = None;
758
759    for (position, property) in header.face_properties.iter().enumerate() {
760        match property.kind {
761            PlyPropertyKind::Scalar(_) => {
762                if cursor >= parts.len() {
763                    return Ok(());
764                }
765                cursor += 1;
766            }
767            PlyPropertyKind::List { .. } => {
768                if cursor >= parts.len() {
769                    return Ok(());
770                }
771                let count: usize = parts[cursor]
772                    .parse()
773                    .map_err(|_| invalid_ply("Bad face list size"))?;
774                cursor += 1;
775                if parts.len() < cursor + count {
776                    return Ok(());
777                }
778
779                if index_property == Some(position) {
780                    polygon_indices = Some(
781                        parts[cursor..cursor + count]
782                            .iter()
783                            .map(|part| {
784                                part.parse::<u32>()
785                                    .map_err(|_| invalid_ply("Bad face index value"))
786                            })
787                            .collect::<io::Result<Vec<u32>>>()?,
788                    );
789                }
790                cursor += count;
791            }
792        }
793    }
794
795    if let Some(indices) = polygon_indices {
796        triangulate_vertex_indices(&indices, faces);
797    }
798
799    Ok(())
800}
801
802fn parse_ascii_f32(token: &str, label: &str) -> io::Result<f32> {
803    token
804        .parse()
805        .map_err(|_| invalid_ply(format!("Bad {label} value")))
806}
807
808fn parse_ascii_i32(token: &str, label: &str) -> io::Result<i32> {
809    token
810        .parse()
811        .map_err(|_| invalid_ply(format!("Bad {label} value")))
812}
813
814fn parse_ascii_u8(token: &str) -> io::Result<u8> {
815    token
816        .parse()
817        .map_err(|_| invalid_ply("Bad color component value"))
818}
819
820fn read_ply_ascii_body(header: &PlyHeader, body: &[u8]) -> io::Result<ParsedPlyData> {
821    let schema = build_read_schema(header)?;
822    let body_text = std::str::from_utf8(body)
823        .map_err(|_| invalid_ply("ASCII PLY payload must be valid UTF-8/ASCII"))?;
824    let (vertex_lines, face_lines) = split_ascii_vertex_lines(header, body_text)?;
825
826    let mut float_positions = matches!(schema.position_data_type, DataType::Float32)
827        .then(|| Vec::with_capacity(header.vertex_count));
828    let mut int_positions = matches!(schema.position_data_type, DataType::Int32)
829        .then(|| Vec::with_capacity(header.vertex_count));
830    let mut normals = schema
831        .has_normals
832        .then(|| Vec::with_capacity(header.vertex_count));
833    let mut colors = (schema.color_components > 0).then(|| ParsedPlyColorData {
834        num_components: schema.color_components,
835        values: Vec::with_capacity(header.vertex_count),
836    });
837    let mut texcoords = schema
838        .texcoord_pair
839        .is_some()
840        .then(|| Vec::with_capacity(header.vertex_count));
841
842    for line in vertex_lines {
843        let trimmed = line.trim();
844        if trimmed.is_empty() {
845            continue;
846        }
847
848        let parts: Vec<&str> = trimmed.split_whitespace().collect();
849        let mut float_position = [0.0f32; 3];
850        let mut int_position = [0i32; 3];
851        let mut normal = [0.0f32; 3];
852        let mut color = [0u8; 4];
853        let mut texcoord = [0.0f32; 2];
854        let mut color_component = 0usize;
855        let mut cursor = 0usize;
856
857        for property in &header.vertex_properties {
858            let Some(data_type) = property.scalar_type() else {
859                if cursor >= parts.len() {
860                    break;
861                }
862                let count: usize = parts[cursor]
863                    .parse()
864                    .map_err(|_| invalid_ply("Bad vertex list size"))?;
865                cursor = cursor
866                    .checked_add(1 + count)
867                    .ok_or_else(|| invalid_ply("ASCII PLY line is too large"))?;
868                continue;
869            };
870            if cursor >= parts.len() {
871                break;
872            }
873            let token = parts[cursor];
874            cursor += ascii_scalar_token_count(data_type);
875
876            match property.name.as_str() {
877                "x" => match schema.position_data_type {
878                    DataType::Int32 => int_position[0] = parse_ascii_i32(token, "x")?,
879                    _ => float_position[0] = parse_ascii_f32(token, "x")?,
880                },
881                "y" => match schema.position_data_type {
882                    DataType::Int32 => int_position[1] = parse_ascii_i32(token, "y")?,
883                    _ => float_position[1] = parse_ascii_f32(token, "y")?,
884                },
885                "z" => match schema.position_data_type {
886                    DataType::Int32 => int_position[2] = parse_ascii_i32(token, "z")?,
887                    _ => float_position[2] = parse_ascii_f32(token, "z")?,
888                },
889                "nx" if schema.has_normals => normal[0] = parse_ascii_f32(token, "nx")?,
890                "ny" if schema.has_normals => normal[1] = parse_ascii_f32(token, "ny")?,
891                "nz" if schema.has_normals => normal[2] = parse_ascii_f32(token, "nz")?,
892                "red" | "green" | "blue" | "alpha" if schema.color_components > 0 => {
893                    color[color_component] = parse_ascii_u8(token)?;
894                    color_component += 1;
895                }
896                name if schema.texcoord_pair.is_some_and(|pair| name == pair.u) => {
897                    texcoord[0] = parse_ascii_f32(token, name)?;
898                }
899                name if schema.texcoord_pair.is_some_and(|pair| name == pair.v) => {
900                    texcoord[1] = parse_ascii_f32(token, name)?;
901                }
902                _ => {}
903            }
904        }
905
906        match schema.position_data_type {
907            DataType::Int32 => int_positions.as_mut().unwrap().push(int_position),
908            _ => float_positions.as_mut().unwrap().push(float_position),
909        }
910
911        if let Some(normals) = normals.as_mut() {
912            normals.push(normal);
913        }
914
915        if let Some(colors) = colors.as_mut() {
916            colors.values.push(color);
917        }
918
919        if let Some(texcoords) = texcoords.as_mut() {
920            texcoords.push(texcoord);
921        }
922    }
923
924    let mut faces = Vec::with_capacity(header.face_count);
925    for line in face_lines {
926        let trimmed = line.trim();
927        if trimmed.is_empty() {
928            continue;
929        }
930        parse_ascii_face_line(header, trimmed, &mut faces)?;
931    }
932
933    Ok(ParsedPlyData {
934        positions: match schema.position_data_type {
935            DataType::Int32 => ParsedPlyPositionData::Int32(int_positions.unwrap_or_default()),
936            _ => ParsedPlyPositionData::Float32(float_positions.unwrap_or_default()),
937        },
938        faces,
939        normals,
940        colors,
941        texcoords,
942    })
943}
944
945fn ensure_remaining(cursor: &Cursor<&[u8]>, bytes_needed: usize) -> io::Result<()> {
946    let position = cursor.position() as usize;
947    let end = position
948        .checked_add(bytes_needed)
949        .ok_or_else(|| invalid_ply("PLY payload is too large"))?;
950    if end > cursor.get_ref().len() {
951        return Err(io::Error::new(
952            io::ErrorKind::UnexpectedEof,
953            "Unexpected end of binary PLY payload",
954        ));
955    }
956    Ok(())
957}
958
959fn skip_binary_scalar(cursor: &mut Cursor<&[u8]>, data_type: DataType) -> io::Result<()> {
960    ensure_remaining(cursor, data_type.byte_length())?;
961    cursor.set_position(cursor.position() + data_type.byte_length() as u64);
962    Ok(())
963}
964
965#[derive(Debug, Clone, Copy)]
966enum BinaryEndian {
967    Little,
968    Big,
969}
970
971fn read_binary_scalar_as_f32(
972    cursor: &mut Cursor<&[u8]>,
973    data_type: DataType,
974    endian: BinaryEndian,
975) -> io::Result<f32> {
976    ensure_remaining(cursor, data_type.byte_length())?;
977    match data_type {
978        DataType::Int8 => cursor.read_i8().map(|value| value as f32),
979        DataType::Uint8 => cursor.read_u8().map(|value| value as f32),
980        DataType::Int16 => match endian {
981            BinaryEndian::Little => cursor.read_i16::<LittleEndian>().map(|value| value as f32),
982            BinaryEndian::Big => cursor.read_i16::<BigEndian>().map(|value| value as f32),
983        },
984        DataType::Uint16 => match endian {
985            BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as f32),
986            BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as f32),
987        },
988        DataType::Int32 => match endian {
989            BinaryEndian::Little => cursor.read_i32::<LittleEndian>().map(|value| value as f32),
990            BinaryEndian::Big => cursor.read_i32::<BigEndian>().map(|value| value as f32),
991        },
992        DataType::Uint32 => match endian {
993            BinaryEndian::Little => cursor.read_u32::<LittleEndian>().map(|value| value as f32),
994            BinaryEndian::Big => cursor.read_u32::<BigEndian>().map(|value| value as f32),
995        },
996        DataType::Int64 => match endian {
997            BinaryEndian::Little => cursor.read_i64::<LittleEndian>().map(|value| value as f32),
998            BinaryEndian::Big => cursor.read_i64::<BigEndian>().map(|value| value as f32),
999        },
1000        DataType::Uint64 => match endian {
1001            BinaryEndian::Little => cursor.read_u64::<LittleEndian>().map(|value| value as f32),
1002            BinaryEndian::Big => cursor.read_u64::<BigEndian>().map(|value| value as f32),
1003        },
1004        DataType::Float32 => match endian {
1005            BinaryEndian::Little => cursor.read_f32::<LittleEndian>(),
1006            BinaryEndian::Big => cursor.read_f32::<BigEndian>(),
1007        },
1008        DataType::Float64 => match endian {
1009            BinaryEndian::Little => cursor.read_f64::<LittleEndian>().map(|value| value as f32),
1010            BinaryEndian::Big => cursor.read_f64::<BigEndian>().map(|value| value as f32),
1011        },
1012        _ => Err(invalid_ply("Unsupported binary scalar type")),
1013    }
1014}
1015
1016fn read_binary_scalar_as_i32(
1017    cursor: &mut Cursor<&[u8]>,
1018    data_type: DataType,
1019    endian: BinaryEndian,
1020) -> io::Result<i32> {
1021    ensure_remaining(cursor, data_type.byte_length())?;
1022    match data_type {
1023        DataType::Int8 => cursor.read_i8().map(|value| value as i32),
1024        DataType::Uint8 => cursor.read_u8().map(|value| value as i32),
1025        DataType::Int16 => match endian {
1026            BinaryEndian::Little => cursor.read_i16::<LittleEndian>().map(|value| value as i32),
1027            BinaryEndian::Big => cursor.read_i16::<BigEndian>().map(|value| value as i32),
1028        },
1029        DataType::Uint16 => match endian {
1030            BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as i32),
1031            BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as i32),
1032        },
1033        DataType::Int32 => match endian {
1034            BinaryEndian::Little => cursor.read_i32::<LittleEndian>(),
1035            BinaryEndian::Big => cursor.read_i32::<BigEndian>(),
1036        },
1037        DataType::Uint32 => {
1038            let value = match endian {
1039                BinaryEndian::Little => cursor.read_u32::<LittleEndian>()?,
1040                BinaryEndian::Big => cursor.read_u32::<BigEndian>()?,
1041            };
1042            i32::try_from(value).map_err(|_| invalid_ply("Binary PLY value does not fit in int32"))
1043        }
1044        _ => Err(invalid_ply("Unsupported binary int32 scalar type")),
1045    }
1046}
1047
1048fn read_binary_scalar_as_u8(cursor: &mut Cursor<&[u8]>, data_type: DataType) -> io::Result<u8> {
1049    ensure_remaining(cursor, data_type.byte_length())?;
1050    match data_type {
1051        DataType::Uint8 => cursor.read_u8(),
1052        DataType::Int8 => {
1053            let value = cursor.read_i8()?;
1054            u8::try_from(value).map_err(|_| invalid_ply("Negative color component value"))
1055        }
1056        _ => Err(invalid_ply("Color properties must be uint8")),
1057    }
1058}
1059
1060fn read_binary_scalar_as_u32(
1061    cursor: &mut Cursor<&[u8]>,
1062    data_type: DataType,
1063    endian: BinaryEndian,
1064) -> io::Result<u32> {
1065    ensure_remaining(cursor, data_type.byte_length())?;
1066    match data_type {
1067        DataType::Uint8 => cursor.read_u8().map(|value| value as u32),
1068        DataType::Int8 => {
1069            let value = cursor.read_i8()?;
1070            u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
1071        }
1072        DataType::Uint16 => match endian {
1073            BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as u32),
1074            BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as u32),
1075        },
1076        DataType::Int16 => {
1077            let value = match endian {
1078                BinaryEndian::Little => cursor.read_i16::<LittleEndian>()?,
1079                BinaryEndian::Big => cursor.read_i16::<BigEndian>()?,
1080            };
1081            u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
1082        }
1083        DataType::Uint32 => match endian {
1084            BinaryEndian::Little => cursor.read_u32::<LittleEndian>(),
1085            BinaryEndian::Big => cursor.read_u32::<BigEndian>(),
1086        },
1087        DataType::Int32 => {
1088            let value = match endian {
1089                BinaryEndian::Little => cursor.read_i32::<LittleEndian>()?,
1090                BinaryEndian::Big => cursor.read_i32::<BigEndian>()?,
1091            };
1092            u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
1093        }
1094        _ => Err(invalid_ply("Unsupported face index scalar type")),
1095    }
1096}
1097
1098fn read_binary_scalar_as_usize(
1099    cursor: &mut Cursor<&[u8]>,
1100    data_type: DataType,
1101    endian: BinaryEndian,
1102) -> io::Result<usize> {
1103    let value = read_binary_scalar_as_u32(cursor, data_type, endian)?;
1104    usize::try_from(value).map_err(|_| invalid_ply("Binary list size is too large"))
1105}
1106
1107fn skip_binary_element(
1108    cursor: &mut Cursor<&[u8]>,
1109    element: &PlyElementDef,
1110    endian: BinaryEndian,
1111) -> io::Result<()> {
1112    for _ in 0..element.count {
1113        for property in &element.properties {
1114            match property.kind {
1115                PlyPropertyKind::Scalar(data_type) => skip_binary_scalar(cursor, data_type)?,
1116                PlyPropertyKind::List {
1117                    count_type,
1118                    item_type,
1119                } => {
1120                    let count = read_binary_scalar_as_usize(cursor, count_type, endian)?;
1121                    for _ in 0..count {
1122                        skip_binary_scalar(cursor, item_type)?;
1123                    }
1124                }
1125            }
1126        }
1127    }
1128    Ok(())
1129}
1130
1131fn read_ply_binary_body(
1132    header: &PlyHeader,
1133    body: &[u8],
1134    endian: BinaryEndian,
1135) -> io::Result<ParsedPlyData> {
1136    let schema = build_read_schema(header)?;
1137    let mut cursor = Cursor::new(body);
1138    let vertex_element_index = header
1139        .elements
1140        .iter()
1141        .position(|element| element.name == "vertex")
1142        .ok_or_else(|| invalid_ply("Missing vertex element"))?;
1143    for element in &header.elements[..vertex_element_index] {
1144        skip_binary_element(&mut cursor, element, endian)?;
1145    }
1146
1147    let mut float_positions = matches!(schema.position_data_type, DataType::Float32)
1148        .then(|| Vec::with_capacity(header.vertex_count));
1149    let mut int_positions = matches!(schema.position_data_type, DataType::Int32)
1150        .then(|| Vec::with_capacity(header.vertex_count));
1151    let mut normals = schema
1152        .has_normals
1153        .then(|| Vec::with_capacity(header.vertex_count));
1154    let mut colors = (schema.color_components > 0).then(|| ParsedPlyColorData {
1155        num_components: schema.color_components,
1156        values: Vec::with_capacity(header.vertex_count),
1157    });
1158    let mut texcoords = schema
1159        .texcoord_pair
1160        .is_some()
1161        .then(|| Vec::with_capacity(header.vertex_count));
1162
1163    for _ in 0..header.vertex_count {
1164        let mut float_position = [0.0f32; 3];
1165        let mut int_position = [0i32; 3];
1166        let mut normal = [0.0f32; 3];
1167        let mut color = [0u8; 4];
1168        let mut texcoord = [0.0f32; 2];
1169        let mut color_component = 0usize;
1170
1171        for property in &header.vertex_properties {
1172            match property.kind {
1173                PlyPropertyKind::Scalar(data_type) => match property.name.as_str() {
1174                    "x" => match schema.position_data_type {
1175                        DataType::Int32 => {
1176                            int_position[0] =
1177                                read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
1178                        }
1179                        _ => {
1180                            float_position[0] =
1181                                read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1182                        }
1183                    },
1184                    "y" => match schema.position_data_type {
1185                        DataType::Int32 => {
1186                            int_position[1] =
1187                                read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
1188                        }
1189                        _ => {
1190                            float_position[1] =
1191                                read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1192                        }
1193                    },
1194                    "z" => match schema.position_data_type {
1195                        DataType::Int32 => {
1196                            int_position[2] =
1197                                read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
1198                        }
1199                        _ => {
1200                            float_position[2] =
1201                                read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1202                        }
1203                    },
1204                    "nx" if schema.has_normals => {
1205                        normal[0] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1206                    }
1207                    "ny" if schema.has_normals => {
1208                        normal[1] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1209                    }
1210                    "nz" if schema.has_normals => {
1211                        normal[2] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1212                    }
1213                    "red" | "green" | "blue" | "alpha" if schema.color_components > 0 => {
1214                        color[color_component] = read_binary_scalar_as_u8(&mut cursor, data_type)?;
1215                        color_component += 1;
1216                    }
1217                    name if schema.texcoord_pair.is_some_and(|pair| name == pair.u) => {
1218                        texcoord[0] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1219                    }
1220                    name if schema.texcoord_pair.is_some_and(|pair| name == pair.v) => {
1221                        texcoord[1] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
1222                    }
1223                    _ => skip_binary_scalar(&mut cursor, data_type)?,
1224                },
1225                PlyPropertyKind::List {
1226                    count_type,
1227                    item_type,
1228                } => {
1229                    let count = read_binary_scalar_as_usize(&mut cursor, count_type, endian)?;
1230                    for _ in 0..count {
1231                        skip_binary_scalar(&mut cursor, item_type)?;
1232                    }
1233                }
1234            }
1235        }
1236
1237        match schema.position_data_type {
1238            DataType::Int32 => int_positions.as_mut().unwrap().push(int_position),
1239            _ => float_positions.as_mut().unwrap().push(float_position),
1240        }
1241
1242        if let Some(normals) = normals.as_mut() {
1243            normals.push(normal);
1244        }
1245
1246        if let Some(colors) = colors.as_mut() {
1247            colors.values.push(color);
1248        }
1249
1250        if let Some(texcoords) = texcoords.as_mut() {
1251            texcoords.push(texcoord);
1252        }
1253    }
1254
1255    let face_element_index = header
1256        .elements
1257        .iter()
1258        .position(|element| element.name == "face");
1259    if let Some(face_element_index) = face_element_index {
1260        if face_element_index < vertex_element_index {
1261            return Err(invalid_ply(
1262                "PLY face element before vertex element is not supported",
1263            ));
1264        }
1265        for element in &header.elements[vertex_element_index + 1..face_element_index] {
1266            skip_binary_element(&mut cursor, element, endian)?;
1267        }
1268    }
1269
1270    if header.face_count > 0 && header.face_properties.is_empty() {
1271        return Err(invalid_ply(
1272            "Binary PLY faces require a face property declaration",
1273        ));
1274    }
1275
1276    let index_property = face_index_property(&header.face_properties);
1277    let mut faces = Vec::with_capacity(header.face_count);
1278    for _ in 0..header.face_count {
1279        let mut polygon_indices: Option<Vec<u32>> = None;
1280
1281        for (position, property) in header.face_properties.iter().enumerate() {
1282            match property.kind {
1283                PlyPropertyKind::Scalar(data_type) => skip_binary_scalar(&mut cursor, data_type)?,
1284                PlyPropertyKind::List {
1285                    count_type,
1286                    item_type,
1287                } => {
1288                    let count = read_binary_scalar_as_usize(&mut cursor, count_type, endian)?;
1289                    if index_property == Some(position) {
1290                        let mut values = Vec::with_capacity(count);
1291                        for _ in 0..count {
1292                            values.push(read_binary_scalar_as_u32(&mut cursor, item_type, endian)?);
1293                        }
1294                        polygon_indices = Some(values);
1295                    } else {
1296                        for _ in 0..count {
1297                            skip_binary_scalar(&mut cursor, item_type)?;
1298                        }
1299                    }
1300                }
1301            }
1302        }
1303
1304        if let Some(indices) = polygon_indices {
1305            triangulate_vertex_indices(&indices, &mut faces);
1306        }
1307    }
1308
1309    Ok(ParsedPlyData {
1310        positions: match schema.position_data_type {
1311            DataType::Int32 => ParsedPlyPositionData::Int32(int_positions.unwrap_or_default()),
1312            _ => ParsedPlyPositionData::Float32(float_positions.unwrap_or_default()),
1313        },
1314        faces,
1315        normals,
1316        colors,
1317        texcoords,
1318    })
1319}
1320
1321fn read_ply<P: AsRef<Path>>(path: P) -> io::Result<ParsedPlyData> {
1322    let bytes = fs::read(path)?;
1323    read_ply_bytes(&bytes)
1324}
1325
1326fn read_ply_source(source: &PlyReaderSource) -> io::Result<ParsedPlyData> {
1327    match source {
1328        PlyReaderSource::Path(path) => read_ply(path),
1329        PlyReaderSource::Bytes(bytes) => read_ply_bytes(bytes),
1330    }
1331}
1332
1333fn read_ply_bytes(bytes: &[u8]) -> io::Result<ParsedPlyData> {
1334    let (header, body_offset) = parse_ply_header(bytes)?;
1335
1336    match header.format {
1337        PlyFormat::Ascii => read_ply_ascii_body(&header, &bytes[body_offset..]),
1338        PlyFormat::BinaryLittleEndian => {
1339            read_ply_binary_body(&header, &bytes[body_offset..], BinaryEndian::Little)
1340        }
1341        PlyFormat::BinaryBigEndian => {
1342            read_ply_binary_body(&header, &bytes[body_offset..], BinaryEndian::Big)
1343        }
1344    }
1345}
1346
1347/// Write point positions to an ASCII PLY file.
1348pub fn write_ply_positions<P: AsRef<Path>>(path: P, points: &[[f32; 3]]) -> io::Result<()> {
1349    let mut file = fs::File::create(path)?;
1350
1351    writeln!(file, "ply")?;
1352    writeln!(file, "format ascii 1.0")?;
1353    writeln!(file, "element vertex {}", points.len())?;
1354    writeln!(file, "property float x")?;
1355    writeln!(file, "property float y")?;
1356    writeln!(file, "property float z")?;
1357    writeln!(file, "end_header")?;
1358
1359    for p in points {
1360        writeln!(file, "{:.6} {:.6} {:.6}", p[0], p[1], p[2])?;
1361    }
1362
1363    Ok(())
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368    use super::*;
1369    use draco_core::geometry_attribute::GeometryAttributeType;
1370    use tempfile::NamedTempFile;
1371
1372    #[test]
1373    fn test_read_write_ply() {
1374        let expected = vec![
1375            [0.0, 0.0, 0.0],
1376            [1.0, 0.0, 0.0],
1377            [0.0, 1.0, 0.0],
1378            [0.0, 0.0, 1.0],
1379            [-1.0, -1.0, -1.0],
1380        ];
1381
1382        let file = NamedTempFile::new().unwrap();
1383        write_ply_positions(file.path(), &expected).unwrap();
1384
1385        let positions = read_ply_positions(file.path()).unwrap();
1386        assert_eq!(positions.len(), expected.len());
1387
1388        for (i, (a, b)) in positions.iter().zip(expected.iter()).enumerate() {
1389            let diff = (a[0] - b[0]).abs() + (a[1] - b[1]).abs() + (a[2] - b[2]).abs();
1390            assert!(
1391                diff < 1e-5,
1392                "Position mismatch at index {i}: {a:?} vs {b:?}"
1393            );
1394        }
1395    }
1396
1397    #[test]
1398    fn test_read_mesh_parses_and_triangulates_faces() {
1399        let file = NamedTempFile::new().unwrap();
1400        let ply = r#"ply
1401format ascii 1.0
1402element vertex 4
1403property float x
1404property float y
1405property float z
1406element face 2
1407property list uchar int vertex_indices
1408end_header
14090 0 0
14101 0 0
14111 1 0
14120 1 0
14133 0 1 2
14144 0 1 2 3
1415"#;
1416
1417        std::fs::write(file.path(), ply).unwrap();
1418
1419        let mut reader = PlyReader::open(file.path()).unwrap();
1420        let mesh = reader.read_mesh().unwrap();
1421
1422        assert_eq!(mesh.num_points(), 4);
1423        assert_eq!(mesh.num_faces(), 3);
1424        assert_eq!(
1425            mesh.face(draco_core::geometry_indices::FaceIndex(0)),
1426            [0u32.into(), 1u32.into(), 2u32.into()]
1427        );
1428        assert_eq!(
1429            mesh.face(draco_core::geometry_indices::FaceIndex(1)),
1430            [0u32.into(), 1u32.into(), 2u32.into()]
1431        );
1432        assert_eq!(
1433            mesh.face(draco_core::geometry_indices::FaceIndex(2)),
1434            [0u32.into(), 2u32.into(), 3u32.into()]
1435        );
1436    }
1437
1438    #[test]
1439    fn test_read_mesh_parses_normals_and_colors() {
1440        let file = NamedTempFile::new().unwrap();
1441        let ply = r#"ply
1442format ascii 1.0
1443element vertex 2
1444property float x
1445property float y
1446property float z
1447property float nx
1448property float ny
1449property float nz
1450property uchar red
1451property uchar green
1452property uchar blue
1453property uchar alpha
1454end_header
14550 0 0 0 0 1 10 20 30 40
14561 0 0 0 1 0 50 60 70 80
1457"#;
1458
1459        std::fs::write(file.path(), ply).unwrap();
1460
1461        let mut reader = PlyReader::open(file.path()).unwrap();
1462        let mesh = reader.read_mesh().unwrap();
1463
1464        assert_eq!(mesh.num_points(), 2);
1465        assert_eq!(mesh.num_faces(), 0);
1466        assert_eq!(mesh.num_attributes(), 3);
1467
1468        let normal_att = mesh.named_attribute(GeometryAttributeType::Normal).unwrap();
1469        assert_eq!(normal_att.data_type(), DataType::Float32);
1470        assert_eq!(normal_att.num_components(), 3);
1471        assert!(!normal_att.normalized());
1472
1473        let normal_data = normal_att.buffer().data();
1474        let first_normal = [
1475            f32::from_le_bytes(normal_data[0..4].try_into().unwrap()),
1476            f32::from_le_bytes(normal_data[4..8].try_into().unwrap()),
1477            f32::from_le_bytes(normal_data[8..12].try_into().unwrap()),
1478        ];
1479        assert_eq!(first_normal, [0.0, 0.0, 1.0]);
1480
1481        let color_att = mesh.named_attribute(GeometryAttributeType::Color).unwrap();
1482        assert_eq!(color_att.data_type(), DataType::Uint8);
1483        assert_eq!(color_att.num_components(), 4);
1484        assert!(color_att.normalized());
1485        assert_eq!(color_att.buffer().data(), &[10, 20, 30, 40, 50, 60, 70, 80]);
1486    }
1487
1488    #[test]
1489    fn test_read_mesh_preserves_int32_positions() {
1490        let file = NamedTempFile::new().unwrap();
1491        let ply = r#"ply
1492format ascii 1.0
1493element vertex 2
1494property int x
1495property int y
1496property int z
1497end_header
14981 2 3
14994 5 6
1500"#;
1501
1502        std::fs::write(file.path(), ply).unwrap();
1503
1504        let mut reader = PlyReader::open(file.path()).unwrap();
1505        let mesh = reader.read_mesh().unwrap();
1506
1507        let position_att = mesh
1508            .named_attribute(GeometryAttributeType::Position)
1509            .unwrap();
1510        assert_eq!(position_att.data_type(), DataType::Int32);
1511        assert_eq!(position_att.num_components(), 3);
1512        assert!(!position_att.normalized());
1513
1514        let position_data = position_att.buffer().data();
1515        let first_position = [
1516            i32::from_le_bytes(position_data[0..4].try_into().unwrap()),
1517            i32::from_le_bytes(position_data[4..8].try_into().unwrap()),
1518            i32::from_le_bytes(position_data[8..12].try_into().unwrap()),
1519        ];
1520        assert_eq!(first_position, [1, 2, 3]);
1521    }
1522
1523    #[test]
1524    fn test_read_mesh_ignores_non_float_normals() {
1525        let file = NamedTempFile::new().unwrap();
1526        let ply = r#"ply
1527format ascii 1.0
1528element vertex 1
1529property float x
1530property float y
1531property float z
1532property int nx
1533property int ny
1534property int nz
1535end_header
15360 0 0 0 0 1
1537"#;
1538
1539        std::fs::write(file.path(), ply).unwrap();
1540
1541        let mut reader = PlyReader::open(file.path()).unwrap();
1542        let mesh = reader.read_mesh().unwrap();
1543
1544        assert_eq!(mesh.named_attribute_id(GeometryAttributeType::Normal), -1);
1545    }
1546
1547    #[test]
1548    fn test_read_mesh_rejects_non_uint8_colors() {
1549        let file = NamedTempFile::new().unwrap();
1550        let ply = r#"ply
1551format ascii 1.0
1552element vertex 1
1553property float x
1554property float y
1555property float z
1556property int red
1557property int green
1558property int blue
1559end_header
15600 0 0 1 2 3
1561"#;
1562
1563        std::fs::write(file.path(), ply).unwrap();
1564
1565        let mut reader = PlyReader::open(file.path()).unwrap();
1566        let error = reader.read_mesh().unwrap_err();
1567        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
1568        assert!(error.to_string().contains("Color properties must be uint8"));
1569    }
1570
1571    /// Per-corner texture coordinates sit next to the indices on the face
1572    /// element, and Draco's own PLY encoder writes them there. Both are lists,
1573    /// so a reader that takes every face list for indices reads floats as
1574    /// vertex numbers — which is a hard parse error, not a wrong mesh.
1575    #[test]
1576    fn test_read_mesh_skips_non_index_face_lists() {
1577        let file = NamedTempFile::new().unwrap();
1578        let ply = r#"ply
1579format ascii 1.0
1580element vertex 4
1581property float x
1582property float y
1583property float z
1584element face 2
1585property list uchar int vertex_indices
1586property list uchar float texcoord
1587end_header
15880 0 0
15891 0 0
15901 1 0
15910 1 0
15923 0 1 2 6 0 0 1 0 1 1
15934 0 1 2 3 8 0 0 1 0 1 1 0 1
1594"#;
1595
1596        std::fs::write(file.path(), ply).unwrap();
1597
1598        let mut reader = PlyReader::open(file.path()).unwrap();
1599        let mesh = reader.read_mesh().unwrap();
1600
1601        assert_eq!(mesh.num_points(), 4);
1602        assert_eq!(mesh.num_faces(), 3);
1603        assert_eq!(
1604            mesh.face(draco_core::geometry_indices::FaceIndex(2)),
1605            [0u32.into(), 2u32.into(), 3u32.into()]
1606        );
1607    }
1608
1609    /// The same file binary, where the float list is not merely mis-parsed but
1610    /// mis-sized: skipping it has to consume exactly its own bytes, or every
1611    /// face after the first reads from the wrong offset.
1612    #[test]
1613    fn test_read_binary_mesh_skips_non_index_face_lists() {
1614        let file = NamedTempFile::new().unwrap();
1615        let mut ply = Vec::new();
1616        ply.extend_from_slice(
1617            br#"ply
1618format binary_little_endian 1.0
1619element vertex 4
1620property float x
1621property float y
1622property float z
1623element face 2
1624property list uchar int vertex_indices
1625property list uchar float texcoord
1626end_header
1627"#,
1628        );
1629
1630        for vertex in [
1631            [0.0f32, 0.0, 0.0],
1632            [1.0, 0.0, 0.0],
1633            [1.0, 1.0, 0.0],
1634            [0.0, 1.0, 0.0],
1635        ] {
1636            for component in vertex {
1637                ply.extend_from_slice(&component.to_le_bytes());
1638            }
1639        }
1640
1641        for indices in [vec![0i32, 1, 2], vec![0, 2, 3]] {
1642            ply.push(indices.len() as u8);
1643            for index in &indices {
1644                ply.extend_from_slice(&index.to_le_bytes());
1645            }
1646            ply.push((indices.len() * 2) as u8);
1647            for corner in 0..indices.len() * 2 {
1648                ply.extend_from_slice(&(corner as f32).to_le_bytes());
1649            }
1650        }
1651
1652        std::fs::write(file.path(), ply).unwrap();
1653
1654        let mut reader = PlyReader::open(file.path()).unwrap();
1655        let mesh = reader.read_mesh().unwrap();
1656
1657        assert_eq!(mesh.num_points(), 4);
1658        assert_eq!(mesh.num_faces(), 2);
1659        assert_eq!(
1660            mesh.face(draco_core::geometry_indices::FaceIndex(1)),
1661            [0u32.into(), 2u32.into(), 3u32.into()]
1662        );
1663    }
1664
1665    #[test]
1666    fn test_read_binary_little_endian_mesh() {
1667        let file = NamedTempFile::new().unwrap();
1668        let mut ply = Vec::new();
1669        ply.extend_from_slice(
1670            br#"ply
1671format binary_little_endian 1.0
1672element vertex 4
1673property float x
1674property float y
1675property float z
1676element face 2
1677property list uchar int vertex_indices
1678end_header
1679"#,
1680        );
1681
1682        for vertex in [
1683            [0.0f32, 0.0, 0.0],
1684            [1.0, 0.0, 0.0],
1685            [1.0, 1.0, 0.0],
1686            [0.0, 1.0, 0.0],
1687        ] {
1688            for component in vertex {
1689                ply.extend_from_slice(&component.to_le_bytes());
1690            }
1691        }
1692
1693        ply.push(3);
1694        for index in [0i32, 1, 2] {
1695            ply.extend_from_slice(&index.to_le_bytes());
1696        }
1697
1698        ply.push(4);
1699        for index in [0i32, 1, 2, 3] {
1700            ply.extend_from_slice(&index.to_le_bytes());
1701        }
1702
1703        std::fs::write(file.path(), ply).unwrap();
1704
1705        let mut reader = PlyReader::open(file.path()).unwrap();
1706        let mesh = reader.read_mesh().unwrap();
1707
1708        assert_eq!(mesh.num_points(), 4);
1709        assert_eq!(mesh.num_faces(), 3);
1710        assert_eq!(
1711            mesh.face(draco_core::geometry_indices::FaceIndex(0)),
1712            [0u32.into(), 1u32.into(), 2u32.into()]
1713        );
1714        assert_eq!(
1715            mesh.face(draco_core::geometry_indices::FaceIndex(1)),
1716            [0u32.into(), 1u32.into(), 2u32.into()]
1717        );
1718        assert_eq!(
1719            mesh.face(draco_core::geometry_indices::FaceIndex(2)),
1720            [0u32.into(), 2u32.into(), 3u32.into()]
1721        );
1722    }
1723
1724    #[test]
1725    fn test_read_binary_little_endian_mesh_with_cr_only_header() {
1726        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();
1727        for index in 0..24 {
1728            ply.extend_from_slice(&(index as f32).to_le_bytes());
1729            ply.extend_from_slice(&0.0f32.to_le_bytes());
1730            ply.extend_from_slice(&0.0f32.to_le_bytes());
1731        }
1732        ply.extend_from_slice(&[3]);
1733        for index in [0i32, 1, 2] {
1734            ply.extend_from_slice(&index.to_le_bytes());
1735        }
1736
1737        let mesh =
1738            PlyReader::read_from_bytes(&ply).expect("CR-only binary PLY header should parse");
1739
1740        assert_eq!(mesh.num_points(), 24);
1741        assert!(mesh.num_faces() > 0);
1742    }
1743
1744    #[test]
1745    fn test_read_binary_little_endian_attributes_and_int_positions() {
1746        let file = NamedTempFile::new().unwrap();
1747        let mut ply = Vec::new();
1748        ply.extend_from_slice(
1749            br#"ply
1750format binary_little_endian 1.0
1751element vertex 2
1752property int x
1753property int y
1754property int z
1755property float nx
1756property float ny
1757property float nz
1758property uchar red
1759property uchar green
1760property uchar blue
1761property uchar alpha
1762end_header
1763"#,
1764        );
1765
1766        for (position, normal, color) in [
1767            ([1i32, 2, 3], [0.0f32, 0.0, 1.0], [10u8, 20, 30, 40]),
1768            ([4i32, 5, 6], [0.0f32, 1.0, 0.0], [50u8, 60, 70, 80]),
1769        ] {
1770            for component in position {
1771                ply.extend_from_slice(&component.to_le_bytes());
1772            }
1773            for component in normal {
1774                ply.extend_from_slice(&component.to_le_bytes());
1775            }
1776            ply.extend_from_slice(&color);
1777        }
1778
1779        std::fs::write(file.path(), ply).unwrap();
1780
1781        let mut reader = PlyReader::open(file.path()).unwrap();
1782        let mesh = reader.read_mesh().unwrap();
1783
1784        let position_att = mesh
1785            .named_attribute(GeometryAttributeType::Position)
1786            .unwrap();
1787        assert_eq!(position_att.data_type(), DataType::Int32);
1788        assert_eq!(position_att.num_components(), 3);
1789
1790        let position_data = position_att.buffer().data();
1791        let first_position = [
1792            i32::from_le_bytes(position_data[0..4].try_into().unwrap()),
1793            i32::from_le_bytes(position_data[4..8].try_into().unwrap()),
1794            i32::from_le_bytes(position_data[8..12].try_into().unwrap()),
1795        ];
1796        assert_eq!(first_position, [1, 2, 3]);
1797
1798        let normal_att = mesh.named_attribute(GeometryAttributeType::Normal).unwrap();
1799        assert_eq!(normal_att.data_type(), DataType::Float32);
1800        assert_eq!(normal_att.num_components(), 3);
1801
1802        let normal_data = normal_att.buffer().data();
1803        let first_normal = [
1804            f32::from_le_bytes(normal_data[0..4].try_into().unwrap()),
1805            f32::from_le_bytes(normal_data[4..8].try_into().unwrap()),
1806            f32::from_le_bytes(normal_data[8..12].try_into().unwrap()),
1807        ];
1808        assert_eq!(first_normal, [0.0, 0.0, 1.0]);
1809
1810        let color_att = mesh.named_attribute(GeometryAttributeType::Color).unwrap();
1811        assert_eq!(color_att.data_type(), DataType::Uint8);
1812        assert_eq!(color_att.num_components(), 4);
1813        assert!(color_att.normalized());
1814        assert_eq!(color_att.buffer().data(), &[10, 20, 30, 40, 50, 60, 70, 80]);
1815    }
1816
1817    #[test]
1818    fn test_read_binary_big_endian_mesh() {
1819        let mut ply = Vec::new();
1820        ply.extend_from_slice(
1821            br#"ply
1822format binary_big_endian 1.0
1823element vertex 4
1824property float x
1825property float y
1826property float z
1827element face 1
1828property list uchar int vertex_indices
1829end_header
1830"#,
1831        );
1832
1833        for vertex in [
1834            [0.0f32, 0.0, 0.0],
1835            [1.0, 0.0, 0.0],
1836            [1.0, 1.0, 0.0],
1837            [0.0, 1.0, 0.0],
1838        ] {
1839            for component in vertex {
1840                ply.extend_from_slice(&component.to_be_bytes());
1841            }
1842        }
1843
1844        ply.push(4);
1845        for index in [0i32, 1, 2, 3] {
1846            ply.extend_from_slice(&index.to_be_bytes());
1847        }
1848
1849        let mesh = PlyReader::read_from_bytes(&ply).unwrap();
1850        assert_eq!(mesh.num_points(), 4);
1851        assert_eq!(mesh.num_faces(), 2);
1852        assert_eq!(
1853            mesh.face(draco_core::geometry_indices::FaceIndex(1)),
1854            [0u32.into(), 2u32.into(), 3u32.into()]
1855        );
1856    }
1857}