use crate::raw_attribute::{make_f32x2_attribute, make_f32x3_attribute};
use crate::traits::finalize_mesh;
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
use std::fs;
use std::io::{self, Cursor, Write};
use std::path::Path;
use draco_core::draco_types::DataType;
use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
use draco_core::mesh::Mesh;
pub use crate::ply_format::PlyFormat;
use crate::traits::{PointCloudReader, ReadFromBytes, Reader};
#[derive(Debug)]
struct ParsedPlyColorData {
num_components: u8,
values: Vec<[u8; 4]>,
}
#[derive(Debug)]
struct ParsedPlyData {
positions: ParsedPlyPositionData,
faces: Vec<[u32; 3]>,
normals: Option<Vec<[f32; 3]>>,
colors: Option<ParsedPlyColorData>,
texcoords: Option<Vec<[f32; 2]>>,
generic: Vec<ParsedGenericProperty>,
}
#[derive(Debug)]
struct ParsedGenericProperty {
name: String,
data_type: DataType,
values: Vec<f64>,
}
#[derive(Debug)]
enum ParsedPlyPositionData {
Float32(Vec<[f32; 3]>),
Int32(Vec<[i32; 3]>),
}
impl ParsedPlyPositionData {
fn len(&self) -> usize {
match self {
ParsedPlyPositionData::Float32(values) => values.len(),
ParsedPlyPositionData::Int32(values) => values.len(),
}
}
fn to_f32_positions(&self) -> Vec<[f32; 3]> {
match self {
ParsedPlyPositionData::Float32(values) => values.clone(),
ParsedPlyPositionData::Int32(values) => values
.iter()
.map(|value| [value[0] as f32, value[1] as f32, value[2] as f32])
.collect(),
}
}
}
#[derive(Debug, Clone)]
enum PlyPropertyKind {
Scalar(DataType),
List {
count_type: DataType,
item_type: DataType,
},
}
#[derive(Debug, Clone)]
struct PlyPropertyDef {
name: String,
kind: PlyPropertyKind,
}
impl PlyPropertyDef {
fn scalar_type(&self) -> Option<DataType> {
match self.kind {
PlyPropertyKind::Scalar(data_type) => Some(data_type),
PlyPropertyKind::List { .. } => None,
}
}
}
#[derive(Debug, Clone)]
struct PlyHeader {
format: PlyFormat,
vertex_count: usize,
face_count: usize,
elements: Vec<PlyElementDef>,
vertex_properties: Vec<PlyPropertyDef>,
face_properties: Vec<PlyPropertyDef>,
}
#[derive(Debug, Clone)]
struct PlyElementDef {
name: String,
count: usize,
properties: Vec<PlyPropertyDef>,
}
#[derive(Debug, Clone, Copy)]
struct PlyReadSchema {
position_data_type: DataType,
has_normals: bool,
color_components: u8,
texcoord_pair: Option<TexcoordPropertyPair>,
}
#[derive(Debug, Clone, Copy)]
struct TexcoordPropertyPair {
u: &'static str,
v: &'static str,
}
fn parse_ply_scalar_type(token: &str) -> Option<DataType> {
match token {
"char" | "int8" => Some(DataType::Int8),
"uchar" | "uint8" => Some(DataType::Uint8),
"short" | "int16" => Some(DataType::Int16),
"ushort" | "uint16" => Some(DataType::Uint16),
"int" | "int32" => Some(DataType::Int32),
"uint" | "uint32" => Some(DataType::Uint32),
"float" | "float32" => Some(DataType::Float32),
"double" | "float64" => Some(DataType::Float64),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlyDroppedItem {
VertexProperty {
name: String,
data_type: Option<DataType>,
},
Normals,
FaceProperty {
name: String,
},
Element {
name: String,
count: usize,
},
}
impl std::fmt::Display for PlyDroppedItem {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlyDroppedItem::VertexProperty { name, data_type } => match data_type {
Some(data_type) => write!(
formatter,
"vertex property {name:?} ({data_type:?}) has no attribute to read it into"
),
None => write!(
formatter,
"vertex property {name:?} is a list, which the vertex element has no reading for"
),
},
PlyDroppedItem::Normals => write!(
formatter,
"normals are declared but not as three float32 components, so they are not read"
),
PlyDroppedItem::FaceProperty { name } => write!(
formatter,
"face property {name:?} is not the corner-index list and is skipped"
),
PlyDroppedItem::Element { name, count } => write!(
formatter,
"element {name:?} and its {count} entries are skipped entirely"
),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PlyLossReport {
dropped: Vec<PlyDroppedItem>,
}
impl PlyLossReport {
pub fn dropped(&self) -> &[PlyDroppedItem] {
&self.dropped
}
pub fn is_lossless(&self) -> bool {
self.dropped.is_empty()
}
}
#[derive(Debug)]
pub struct PlyReader {
source: PlyReaderSource,
carry_generics: bool,
}
#[derive(Debug, Clone)]
enum PlyReaderSource {
Path(std::path::PathBuf),
Bytes(Vec<u8>),
}
impl PlyReader {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let path = path.as_ref().to_path_buf();
if !path.exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("File not found: {}", path.display()),
));
}
Ok(Self {
source: PlyReaderSource::Path(path),
carry_generics: false,
})
}
pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
Self {
source: PlyReaderSource::Bytes(bytes.into()),
carry_generics: false,
}
}
pub fn with_generic_attributes(mut self, enabled: bool) -> Self {
self.carry_generics = enabled;
self
}
pub fn set_generic_attributes(&mut self, enabled: bool) -> &mut Self {
self.carry_generics = enabled;
self
}
pub fn read_from_bytes(bytes: &[u8]) -> io::Result<Mesh> {
let mut reader = Self::from_bytes(bytes.to_vec());
reader.read_mesh()
}
pub fn loss_report(&mut self) -> io::Result<PlyLossReport> {
let bytes = match &self.source {
PlyReaderSource::Path(path) => std::borrow::Cow::Owned(fs::read(path)?),
PlyReaderSource::Bytes(bytes) => std::borrow::Cow::Borrowed(bytes.as_slice()),
};
let (header, _) = parse_ply_header(&bytes)?;
let schema = build_read_schema(&header)?;
let plan = GenericPlan::build(&header, &schema, self.carry_generics);
Ok(build_loss_report(&header, &schema, !plan.is_empty()))
}
pub fn read_positions(&mut self) -> io::Result<Vec<[f32; 3]>> {
Ok(read_ply_source(&self.source)?.positions.to_f32_positions())
}
pub fn read_mesh(&mut self) -> io::Result<Mesh> {
Ok(self.read_mesh_reporting_loss()?.0)
}
pub fn read_mesh_reporting_loss(&mut self) -> io::Result<(Mesh, PlyLossReport)> {
let (parsed, report) = read_ply_source_reporting(&self.source, self.carry_generics)?;
Ok((mesh_from_parsed(parsed)?, report))
}
}
fn mesh_from_parsed(parsed: ParsedPlyData) -> io::Result<Mesh> {
let mut mesh = Mesh::new();
if parsed.positions.len() == 0 {
return Ok(mesh);
}
mesh.set_num_points(parsed.positions.len());
mesh.set_num_faces(parsed.faces.len());
match &parsed.positions {
ParsedPlyPositionData::Float32(values) => {
mesh.add_attribute(make_f32x3_attribute(
GeometryAttributeType::Position,
values,
));
}
ParsedPlyPositionData::Int32(values) => {
mesh.add_attribute(make_i32x3_attribute(
GeometryAttributeType::Position,
values,
));
}
}
if let Some(normals) = parsed.normals.as_ref() {
mesh.add_attribute(make_f32x3_attribute(GeometryAttributeType::Normal, normals));
}
if let Some(colors) = parsed.colors.as_ref() {
mesh.add_attribute(make_u8_attribute(
GeometryAttributeType::Color,
colors.num_components,
true,
&colors.values,
));
}
if let Some(texcoords) = parsed.texcoords.as_ref() {
mesh.add_attribute(make_f32x2_attribute(
GeometryAttributeType::TexCoord,
texcoords,
));
}
for property in &parsed.generic {
if property.values.len() != mesh.num_points() {
continue;
}
let attribute_id = mesh.add_attribute(make_generic_attribute(property));
let unique_id = mesh.attribute(attribute_id).unique_id();
let mut metadata = draco_core::metadata::Metadata::new();
metadata
.set_string("name", property.name.clone())
.map_err(|error| invalid_ply(format!("Cannot name attribute: {error:?}")))?;
mesh.metadata_or_insert()
.set_attribute_metadata(unique_id, metadata);
}
for (i, face) in parsed.faces.iter().enumerate() {
mesh.set_face(
draco_core::geometry_indices::FaceIndex(i as u32),
[
draco_core::geometry_indices::PointIndex(face[0]),
draco_core::geometry_indices::PointIndex(face[1]),
draco_core::geometry_indices::PointIndex(face[2]),
],
);
}
if mesh.num_faces() > 0 {
finalize_mesh(&mut mesh)?;
}
Ok(mesh)
}
impl Reader for PlyReader {
fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
PlyReader::open(path)
}
fn read_meshes(&mut self) -> io::Result<Vec<Mesh>> {
let m = self.read_mesh()?;
Ok(vec![m])
}
}
impl ReadFromBytes for PlyReader {
fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
Ok(Self::from_bytes(bytes.to_vec()))
}
}
impl PointCloudReader for PlyReader {
fn read_points(&mut self) -> io::Result<Vec<[f32; 3]>> {
self.read_positions()
}
}
pub fn read_ply_positions<P: AsRef<Path>>(path: P) -> io::Result<Vec<[f32; 3]>> {
Ok(read_ply(path)?.positions.to_f32_positions())
}
fn make_generic_attribute(property: &ParsedGenericProperty) -> PointAttribute {
let mut attribute = PointAttribute::new();
attribute.init(
GeometryAttributeType::Generic,
1,
property.data_type,
false,
property.values.len(),
);
let buffer = attribute.buffer_mut();
let width = property.data_type.byte_length();
for (index, value) in property.values.iter().enumerate() {
let value = *value;
let bytes: [u8; 8] = match property.data_type {
DataType::Int8 => pad(&(value as i8).to_le_bytes()),
DataType::Uint8 => pad(&(value as u8).to_le_bytes()),
DataType::Int16 => pad(&(value as i16).to_le_bytes()),
DataType::Uint16 => pad(&(value as u16).to_le_bytes()),
DataType::Int32 => pad(&(value as i32).to_le_bytes()),
DataType::Uint32 => pad(&(value as u32).to_le_bytes()),
DataType::Float64 => value.to_le_bytes(),
_ => pad(&(value as f32).to_le_bytes()),
};
buffer.write(index * width, &bytes[..width]);
}
attribute
}
fn pad(bytes: &[u8]) -> [u8; 8] {
let mut padded = [0u8; 8];
padded[..bytes.len()].copy_from_slice(bytes);
padded
}
fn make_i32x3_attribute(
attribute_type: GeometryAttributeType,
values: &[[i32; 3]],
) -> PointAttribute {
let mut attribute = PointAttribute::new();
attribute.init(attribute_type, 3, DataType::Int32, false, values.len());
let buffer = attribute.buffer_mut();
for (i, value) in values.iter().enumerate() {
let bytes: Vec<u8> = value
.iter()
.flat_map(|component| component.to_le_bytes())
.collect();
buffer.write(i * 12, &bytes);
}
attribute
}
fn make_u8_attribute(
attribute_type: GeometryAttributeType,
num_components: u8,
normalized: bool,
values: &[[u8; 4]],
) -> PointAttribute {
let mut attribute = PointAttribute::new();
attribute.init(
attribute_type,
num_components,
DataType::Uint8,
normalized,
values.len(),
);
let buffer = attribute.buffer_mut();
for (i, value) in values.iter().enumerate() {
let end = num_components as usize;
buffer.write(i * end, &value[..end]);
}
attribute
}
fn invalid_ply(message: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message.into())
}
fn parse_ply_property(parts: &[&str]) -> io::Result<PlyPropertyDef> {
if parts.len() < 3 {
return Err(invalid_ply("Malformed property declaration"));
}
if parts[1] == "list" {
if parts.len() < 5 {
return Err(invalid_ply("Malformed list property declaration"));
}
let count_type = parse_ply_scalar_type(parts[2])
.ok_or_else(|| invalid_ply(format!("Unsupported PLY scalar type: {}", parts[2])))?;
let item_type = parse_ply_scalar_type(parts[3])
.ok_or_else(|| invalid_ply(format!("Unsupported PLY scalar type: {}", parts[3])))?;
Ok(PlyPropertyDef {
name: parts[4].to_string(),
kind: PlyPropertyKind::List {
count_type,
item_type,
},
})
} else {
let data_type = parse_ply_scalar_type(parts[1])
.ok_or_else(|| invalid_ply(format!("Unsupported PLY scalar type: {}", parts[1])))?;
Ok(PlyPropertyDef {
name: parts[2].to_string(),
kind: PlyPropertyKind::Scalar(data_type),
})
}
}
fn parse_ply_header(bytes: &[u8]) -> io::Result<(PlyHeader, usize)> {
if bytes.is_empty() {
return Err(invalid_ply("Empty PLY file"));
}
let mut body_offset = None;
let mut offset = 0usize;
while offset < bytes.len() {
let line_end = bytes[offset..]
.iter()
.position(|byte| matches!(*byte, b'\n' | b'\r'))
.map(|idx| offset + idx);
match line_end {
Some(end) => {
let line_bytes = &bytes[offset..end];
let line = std::str::from_utf8(line_bytes)
.map_err(|_| invalid_ply("PLY header must be valid UTF-8/ASCII"))?;
offset = end + 1;
if bytes[end] == b'\r' && bytes.get(offset) == Some(&b'\n') {
offset += 1;
}
if line.trim() == "end_header" {
body_offset = Some(offset);
break;
}
}
None => {
let line = std::str::from_utf8(&bytes[offset..])
.map_err(|_| invalid_ply("PLY header must be valid UTF-8/ASCII"))?;
if line.trim() == "end_header" {
body_offset = Some(bytes.len());
break;
}
break;
}
}
}
let body_offset = body_offset.ok_or_else(|| invalid_ply("No end_header found"))?;
let header_text = std::str::from_utf8(&bytes[..body_offset])
.map_err(|_| invalid_ply("PLY header must be valid UTF-8/ASCII"))?;
let mut lines = header_text.split(['\n', '\r']);
let first_line = lines.next().ok_or_else(|| invalid_ply("Empty PLY file"))?;
if first_line.trim() != "ply" {
return Err(invalid_ply("Missing PLY header"));
}
let mut format = None;
let mut vertex_count = 0usize;
let mut face_count = 0usize;
let mut elements: Vec<PlyElementDef> = Vec::new();
for line in lines {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed == "end_header" {
continue;
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.is_empty() {
continue;
}
match parts[0] {
"comment" | "obj_info" => {}
"format" => {
if parts.len() < 2 {
return Err(invalid_ply("Malformed format declaration"));
}
format = Some(match parts[1] {
"ascii" => PlyFormat::Ascii,
"binary_little_endian" => PlyFormat::BinaryLittleEndian,
"binary_big_endian" => PlyFormat::BinaryBigEndian,
other => {
return Err(invalid_ply(format!("Unsupported PLY format: {other}")));
}
});
}
"element" => {
if parts.len() < 3 {
return Err(invalid_ply("Malformed element declaration"));
}
let count = parts[2]
.parse()
.map_err(|_| invalid_ply("Invalid element count"))?;
elements.push(PlyElementDef {
name: parts[1].to_string(),
count,
properties: Vec::new(),
});
match parts[1] {
"vertex" => {
vertex_count = count;
}
"face" => {
face_count = count;
}
_ => {}
}
}
"property" => {
let property = parse_ply_property(&parts)?;
let Some(element) = elements.last_mut() else {
return Err(invalid_ply("Property declared before element"));
};
element.properties.push(property);
}
_ => {}
}
}
let mut vertex_properties = Vec::new();
let mut face_properties = Vec::new();
for element in &elements {
match element.name.as_str() {
"vertex" => vertex_properties = element.properties.clone(),
"face" => face_properties = element.properties.clone(),
_ => {}
}
}
Ok((
PlyHeader {
format: format.ok_or_else(|| invalid_ply("Missing PLY format declaration"))?,
vertex_count,
face_count,
elements,
vertex_properties,
face_properties,
},
body_offset,
))
}
fn skip_ascii_element_lines(lines: &mut std::str::Lines<'_>, count: usize) {
for _ in 0..count {
if lines.next().is_none() {
return;
}
}
}
fn ascii_scalar_token_count(data_type: DataType) -> usize {
if data_type == DataType::Invalid {
0
} else {
1
}
}
fn split_ascii_vertex_lines<'a>(
header: &PlyHeader,
body_text: &'a str,
) -> io::Result<(Vec<&'a str>, Vec<&'a str>)> {
let mut lines = body_text.lines();
let mut vertex_lines = Vec::new();
let mut face_lines = Vec::new();
for element in &header.elements {
match element.name.as_str() {
"vertex" => {
for _ in 0..element.count {
let Some(line) = lines.next() else { break };
vertex_lines.push(line);
}
}
"face" => {
for _ in 0..element.count {
let Some(line) = lines.next() else { break };
face_lines.push(line);
}
}
_ => skip_ascii_element_lines(&mut lines, element.count),
}
}
Ok((vertex_lines, face_lines))
}
fn position_data_type_for_scalar(data_type: DataType) -> DataType {
match data_type {
DataType::Int32 => DataType::Int32,
_ => DataType::Float32,
}
}
fn scalar_property_type(header: &PlyHeader, name: &str) -> Option<DataType> {
header.vertex_properties.iter().find_map(|property| {
(property.name == name)
.then(|| property.scalar_type())
.flatten()
})
}
fn detect_texcoord_pair(header: &PlyHeader) -> Option<TexcoordPropertyPair> {
const PAIRS: [TexcoordPropertyPair; 3] = [
TexcoordPropertyPair {
u: "texture_u",
v: "texture_v",
},
TexcoordPropertyPair { u: "u", v: "v" },
TexcoordPropertyPair { u: "s", v: "t" },
];
PAIRS.into_iter().find(|pair| {
scalar_property_type(header, pair.u) == Some(DataType::Float32)
&& scalar_property_type(header, pair.v) == Some(DataType::Float32)
})
}
fn build_read_schema(header: &PlyHeader) -> io::Result<PlyReadSchema> {
let mut has_x = false;
let mut has_y = false;
let mut has_z = false;
let mut position_data_type = DataType::Float32;
let mut prop_nx_type = None;
let mut prop_ny_type = None;
let mut prop_nz_type = None;
let mut prop_r_type = None;
let mut prop_g_type = None;
let mut prop_b_type = None;
let mut prop_a_type = None;
for property in &header.vertex_properties {
let Some(data_type) = property.scalar_type() else {
continue;
};
match property.name.as_str() {
"x" => {
has_x = true;
position_data_type = position_data_type_for_scalar(data_type);
}
"y" => {
has_y = true;
position_data_type = position_data_type_for_scalar(data_type);
}
"z" => {
has_z = true;
position_data_type = position_data_type_for_scalar(data_type);
}
"nx" => prop_nx_type = Some(data_type),
"ny" => prop_ny_type = Some(data_type),
"nz" => prop_nz_type = Some(data_type),
"red" => prop_r_type = Some(data_type),
"green" => prop_g_type = Some(data_type),
"blue" => prop_b_type = Some(data_type),
"alpha" => prop_a_type = Some(data_type),
_ => {}
}
}
if !has_x {
return Err(invalid_ply("No x property"));
}
if !has_y {
return Err(invalid_ply("No y property"));
}
if !has_z {
return Err(invalid_ply("No z property"));
}
let has_normals = prop_nx_type == Some(DataType::Float32)
&& prop_ny_type == Some(DataType::Float32)
&& prop_nz_type == Some(DataType::Float32);
let color_types = [prop_r_type, prop_g_type, prop_b_type, prop_a_type];
let color_components = color_types.iter().flatten().count() as u8;
if color_components > 0 {
for color_type in color_types.into_iter().flatten() {
if color_type != DataType::Uint8 {
return Err(invalid_ply("Color properties must be uint8"));
}
}
}
Ok(PlyReadSchema {
position_data_type,
has_normals,
color_components,
texcoord_pair: detect_texcoord_pair(header),
})
}
const NORMAL_NAMES: [&str; 3] = ["nx", "ny", "nz"];
const COLOR_NAMES: [&str; 4] = ["red", "green", "blue", "alpha"];
fn consumes_vertex_property(schema: &PlyReadSchema, name: &str) -> bool {
matches!(name, "x" | "y" | "z")
|| NORMAL_NAMES.contains(&name)
|| COLOR_NAMES.contains(&name)
|| schema
.texcoord_pair
.is_some_and(|pair| name == pair.u || name == pair.v)
}
#[derive(Debug, Default)]
struct GenericPlan {
columns: Vec<Option<usize>>,
properties: Vec<(String, DataType)>,
}
impl GenericPlan {
fn build(header: &PlyHeader, schema: &PlyReadSchema, enabled: bool) -> Self {
let mut plan = Self::default();
if !enabled {
return plan;
}
plan.columns = vec![None; header.vertex_properties.len()];
for (index, property) in header.vertex_properties.iter().enumerate() {
let Some(data_type) = property.scalar_type() else {
continue;
};
if consumes_vertex_property(schema, &property.name) {
continue;
}
plan.columns[index] = Some(plan.properties.len());
plan.properties.push((property.name.clone(), data_type));
}
plan
}
fn column_for(&self, property_index: usize) -> Option<usize> {
self.columns.get(property_index).copied().flatten()
}
fn is_empty(&self) -> bool {
self.properties.is_empty()
}
fn new_values(&self, capacity: usize) -> Vec<Vec<f64>> {
self.properties
.iter()
.map(|_| Vec::with_capacity(capacity))
.collect()
}
fn finish(&self, values: Vec<Vec<f64>>) -> Vec<ParsedGenericProperty> {
self.properties
.iter()
.zip(values)
.map(|((name, data_type), values)| ParsedGenericProperty {
name: name.clone(),
data_type: *data_type,
values,
})
.collect()
}
}
fn build_loss_report(
header: &PlyHeader,
schema: &PlyReadSchema,
carried_generics: bool,
) -> PlyLossReport {
let mut dropped = Vec::new();
let declares_normals = header
.vertex_properties
.iter()
.any(|property| NORMAL_NAMES.contains(&property.name.as_str()));
if declares_normals && !schema.has_normals {
dropped.push(PlyDroppedItem::Normals);
}
for property in &header.vertex_properties {
let name = property.name.as_str();
let consumed = consumes_vertex_property(schema, name);
let carried = carried_generics && property.scalar_type().is_some();
if !consumed && !carried {
dropped.push(PlyDroppedItem::VertexProperty {
name: property.name.clone(),
data_type: property.scalar_type(),
});
}
}
let face_index = face_index_property(&header.face_properties);
for (index, property) in header.face_properties.iter().enumerate() {
if Some(index) != face_index {
dropped.push(PlyDroppedItem::FaceProperty {
name: property.name.clone(),
});
}
}
for element in &header.elements {
if !matches!(element.name.as_str(), "vertex" | "face") {
dropped.push(PlyDroppedItem::Element {
name: element.name.clone(),
count: element.count,
});
}
}
PlyLossReport { dropped }
}
fn triangulate_vertex_indices(indices: &[u32], faces: &mut Vec<[u32; 3]>) {
if indices.len() < 3 {
return;
}
for j in 1..indices.len() - 1 {
faces.push([indices[0], indices[j], indices[j + 1]]);
}
}
fn face_index_property(properties: &[PlyPropertyDef]) -> Option<usize> {
let lists = || {
properties
.iter()
.enumerate()
.filter(|(_, property)| matches!(property.kind, PlyPropertyKind::List { .. }))
};
lists()
.find(|(_, property)| property.name == "vertex_indices")
.or_else(|| lists().next())
.map(|(index, _)| index)
}
fn parse_ascii_face_line(
header: &PlyHeader,
line: &str,
faces: &mut Vec<[u32; 3]>,
) -> io::Result<()> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
return Ok(());
}
if header.face_properties.is_empty() {
let indices: Vec<u32> = parts
.iter()
.map(|part| {
part.parse::<u32>()
.map_err(|_| invalid_ply("Bad face index value"))
})
.collect::<io::Result<Vec<u32>>>()?;
if indices.is_empty() {
return Ok(());
}
let polygon_size = indices[0] as usize;
let Some(end) = polygon_size.checked_add(1) else {
return Ok(());
};
if polygon_size < 3 || indices.len() < end {
return Ok(());
}
triangulate_vertex_indices(&indices[1..end], faces);
return Ok(());
}
let index_property = face_index_property(&header.face_properties);
let mut cursor = 0usize;
let mut polygon_indices: Option<Vec<u32>> = None;
for (position, property) in header.face_properties.iter().enumerate() {
match property.kind {
PlyPropertyKind::Scalar(_) => {
if cursor >= parts.len() {
return Ok(());
}
cursor += 1;
}
PlyPropertyKind::List { .. } => {
if cursor >= parts.len() {
return Ok(());
}
let count: usize = parts[cursor]
.parse()
.map_err(|_| invalid_ply("Bad face list size"))?;
cursor += 1;
let Some(end) = cursor.checked_add(count) else {
return Ok(());
};
if parts.len() < end {
return Ok(());
}
if index_property == Some(position) {
polygon_indices = Some(
parts[cursor..end]
.iter()
.map(|part| {
part.parse::<u32>()
.map_err(|_| invalid_ply("Bad face index value"))
})
.collect::<io::Result<Vec<u32>>>()?,
);
}
cursor = end;
}
}
}
if let Some(indices) = polygon_indices {
triangulate_vertex_indices(&indices, faces);
}
Ok(())
}
fn parse_ascii_f32(token: &str, label: &str) -> io::Result<f32> {
token
.parse()
.map_err(|_| invalid_ply(format!("Bad {label} value")))
}
fn parse_ascii_f64(token: &str, label: &str) -> io::Result<f64> {
token
.parse()
.map_err(|_| invalid_ply(format!("Bad {label} value")))
}
fn parse_ascii_i32(token: &str, label: &str) -> io::Result<i32> {
token
.parse()
.map_err(|_| invalid_ply(format!("Bad {label} value")))
}
fn parse_ascii_u8(token: &str) -> io::Result<u8> {
token
.parse()
.map_err(|_| invalid_ply("Bad color component value"))
}
fn body_bounded_capacity(
declared: usize,
available_bytes: usize,
min_bytes_per_item: usize,
) -> usize {
declared.min(available_bytes / min_bytes_per_item.max(1))
}
fn read_ply_ascii_body(
header: &PlyHeader,
schema: &PlyReadSchema,
generic_plan: &GenericPlan,
body: &[u8],
) -> io::Result<ParsedPlyData> {
let body_text = std::str::from_utf8(body)
.map_err(|_| invalid_ply("ASCII PLY payload must be valid UTF-8/ASCII"))?;
let (vertex_lines, face_lines) = split_ascii_vertex_lines(header, body_text)?;
let vertex_capacity = header.vertex_count.min(vertex_lines.len());
let mut float_positions = matches!(schema.position_data_type, DataType::Float32)
.then(|| Vec::with_capacity(vertex_capacity));
let mut int_positions = matches!(schema.position_data_type, DataType::Int32)
.then(|| Vec::with_capacity(vertex_capacity));
let mut normals = schema
.has_normals
.then(|| Vec::with_capacity(vertex_capacity));
let mut colors = (schema.color_components > 0).then(|| ParsedPlyColorData {
num_components: schema.color_components,
values: Vec::with_capacity(vertex_capacity),
});
let mut texcoords = schema
.texcoord_pair
.is_some()
.then(|| Vec::with_capacity(vertex_capacity));
let mut generic_values = generic_plan.new_values(vertex_capacity);
for line in vertex_lines {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
let mut float_position = [0.0f32; 3];
let mut int_position = [0i32; 3];
let mut normal = [0.0f32; 3];
let mut color = [0u8; 4];
let mut texcoord = [0.0f32; 2];
let mut color_component = 0usize;
let mut cursor = 0usize;
for (property_index, property) in header.vertex_properties.iter().enumerate() {
let Some(data_type) = property.scalar_type() else {
if cursor >= parts.len() {
break;
}
let count: usize = parts[cursor]
.parse()
.map_err(|_| invalid_ply("Bad vertex list size"))?;
cursor = cursor
.checked_add(1 + count)
.ok_or_else(|| invalid_ply("ASCII PLY line is too large"))?;
continue;
};
if cursor >= parts.len() {
break;
}
let token = parts[cursor];
cursor += ascii_scalar_token_count(data_type);
match property.name.as_str() {
"x" => match schema.position_data_type {
DataType::Int32 => int_position[0] = parse_ascii_i32(token, "x")?,
_ => float_position[0] = parse_ascii_f32(token, "x")?,
},
"y" => match schema.position_data_type {
DataType::Int32 => int_position[1] = parse_ascii_i32(token, "y")?,
_ => float_position[1] = parse_ascii_f32(token, "y")?,
},
"z" => match schema.position_data_type {
DataType::Int32 => int_position[2] = parse_ascii_i32(token, "z")?,
_ => float_position[2] = parse_ascii_f32(token, "z")?,
},
"nx" if schema.has_normals => normal[0] = parse_ascii_f32(token, "nx")?,
"ny" if schema.has_normals => normal[1] = parse_ascii_f32(token, "ny")?,
"nz" if schema.has_normals => normal[2] = parse_ascii_f32(token, "nz")?,
"red" | "green" | "blue" | "alpha" if schema.color_components > 0 => {
let value = parse_ascii_u8(token)?;
if let Some(slot) = color.get_mut(color_component) {
*slot = value;
}
color_component += 1;
}
name if schema.texcoord_pair.is_some_and(|pair| name == pair.u) => {
texcoord[0] = parse_ascii_f32(token, name)?;
}
name if schema.texcoord_pair.is_some_and(|pair| name == pair.v) => {
texcoord[1] = parse_ascii_f32(token, name)?;
}
name => {
if let Some(column) = generic_plan.column_for(property_index) {
generic_values[column].push(parse_ascii_f64(token, name)?);
}
}
}
}
match schema.position_data_type {
DataType::Int32 => int_positions.as_mut().unwrap().push(int_position),
_ => float_positions.as_mut().unwrap().push(float_position),
}
if let Some(normals) = normals.as_mut() {
normals.push(normal);
}
if let Some(colors) = colors.as_mut() {
colors.values.push(color);
}
if let Some(texcoords) = texcoords.as_mut() {
texcoords.push(texcoord);
}
}
let mut faces = Vec::with_capacity(header.face_count.min(face_lines.len()));
for line in face_lines {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
parse_ascii_face_line(header, trimmed, &mut faces)?;
}
Ok(ParsedPlyData {
positions: match schema.position_data_type {
DataType::Int32 => ParsedPlyPositionData::Int32(int_positions.unwrap_or_default()),
_ => ParsedPlyPositionData::Float32(float_positions.unwrap_or_default()),
},
faces,
normals,
colors,
texcoords,
generic: generic_plan.finish(generic_values),
})
}
fn ensure_remaining(cursor: &Cursor<&[u8]>, bytes_needed: usize) -> io::Result<()> {
let position = cursor.position() as usize;
let end = position
.checked_add(bytes_needed)
.ok_or_else(|| invalid_ply("PLY payload is too large"))?;
if end > cursor.get_ref().len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Unexpected end of binary PLY payload",
));
}
Ok(())
}
fn skip_binary_scalar(cursor: &mut Cursor<&[u8]>, data_type: DataType) -> io::Result<()> {
ensure_remaining(cursor, data_type.byte_length())?;
cursor.set_position(cursor.position() + data_type.byte_length() as u64);
Ok(())
}
#[derive(Debug, Clone, Copy)]
enum BinaryEndian {
Little,
Big,
}
fn read_binary_scalar_as_f64(
cursor: &mut Cursor<&[u8]>,
data_type: DataType,
endian: BinaryEndian,
) -> io::Result<f64> {
ensure_remaining(cursor, data_type.byte_length())?;
let value = match data_type {
DataType::Int8 => cursor.read_i8()? as f64,
DataType::Uint8 => cursor.read_u8()? as f64,
DataType::Int16 => match endian {
BinaryEndian::Little => cursor.read_i16::<LittleEndian>()? as f64,
BinaryEndian::Big => cursor.read_i16::<BigEndian>()? as f64,
},
DataType::Uint16 => match endian {
BinaryEndian::Little => cursor.read_u16::<LittleEndian>()? as f64,
BinaryEndian::Big => cursor.read_u16::<BigEndian>()? as f64,
},
DataType::Int32 => match endian {
BinaryEndian::Little => cursor.read_i32::<LittleEndian>()? as f64,
BinaryEndian::Big => cursor.read_i32::<BigEndian>()? as f64,
},
DataType::Uint32 => match endian {
BinaryEndian::Little => cursor.read_u32::<LittleEndian>()? as f64,
BinaryEndian::Big => cursor.read_u32::<BigEndian>()? as f64,
},
DataType::Float32 => match endian {
BinaryEndian::Little => cursor.read_f32::<LittleEndian>()? as f64,
BinaryEndian::Big => cursor.read_f32::<BigEndian>()? as f64,
},
DataType::Float64 => match endian {
BinaryEndian::Little => cursor.read_f64::<LittleEndian>()?,
BinaryEndian::Big => cursor.read_f64::<BigEndian>()?,
},
other => {
return Err(invalid_ply(format!(
"Vertex property type {other:?} cannot be carried"
)))
}
};
Ok(value)
}
fn read_binary_scalar_as_f32(
cursor: &mut Cursor<&[u8]>,
data_type: DataType,
endian: BinaryEndian,
) -> io::Result<f32> {
ensure_remaining(cursor, data_type.byte_length())?;
match data_type {
DataType::Int8 => cursor.read_i8().map(|value| value as f32),
DataType::Uint8 => cursor.read_u8().map(|value| value as f32),
DataType::Int16 => match endian {
BinaryEndian::Little => cursor.read_i16::<LittleEndian>().map(|value| value as f32),
BinaryEndian::Big => cursor.read_i16::<BigEndian>().map(|value| value as f32),
},
DataType::Uint16 => match endian {
BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as f32),
BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as f32),
},
DataType::Int32 => match endian {
BinaryEndian::Little => cursor.read_i32::<LittleEndian>().map(|value| value as f32),
BinaryEndian::Big => cursor.read_i32::<BigEndian>().map(|value| value as f32),
},
DataType::Uint32 => match endian {
BinaryEndian::Little => cursor.read_u32::<LittleEndian>().map(|value| value as f32),
BinaryEndian::Big => cursor.read_u32::<BigEndian>().map(|value| value as f32),
},
DataType::Int64 => match endian {
BinaryEndian::Little => cursor.read_i64::<LittleEndian>().map(|value| value as f32),
BinaryEndian::Big => cursor.read_i64::<BigEndian>().map(|value| value as f32),
},
DataType::Uint64 => match endian {
BinaryEndian::Little => cursor.read_u64::<LittleEndian>().map(|value| value as f32),
BinaryEndian::Big => cursor.read_u64::<BigEndian>().map(|value| value as f32),
},
DataType::Float32 => match endian {
BinaryEndian::Little => cursor.read_f32::<LittleEndian>(),
BinaryEndian::Big => cursor.read_f32::<BigEndian>(),
},
DataType::Float64 => match endian {
BinaryEndian::Little => cursor.read_f64::<LittleEndian>().map(|value| value as f32),
BinaryEndian::Big => cursor.read_f64::<BigEndian>().map(|value| value as f32),
},
_ => Err(invalid_ply("Unsupported binary scalar type")),
}
}
fn read_binary_scalar_as_i32(
cursor: &mut Cursor<&[u8]>,
data_type: DataType,
endian: BinaryEndian,
) -> io::Result<i32> {
ensure_remaining(cursor, data_type.byte_length())?;
match data_type {
DataType::Int8 => cursor.read_i8().map(|value| value as i32),
DataType::Uint8 => cursor.read_u8().map(|value| value as i32),
DataType::Int16 => match endian {
BinaryEndian::Little => cursor.read_i16::<LittleEndian>().map(|value| value as i32),
BinaryEndian::Big => cursor.read_i16::<BigEndian>().map(|value| value as i32),
},
DataType::Uint16 => match endian {
BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as i32),
BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as i32),
},
DataType::Int32 => match endian {
BinaryEndian::Little => cursor.read_i32::<LittleEndian>(),
BinaryEndian::Big => cursor.read_i32::<BigEndian>(),
},
DataType::Uint32 => {
let value = match endian {
BinaryEndian::Little => cursor.read_u32::<LittleEndian>()?,
BinaryEndian::Big => cursor.read_u32::<BigEndian>()?,
};
i32::try_from(value).map_err(|_| invalid_ply("Binary PLY value does not fit in int32"))
}
_ => Err(invalid_ply("Unsupported binary int32 scalar type")),
}
}
fn read_binary_scalar_as_u8(cursor: &mut Cursor<&[u8]>, data_type: DataType) -> io::Result<u8> {
ensure_remaining(cursor, data_type.byte_length())?;
match data_type {
DataType::Uint8 => cursor.read_u8(),
DataType::Int8 => {
let value = cursor.read_i8()?;
u8::try_from(value).map_err(|_| invalid_ply("Negative color component value"))
}
_ => Err(invalid_ply("Color properties must be uint8")),
}
}
fn read_binary_scalar_as_u32(
cursor: &mut Cursor<&[u8]>,
data_type: DataType,
endian: BinaryEndian,
) -> io::Result<u32> {
ensure_remaining(cursor, data_type.byte_length())?;
match data_type {
DataType::Uint8 => cursor.read_u8().map(|value| value as u32),
DataType::Int8 => {
let value = cursor.read_i8()?;
u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
}
DataType::Uint16 => match endian {
BinaryEndian::Little => cursor.read_u16::<LittleEndian>().map(|value| value as u32),
BinaryEndian::Big => cursor.read_u16::<BigEndian>().map(|value| value as u32),
},
DataType::Int16 => {
let value = match endian {
BinaryEndian::Little => cursor.read_i16::<LittleEndian>()?,
BinaryEndian::Big => cursor.read_i16::<BigEndian>()?,
};
u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
}
DataType::Uint32 => match endian {
BinaryEndian::Little => cursor.read_u32::<LittleEndian>(),
BinaryEndian::Big => cursor.read_u32::<BigEndian>(),
},
DataType::Int32 => {
let value = match endian {
BinaryEndian::Little => cursor.read_i32::<LittleEndian>()?,
BinaryEndian::Big => cursor.read_i32::<BigEndian>()?,
};
u32::try_from(value).map_err(|_| invalid_ply("Negative face index value"))
}
_ => Err(invalid_ply("Unsupported face index scalar type")),
}
}
fn read_binary_scalar_as_usize(
cursor: &mut Cursor<&[u8]>,
data_type: DataType,
endian: BinaryEndian,
) -> io::Result<usize> {
let value = read_binary_scalar_as_u32(cursor, data_type, endian)?;
usize::try_from(value).map_err(|_| invalid_ply("Binary list size is too large"))
}
fn skip_binary_element(
cursor: &mut Cursor<&[u8]>,
element: &PlyElementDef,
endian: BinaryEndian,
) -> io::Result<()> {
for _ in 0..element.count {
for property in &element.properties {
match property.kind {
PlyPropertyKind::Scalar(data_type) => skip_binary_scalar(cursor, data_type)?,
PlyPropertyKind::List {
count_type,
item_type,
} => {
let count = read_binary_scalar_as_usize(cursor, count_type, endian)?;
for _ in 0..count {
skip_binary_scalar(cursor, item_type)?;
}
}
}
}
}
Ok(())
}
fn read_ply_binary_body(
header: &PlyHeader,
schema: &PlyReadSchema,
generic_plan: &GenericPlan,
body: &[u8],
endian: BinaryEndian,
) -> io::Result<ParsedPlyData> {
let mut cursor = Cursor::new(body);
let vertex_element_index = header
.elements
.iter()
.position(|element| element.name == "vertex")
.ok_or_else(|| invalid_ply("Missing vertex element"))?;
for element in &header.elements[..vertex_element_index] {
skip_binary_element(&mut cursor, element, endian)?;
}
let vertex_capacity = body_bounded_capacity(
header.vertex_count,
body.len().saturating_sub(cursor.position() as usize),
1,
);
let mut float_positions = matches!(schema.position_data_type, DataType::Float32)
.then(|| Vec::with_capacity(vertex_capacity));
let mut int_positions = matches!(schema.position_data_type, DataType::Int32)
.then(|| Vec::with_capacity(vertex_capacity));
let mut normals = schema
.has_normals
.then(|| Vec::with_capacity(vertex_capacity));
let mut colors = (schema.color_components > 0).then(|| ParsedPlyColorData {
num_components: schema.color_components,
values: Vec::with_capacity(vertex_capacity),
});
let mut texcoords = schema
.texcoord_pair
.is_some()
.then(|| Vec::with_capacity(vertex_capacity));
let mut generic_values = generic_plan.new_values(vertex_capacity);
for _ in 0..header.vertex_count {
let mut float_position = [0.0f32; 3];
let mut int_position = [0i32; 3];
let mut normal = [0.0f32; 3];
let mut color = [0u8; 4];
let mut texcoord = [0.0f32; 2];
let mut color_component = 0usize;
for (property_index, property) in header.vertex_properties.iter().enumerate() {
match property.kind {
PlyPropertyKind::Scalar(data_type) => match property.name.as_str() {
"x" => match schema.position_data_type {
DataType::Int32 => {
int_position[0] =
read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
}
_ => {
float_position[0] =
read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
}
},
"y" => match schema.position_data_type {
DataType::Int32 => {
int_position[1] =
read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
}
_ => {
float_position[1] =
read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
}
},
"z" => match schema.position_data_type {
DataType::Int32 => {
int_position[2] =
read_binary_scalar_as_i32(&mut cursor, data_type, endian)?
}
_ => {
float_position[2] =
read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
}
},
"nx" if schema.has_normals => {
normal[0] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
}
"ny" if schema.has_normals => {
normal[1] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
}
"nz" if schema.has_normals => {
normal[2] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
}
"red" | "green" | "blue" | "alpha" if schema.color_components > 0 => {
let value = read_binary_scalar_as_u8(&mut cursor, data_type)?;
if let Some(slot) = color.get_mut(color_component) {
*slot = value;
}
color_component += 1;
}
name if schema.texcoord_pair.is_some_and(|pair| name == pair.u) => {
texcoord[0] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
}
name if schema.texcoord_pair.is_some_and(|pair| name == pair.v) => {
texcoord[1] = read_binary_scalar_as_f32(&mut cursor, data_type, endian)?
}
_ => match generic_plan.column_for(property_index) {
Some(column) => generic_values[column].push(read_binary_scalar_as_f64(
&mut cursor,
data_type,
endian,
)?),
None => skip_binary_scalar(&mut cursor, data_type)?,
},
},
PlyPropertyKind::List {
count_type,
item_type,
} => {
let count = read_binary_scalar_as_usize(&mut cursor, count_type, endian)?;
for _ in 0..count {
skip_binary_scalar(&mut cursor, item_type)?;
}
}
}
}
match schema.position_data_type {
DataType::Int32 => int_positions.as_mut().unwrap().push(int_position),
_ => float_positions.as_mut().unwrap().push(float_position),
}
if let Some(normals) = normals.as_mut() {
normals.push(normal);
}
if let Some(colors) = colors.as_mut() {
colors.values.push(color);
}
if let Some(texcoords) = texcoords.as_mut() {
texcoords.push(texcoord);
}
}
let face_element_index = header
.elements
.iter()
.position(|element| element.name == "face");
if let Some(face_element_index) = face_element_index {
if face_element_index < vertex_element_index {
return Err(invalid_ply(
"PLY face element before vertex element is not supported",
));
}
for element in &header.elements[vertex_element_index + 1..face_element_index] {
skip_binary_element(&mut cursor, element, endian)?;
}
}
if header.face_count > 0 && header.face_properties.is_empty() {
return Err(invalid_ply(
"Binary PLY faces require a face property declaration",
));
}
let index_property = face_index_property(&header.face_properties);
let mut faces = Vec::with_capacity(body_bounded_capacity(
header.face_count,
body.len().saturating_sub(cursor.position() as usize),
1,
));
for _ in 0..header.face_count {
let mut polygon_indices: Option<Vec<u32>> = None;
for (position, property) in header.face_properties.iter().enumerate() {
match property.kind {
PlyPropertyKind::Scalar(data_type) => skip_binary_scalar(&mut cursor, data_type)?,
PlyPropertyKind::List {
count_type,
item_type,
} => {
let count = read_binary_scalar_as_usize(&mut cursor, count_type, endian)?;
if index_property == Some(position) {
let mut values = Vec::with_capacity(body_bounded_capacity(
count,
body.len().saturating_sub(cursor.position() as usize),
item_type.byte_length(),
));
for _ in 0..count {
values.push(read_binary_scalar_as_u32(&mut cursor, item_type, endian)?);
}
polygon_indices = Some(values);
} else {
for _ in 0..count {
skip_binary_scalar(&mut cursor, item_type)?;
}
}
}
}
}
if let Some(indices) = polygon_indices {
triangulate_vertex_indices(&indices, &mut faces);
}
}
Ok(ParsedPlyData {
positions: match schema.position_data_type {
DataType::Int32 => ParsedPlyPositionData::Int32(int_positions.unwrap_or_default()),
_ => ParsedPlyPositionData::Float32(float_positions.unwrap_or_default()),
},
faces,
normals,
colors,
texcoords,
generic: generic_plan.finish(generic_values),
})
}
fn read_ply<P: AsRef<Path>>(path: P) -> io::Result<ParsedPlyData> {
let bytes = fs::read(path)?;
read_ply_bytes(&bytes)
}
fn read_ply_source(source: &PlyReaderSource) -> io::Result<ParsedPlyData> {
match source {
PlyReaderSource::Path(path) => read_ply(path),
PlyReaderSource::Bytes(bytes) => read_ply_bytes(bytes),
}
}
fn read_ply_source_reporting(
source: &PlyReaderSource,
carry_generics: bool,
) -> io::Result<(ParsedPlyData, PlyLossReport)> {
match source {
PlyReaderSource::Path(path) => read_ply_bytes_reporting(&fs::read(path)?, carry_generics),
PlyReaderSource::Bytes(bytes) => read_ply_bytes_reporting(bytes, carry_generics),
}
}
fn read_ply_bytes(bytes: &[u8]) -> io::Result<ParsedPlyData> {
Ok(read_ply_bytes_reporting(bytes, false)?.0)
}
fn read_ply_bytes_reporting(
bytes: &[u8],
carry_generics: bool,
) -> io::Result<(ParsedPlyData, PlyLossReport)> {
let (header, body_offset) = parse_ply_header(bytes)?;
let schema = build_read_schema(&header)?;
let plan = GenericPlan::build(&header, &schema, carry_generics);
let report = build_loss_report(&header, &schema, !plan.is_empty());
let body = &bytes[body_offset..];
let parsed = match header.format {
PlyFormat::Ascii => read_ply_ascii_body(&header, &schema, &plan, body)?,
PlyFormat::BinaryLittleEndian => {
read_ply_binary_body(&header, &schema, &plan, body, BinaryEndian::Little)?
}
PlyFormat::BinaryBigEndian => {
read_ply_binary_body(&header, &schema, &plan, body, BinaryEndian::Big)?
}
};
Ok((parsed, report))
}
pub fn write_ply_positions<P: AsRef<Path>>(path: P, points: &[[f32; 3]]) -> io::Result<()> {
let mut file = fs::File::create(path)?;
writeln!(file, "ply")?;
writeln!(file, "format ascii 1.0")?;
writeln!(file, "element vertex {}", points.len())?;
writeln!(file, "property float x")?;
writeln!(file, "property float y")?;
writeln!(file, "property float z")?;
writeln!(file, "end_header")?;
for p in points {
writeln!(file, "{:.6} {:.6} {:.6}", p[0], p[1], p[2])?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use draco_core::geometry_attribute::GeometryAttributeType;
use tempfile::NamedTempFile;
#[test]
fn test_read_write_ply() {
let expected = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[-1.0, -1.0, -1.0],
];
let file = NamedTempFile::new().unwrap();
write_ply_positions(file.path(), &expected).unwrap();
let positions = read_ply_positions(file.path()).unwrap();
assert_eq!(positions.len(), expected.len());
for (i, (a, b)) in positions.iter().zip(expected.iter()).enumerate() {
let diff = (a[0] - b[0]).abs() + (a[1] - b[1]).abs() + (a[2] - b[2]).abs();
assert!(
diff < 1e-5,
"Position mismatch at index {i}: {a:?} vs {b:?}"
);
}
}
#[test]
fn test_read_mesh_parses_and_triangulates_faces() {
let file = NamedTempFile::new().unwrap();
let ply = r#"ply
format ascii 1.0
element vertex 4
property float x
property float y
property float z
element face 2
property list uchar int vertex_indices
end_header
0 0 0
1 0 0
1 1 0
0 1 0
3 0 1 2
4 0 1 2 3
"#;
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let mesh = reader.read_mesh().unwrap();
assert_eq!(mesh.num_points(), 4);
assert_eq!(mesh.num_faces(), 3);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(0)),
[0u32.into(), 1u32.into(), 2u32.into()]
);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(1)),
[0u32.into(), 1u32.into(), 2u32.into()]
);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(2)),
[0u32.into(), 2u32.into(), 3u32.into()]
);
}
#[test]
fn test_read_mesh_parses_normals_and_colors() {
let file = NamedTempFile::new().unwrap();
let ply = r#"ply
format ascii 1.0
element vertex 2
property float x
property float y
property float z
property float nx
property float ny
property float nz
property uchar red
property uchar green
property uchar blue
property uchar alpha
end_header
0 0 0 0 0 1 10 20 30 40
1 0 0 0 1 0 50 60 70 80
"#;
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let mesh = reader.read_mesh().unwrap();
assert_eq!(mesh.num_points(), 2);
assert_eq!(mesh.num_faces(), 0);
assert_eq!(mesh.num_attributes(), 3);
let normal_att = mesh.named_attribute(GeometryAttributeType::Normal).unwrap();
assert_eq!(normal_att.data_type(), DataType::Float32);
assert_eq!(normal_att.num_components(), 3);
assert!(!normal_att.normalized());
let normal_data = normal_att.buffer().data();
let first_normal = [
f32::from_le_bytes(normal_data[0..4].try_into().unwrap()),
f32::from_le_bytes(normal_data[4..8].try_into().unwrap()),
f32::from_le_bytes(normal_data[8..12].try_into().unwrap()),
];
assert_eq!(first_normal, [0.0, 0.0, 1.0]);
let color_att = mesh.named_attribute(GeometryAttributeType::Color).unwrap();
assert_eq!(color_att.data_type(), DataType::Uint8);
assert_eq!(color_att.num_components(), 4);
assert!(color_att.normalized());
assert_eq!(color_att.buffer().data(), &[10, 20, 30, 40, 50, 60, 70, 80]);
}
#[test]
fn test_read_mesh_preserves_int32_positions() {
let file = NamedTempFile::new().unwrap();
let ply = r#"ply
format ascii 1.0
element vertex 2
property int x
property int y
property int z
end_header
1 2 3
4 5 6
"#;
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let mesh = reader.read_mesh().unwrap();
let position_att = mesh
.named_attribute(GeometryAttributeType::Position)
.unwrap();
assert_eq!(position_att.data_type(), DataType::Int32);
assert_eq!(position_att.num_components(), 3);
assert!(!position_att.normalized());
let position_data = position_att.buffer().data();
let first_position = [
i32::from_le_bytes(position_data[0..4].try_into().unwrap()),
i32::from_le_bytes(position_data[4..8].try_into().unwrap()),
i32::from_le_bytes(position_data[8..12].try_into().unwrap()),
];
assert_eq!(first_position, [1, 2, 3]);
}
#[test]
fn test_read_mesh_ignores_non_float_normals() {
let file = NamedTempFile::new().unwrap();
let ply = r#"ply
format ascii 1.0
element vertex 1
property float x
property float y
property float z
property int nx
property int ny
property int nz
end_header
0 0 0 0 0 1
"#;
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let mesh = reader.read_mesh().unwrap();
assert_eq!(mesh.named_attribute_id(GeometryAttributeType::Normal), -1);
}
#[test]
fn test_read_mesh_rejects_non_uint8_colors() {
let file = NamedTempFile::new().unwrap();
let ply = r#"ply
format ascii 1.0
element vertex 1
property float x
property float y
property float z
property int red
property int green
property int blue
end_header
0 0 0 1 2 3
"#;
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let error = reader.read_mesh().unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
assert!(error.to_string().contains("Color properties must be uint8"));
}
#[test]
fn test_read_mesh_skips_non_index_face_lists() {
let file = NamedTempFile::new().unwrap();
let ply = r#"ply
format ascii 1.0
element vertex 4
property float x
property float y
property float z
element face 2
property list uchar int vertex_indices
property list uchar float texcoord
end_header
0 0 0
1 0 0
1 1 0
0 1 0
3 0 1 2 6 0 0 1 0 1 1
4 0 1 2 3 8 0 0 1 0 1 1 0 1
"#;
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let mesh = reader.read_mesh().unwrap();
assert_eq!(mesh.num_points(), 4);
assert_eq!(mesh.num_faces(), 3);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(2)),
[0u32.into(), 2u32.into(), 3u32.into()]
);
}
#[test]
fn test_read_binary_mesh_skips_non_index_face_lists() {
let file = NamedTempFile::new().unwrap();
let mut ply = Vec::new();
ply.extend_from_slice(
br#"ply
format binary_little_endian 1.0
element vertex 4
property float x
property float y
property float z
element face 2
property list uchar int vertex_indices
property list uchar float texcoord
end_header
"#,
);
for vertex in [
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
] {
for component in vertex {
ply.extend_from_slice(&component.to_le_bytes());
}
}
for indices in [vec![0i32, 1, 2], vec![0, 2, 3]] {
ply.push(indices.len() as u8);
for index in &indices {
ply.extend_from_slice(&index.to_le_bytes());
}
ply.push((indices.len() * 2) as u8);
for corner in 0..indices.len() * 2 {
ply.extend_from_slice(&(corner as f32).to_le_bytes());
}
}
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let mesh = reader.read_mesh().unwrap();
assert_eq!(mesh.num_points(), 4);
assert_eq!(mesh.num_faces(), 2);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(1)),
[0u32.into(), 2u32.into(), 3u32.into()]
);
}
#[test]
fn test_read_binary_little_endian_mesh() {
let file = NamedTempFile::new().unwrap();
let mut ply = Vec::new();
ply.extend_from_slice(
br#"ply
format binary_little_endian 1.0
element vertex 4
property float x
property float y
property float z
element face 2
property list uchar int vertex_indices
end_header
"#,
);
for vertex in [
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
] {
for component in vertex {
ply.extend_from_slice(&component.to_le_bytes());
}
}
ply.push(3);
for index in [0i32, 1, 2] {
ply.extend_from_slice(&index.to_le_bytes());
}
ply.push(4);
for index in [0i32, 1, 2, 3] {
ply.extend_from_slice(&index.to_le_bytes());
}
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let mesh = reader.read_mesh().unwrap();
assert_eq!(mesh.num_points(), 4);
assert_eq!(mesh.num_faces(), 3);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(0)),
[0u32.into(), 1u32.into(), 2u32.into()]
);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(1)),
[0u32.into(), 1u32.into(), 2u32.into()]
);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(2)),
[0u32.into(), 2u32.into(), 3u32.into()]
);
}
#[test]
fn test_read_binary_little_endian_mesh_with_cr_only_header() {
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();
for index in 0..24 {
ply.extend_from_slice(&(index as f32).to_le_bytes());
ply.extend_from_slice(&0.0f32.to_le_bytes());
ply.extend_from_slice(&0.0f32.to_le_bytes());
}
ply.extend_from_slice(&[3]);
for index in [0i32, 1, 2] {
ply.extend_from_slice(&index.to_le_bytes());
}
let mesh =
PlyReader::read_from_bytes(&ply).expect("CR-only binary PLY header should parse");
assert_eq!(mesh.num_faces(), 1);
assert_eq!(mesh.num_points(), 3);
let positions = mesh.attribute(0).read_f32s(mesh.num_points(), 3);
assert_eq!(
positions,
vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0],
"the CR-only header left the vertex payload misaligned"
);
}
#[test]
fn test_read_binary_little_endian_attributes_and_int_positions() {
let file = NamedTempFile::new().unwrap();
let mut ply = Vec::new();
ply.extend_from_slice(
br#"ply
format binary_little_endian 1.0
element vertex 2
property int x
property int y
property int z
property float nx
property float ny
property float nz
property uchar red
property uchar green
property uchar blue
property uchar alpha
end_header
"#,
);
for (position, normal, color) in [
([1i32, 2, 3], [0.0f32, 0.0, 1.0], [10u8, 20, 30, 40]),
([4i32, 5, 6], [0.0f32, 1.0, 0.0], [50u8, 60, 70, 80]),
] {
for component in position {
ply.extend_from_slice(&component.to_le_bytes());
}
for component in normal {
ply.extend_from_slice(&component.to_le_bytes());
}
ply.extend_from_slice(&color);
}
std::fs::write(file.path(), ply).unwrap();
let mut reader = PlyReader::open(file.path()).unwrap();
let mesh = reader.read_mesh().unwrap();
let position_att = mesh
.named_attribute(GeometryAttributeType::Position)
.unwrap();
assert_eq!(position_att.data_type(), DataType::Int32);
assert_eq!(position_att.num_components(), 3);
let position_data = position_att.buffer().data();
let first_position = [
i32::from_le_bytes(position_data[0..4].try_into().unwrap()),
i32::from_le_bytes(position_data[4..8].try_into().unwrap()),
i32::from_le_bytes(position_data[8..12].try_into().unwrap()),
];
assert_eq!(first_position, [1, 2, 3]);
let normal_att = mesh.named_attribute(GeometryAttributeType::Normal).unwrap();
assert_eq!(normal_att.data_type(), DataType::Float32);
assert_eq!(normal_att.num_components(), 3);
let normal_data = normal_att.buffer().data();
let first_normal = [
f32::from_le_bytes(normal_data[0..4].try_into().unwrap()),
f32::from_le_bytes(normal_data[4..8].try_into().unwrap()),
f32::from_le_bytes(normal_data[8..12].try_into().unwrap()),
];
assert_eq!(first_normal, [0.0, 0.0, 1.0]);
let color_att = mesh.named_attribute(GeometryAttributeType::Color).unwrap();
assert_eq!(color_att.data_type(), DataType::Uint8);
assert_eq!(color_att.num_components(), 4);
assert!(color_att.normalized());
assert_eq!(color_att.buffer().data(), &[10, 20, 30, 40, 50, 60, 70, 80]);
}
#[test]
fn test_read_binary_big_endian_mesh() {
let mut ply = Vec::new();
ply.extend_from_slice(
br#"ply
format binary_big_endian 1.0
element vertex 4
property float x
property float y
property float z
element face 1
property list uchar int vertex_indices
end_header
"#,
);
for vertex in [
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
] {
for component in vertex {
ply.extend_from_slice(&component.to_be_bytes());
}
}
ply.push(4);
for index in [0i32, 1, 2, 3] {
ply.extend_from_slice(&index.to_be_bytes());
}
let mesh = PlyReader::read_from_bytes(&ply).unwrap();
assert_eq!(mesh.num_points(), 4);
assert_eq!(mesh.num_faces(), 2);
assert_eq!(
mesh.face(draco_core::geometry_indices::FaceIndex(1)),
[0u32.into(), 2u32.into(), 3u32.into()]
);
}
#[test]
fn test_loss_report_is_empty_for_a_file_the_reader_carries_whole() {
let ply = r#"ply
format ascii 1.0
element vertex 1
property float x
property float y
property float z
property float nx
property float ny
property float nz
property uchar red
property uchar green
property uchar blue
element face 1
property list uchar int vertex_indices
end_header
0 0 0 0 0 1 255 0 0
3 0 0 0
"#;
let report = PlyReader::from_bytes(ply.as_bytes().to_vec())
.loss_report()
.unwrap();
assert!(report.is_lossless(), "{:?}", report.dropped());
}
#[test]
fn test_loss_report_omits_every_property_spelling_the_reader_consumes() {
for (label, texcoords) in [
("texture_u/texture_v", ("texture_u", "texture_v")),
("u/v", ("u", "v")),
("s/t", ("s", "t")),
] {
let (u, v) = texcoords;
let ply = format!(
"ply\n\
format ascii 1.0\n\
element vertex 1\n\
property float x\n\
property float y\n\
property float z\n\
property float nx\n\
property float ny\n\
property float nz\n\
property uchar red\n\
property uchar green\n\
property uchar blue\n\
property uchar alpha\n\
property float {u}\n\
property float {v}\n\
end_header\n\
0 0 0 0 0 1 255 0 0 255 0.5 0.5\n"
);
let report = PlyReader::from_bytes(ply.into_bytes())
.loss_report()
.unwrap();
assert!(
report.is_lossless(),
"{label} is read but reported as lost: {:?}",
report.dropped()
);
}
}
#[test]
fn test_loss_report_names_the_texcoord_pair_that_lost() {
let ply = r#"ply
format ascii 1.0
element vertex 1
property float x
property float y
property float z
property float u
property float v
property float s
property float t
end_header
0 0 0 0.5 0.5 0.25 0.75
"#;
let report = PlyReader::from_bytes(ply.as_bytes().to_vec())
.loss_report()
.unwrap();
assert_eq!(
report.dropped(),
[
PlyDroppedItem::VertexProperty {
name: "s".to_string(),
data_type: Some(DataType::Float32),
},
PlyDroppedItem::VertexProperty {
name: "t".to_string(),
data_type: Some(DataType::Float32),
},
]
);
}
#[test]
fn test_an_incomplete_texcoord_pair_is_an_ordinary_property() {
let lone = r#"ply
format ascii 1.0
element vertex 2
property float x
property float y
property float z
property float t
end_header
0 0 0 0.5
1 0 0 1.5
"#;
let (mesh, report) = PlyReader::from_bytes(lone.as_bytes().to_vec())
.read_mesh_reporting_loss()
.expect("a lone t is not a malformed texture coordinate");
assert_eq!(mesh.num_points(), 2);
assert!(mesh.named_attribute_id(GeometryAttributeType::TexCoord) < 0);
assert_eq!(
report.dropped(),
[PlyDroppedItem::VertexProperty {
name: "t".to_string(),
data_type: Some(DataType::Float32),
}]
);
let mesh = PlyReader::from_bytes(lone.as_bytes().to_vec())
.with_generic_attributes(true)
.read_mesh()
.unwrap();
assert_eq!(generic_values(&mesh, "t"), Some(vec![0.5, 1.5]));
}
#[test]
fn test_only_a_complete_float_pair_is_read_as_texture_coordinates() {
let doubles = r#"ply
format ascii 1.0
element vertex 1
property float x
property float y
property float z
property double u
property double v
end_header
0 0 0 0.25 0.75
"#;
let (mesh, report) = PlyReader::from_bytes(doubles.as_bytes().to_vec())
.read_mesh_reporting_loss()
.expect("a double pair reads as two ordinary properties");
assert!(mesh.named_attribute_id(GeometryAttributeType::TexCoord) < 0);
assert_eq!(report.dropped().len(), 2, "{:?}", report.dropped());
let mixed = r#"ply
format ascii 1.0
element vertex 1
property float x
property float y
property float z
property float s
property float u
property float v
end_header
0 0 0 9 0.25 0.75
"#;
let (mesh, report) = PlyReader::from_bytes(mixed.as_bytes().to_vec())
.read_mesh_reporting_loss()
.unwrap();
assert!(mesh.named_attribute_id(GeometryAttributeType::TexCoord) >= 0);
assert_eq!(
report.dropped(),
[PlyDroppedItem::VertexProperty {
name: "s".to_string(),
data_type: Some(DataType::Float32),
}]
);
}
#[test]
fn test_read_mesh_reporting_loss_agrees_with_reading_each_half_alone() {
let ply = r#"ply
format ascii 1.0
element vertex 2
property float x
property float y
property float z
property float confidence
element face 1
property list uchar int vertex_indices
property uchar flags
end_header
0 0 0 0.25
1 0 0 0.75
3 0 1 1
"#;
let (mesh, report) = PlyReader::from_bytes(ply.as_bytes().to_vec())
.read_mesh_reporting_loss()
.unwrap();
let separate_mesh = PlyReader::from_bytes(ply.as_bytes().to_vec())
.read_mesh()
.unwrap();
let separate_report = PlyReader::from_bytes(ply.as_bytes().to_vec())
.loss_report()
.unwrap();
assert_eq!(mesh.num_points(), separate_mesh.num_points());
assert_eq!(mesh.num_faces(), separate_mesh.num_faces());
assert_eq!(report, separate_report);
assert_eq!(
report.dropped(),
[
PlyDroppedItem::VertexProperty {
name: "confidence".to_string(),
data_type: Some(DataType::Float32),
},
PlyDroppedItem::FaceProperty {
name: "flags".to_string(),
},
]
);
}
#[test]
fn test_dropped_items_describe_themselves() {
let rendered: Vec<String> = [
PlyDroppedItem::VertexProperty {
name: "f_dc_0".to_string(),
data_type: Some(DataType::Float32),
},
PlyDroppedItem::VertexProperty {
name: "weights".to_string(),
data_type: None,
},
PlyDroppedItem::Normals,
PlyDroppedItem::FaceProperty {
name: "texcoord".to_string(),
},
PlyDroppedItem::Element {
name: "camera".to_string(),
count: 2,
},
]
.iter()
.map(ToString::to_string)
.collect();
assert_eq!(
rendered,
[
"vertex property \"f_dc_0\" (Float32) has no attribute to read it into",
"vertex property \"weights\" is a list, which the vertex element has no reading for",
"normals are declared but not as three float32 components, so they are not read",
"face property \"texcoord\" is not the corner-index list and is skipped",
"element \"camera\" and its 2 entries are skipped entirely",
]
);
}
fn generic_values(mesh: &Mesh, name: &str) -> Option<Vec<f64>> {
for id in 0..mesh.num_attributes() {
let attribute = mesh.attribute(id);
let unique_id = attribute.unique_id();
let carries_name = mesh
.attribute_metadata_by_unique_id(unique_id)
.and_then(|metadata| metadata.metadata().get_string("name"))
.is_some_and(|found| found == name);
if !carries_name {
continue;
}
let width = attribute.data_type().byte_length();
let data = attribute.buffer().data();
return Some(
(0..mesh.num_points())
.map(|index| {
let bytes = &data[index * width..(index + 1) * width];
match attribute.data_type() {
DataType::Uint8 => bytes[0] as f64,
DataType::Int32 => i32::from_le_bytes(bytes.try_into().unwrap()) as f64,
DataType::Float64 => f64::from_le_bytes(bytes.try_into().unwrap()),
_ => f32::from_le_bytes(bytes.try_into().unwrap()) as f64,
}
})
.collect(),
);
}
None
}
const SPLAT_PLY: &str = r#"ply
format ascii 1.0
element vertex 2
property float x
property float y
property float z
property float f_dc_0
property float opacity
property uchar confidence
end_header
0 0 0 1.5 -3.0 200
1 0 0 2.5 -4.0 100
"#;
#[test]
fn test_generic_attributes_are_off_unless_asked_for() {
let mesh = PlyReader::from_bytes(SPLAT_PLY.as_bytes().to_vec())
.read_mesh()
.unwrap();
assert_eq!(mesh.num_attributes(), 1);
assert!(generic_values(&mesh, "f_dc_0").is_none());
}
#[test]
fn test_generic_attributes_carry_values_names_and_declared_types() {
let mesh = PlyReader::from_bytes(SPLAT_PLY.as_bytes().to_vec())
.with_generic_attributes(true)
.read_mesh()
.unwrap();
assert_eq!(mesh.num_attributes(), 4, "position plus three carried");
assert_eq!(generic_values(&mesh, "f_dc_0"), Some(vec![1.5, 2.5]));
assert_eq!(generic_values(&mesh, "opacity"), Some(vec![-3.0, -4.0]));
assert_eq!(
generic_values(&mesh, "confidence"),
Some(vec![200.0, 100.0])
);
let confidence = (0..mesh.num_attributes())
.map(|id| mesh.attribute(id))
.find(|attribute| attribute.data_type() == DataType::Uint8)
.expect("the uchar property stays a uchar");
assert_eq!(confidence.attribute_type(), GeometryAttributeType::Generic);
assert_eq!(confidence.num_components(), 1);
}
#[test]
fn test_a_double_property_is_carried_onto_a_mesh() {
let ply = r#"ply
format ascii 1.0
element vertex 4
property float x
property float y
property float z
property double gps_time
element face 2
property list uchar int vertex_indices
end_header
0 0 0 1e300
1 0 0 0.5
0 1 0 1e300
1 1 0 -0.0
3 0 1 2
3 1 3 2
"#;
let mesh = PlyReader::from_bytes(ply.as_bytes().to_vec())
.with_generic_attributes(true)
.read_mesh()
.unwrap();
let attribute = (0..mesh.num_attributes())
.map(|id| mesh.attribute(id))
.find(|attribute| attribute.data_type() == DataType::Float64)
.expect("the double property stays a double");
assert_eq!(attribute.size(), 3, "the repeated value is stored once");
let positions = mesh
.named_attribute(GeometryAttributeType::Position)
.unwrap();
let mut by_position: Vec<([f32; 3], u64)> = (0..mesh.num_points())
.map(|point| {
let point = draco_core::geometry_indices::PointIndex(point as u32);
let mut position = [0.0f32; 3];
let at = positions.mapped_index(point).0 as usize * 12;
for (axis, value) in position.iter_mut().enumerate() {
let bytes = &positions.buffer().data()[at + axis * 4..at + axis * 4 + 4];
*value = f32::from_le_bytes(bytes.try_into().unwrap());
}
let at = attribute.mapped_index(point).0 as usize * 8;
let bytes = &attribute.buffer().data()[at..at + 8];
(position, u64::from_le_bytes(bytes.try_into().unwrap()))
})
.collect();
by_position.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
assert_eq!(
by_position,
[
([0.0, 0.0, 0.0], 1e300f64.to_bits()),
([0.0, 1.0, 0.0], 1e300f64.to_bits()),
([1.0, 0.0, 0.0], 0.5f64.to_bits()),
([1.0, 1.0, 0.0], (-0.0f64).to_bits()),
]
);
}
#[test]
fn test_carrying_removes_the_properties_from_the_loss_report() {
let dropped_by_default = PlyReader::from_bytes(SPLAT_PLY.as_bytes().to_vec())
.loss_report()
.unwrap();
assert_eq!(dropped_by_default.dropped().len(), 3);
let carried = PlyReader::from_bytes(SPLAT_PLY.as_bytes().to_vec())
.with_generic_attributes(true)
.loss_report()
.unwrap();
assert!(
carried.is_lossless(),
"a carried property is not lost, so naming it would name a non-problem: {:?}",
carried.dropped()
);
}
#[test]
fn test_lists_stay_dropped_and_reported_even_when_carrying() {
let ply = r#"ply
format ascii 1.0
element vertex 1
property float x
property float y
property float z
property list uchar int weights
property float confidence
end_header
0 0 0 2 7 8 0.5
"#;
let (mesh, report) = PlyReader::from_bytes(ply.as_bytes().to_vec())
.with_generic_attributes(true)
.read_mesh_reporting_loss()
.unwrap();
assert_eq!(generic_values(&mesh, "confidence"), Some(vec![0.5]));
assert_eq!(
report.dropped(),
[PlyDroppedItem::VertexProperty {
name: "weights".to_string(),
data_type: None,
}]
);
}
#[test]
fn test_generic_attributes_survive_the_binary_path() {
let mut ply = b"ply\nformat binary_little_endian 1.0\nelement vertex 2\n\
property float x\nproperty float y\nproperty float z\n\
property float opacity\nproperty uchar confidence\nend_header\n"
.to_vec();
for (position, opacity, confidence) in [
([0.0f32, 0.0, 0.0], -3.0f32, 200u8),
([1.0f32, 0.0, 0.0], -4.0f32, 100u8),
] {
for value in position {
ply.extend_from_slice(&value.to_le_bytes());
}
ply.extend_from_slice(&opacity.to_le_bytes());
ply.push(confidence);
}
let mesh = PlyReader::from_bytes(ply)
.with_generic_attributes(true)
.read_mesh()
.unwrap();
assert_eq!(generic_values(&mesh, "opacity"), Some(vec![-3.0, -4.0]));
assert_eq!(
generic_values(&mesh, "confidence"),
Some(vec![200.0, 100.0])
);
}
#[test]
fn test_loss_report_names_gaussian_splat_properties() {
let ply = r#"ply
format ascii 1.0
element vertex 1
property float x
property float y
property float z
property float f_dc_0
property float f_rest_0
property float opacity
property float scale_0
property float rot_0
end_header
0 0 0 1.2 0.1 -3.0 -2.5 1.0
"#;
let report = PlyReader::from_bytes(ply.as_bytes().to_vec())
.loss_report()
.unwrap();
let names: Vec<&str> = report
.dropped()
.iter()
.map(|item| match item {
PlyDroppedItem::VertexProperty { name, data_type } => {
assert_eq!(*data_type, Some(DataType::Float32));
name.as_str()
}
other => panic!("unexpected drop: {other:?}"),
})
.collect();
assert_eq!(names, ["f_dc_0", "f_rest_0", "opacity", "scale_0", "rot_0"]);
let mesh = PlyReader::read_from_bytes(ply.as_bytes()).unwrap();
assert_eq!(mesh.num_points(), 1);
}
#[test]
fn test_loss_report_covers_normals_faces_and_whole_elements() {
let ply = r#"ply
format ascii 1.0
element vertex 1
property double nx
property double ny
property double nz
property float x
property float y
property float z
element face 1
property list uchar int vertex_indices
property list uchar float texcoord
element camera 2
property float view_px
end_header
0 0 1 0 0 0
3 0 0 0
0.5
0.5
"#;
let report = PlyReader::from_bytes(ply.as_bytes().to_vec())
.loss_report()
.unwrap();
assert_eq!(
report.dropped(),
[
PlyDroppedItem::Normals,
PlyDroppedItem::FaceProperty {
name: "texcoord".to_string(),
},
PlyDroppedItem::Element {
name: "camera".to_string(),
count: 2,
},
]
);
}
}