#[cfg(feature = "io-mmap")]
use crate::mmap::MmapReader;
use crate::{MeshReader, MeshWriter, PointCloudReader, PointCloudWriter};
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Read};
use std::path::Path;
use threecrate_core::{Error, Point3f, PointCloud, Result, TriangleMesh, Vector3f};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlyFormat {
Ascii,
BinaryLittleEndian,
BinaryBigEndian,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PlyPropertyType {
Char,
UChar,
Short,
UShort,
Int,
UInt,
Float,
Double,
List(Box<PlyPropertyType>, Box<PlyPropertyType>), }
#[derive(Debug, Clone)]
pub struct PlyProperty {
pub name: String,
pub property_type: PlyPropertyType,
}
#[derive(Debug, Clone)]
pub struct PlyElement {
pub name: String,
pub count: usize,
pub properties: Vec<PlyProperty>,
}
#[derive(Debug, Clone)]
pub enum PlyValue {
Char(i8),
UChar(u8),
Short(i16),
UShort(u16),
Int(i32),
UInt(u32),
Float(f32),
Double(f64),
List(Vec<PlyValue>),
}
#[derive(Debug, Clone)]
pub struct PlyHeader {
pub format: PlyFormat,
pub version: String,
pub elements: Vec<PlyElement>,
pub comments: Vec<String>,
pub obj_info: Vec<String>,
}
#[derive(Debug)]
pub struct PlyData {
pub header: PlyHeader,
pub elements: HashMap<String, Vec<HashMap<String, PlyValue>>>,
}
pub struct RobustPlyReader;
#[derive(Debug, Clone)]
pub struct PlyWriteOptions {
pub format: PlyFormat,
pub comments: Vec<String>,
pub obj_info: Vec<String>,
pub custom_vertex_properties: Vec<(String, Vec<PlyValue>)>,
pub custom_face_properties: Vec<(String, Vec<PlyValue>)>,
pub include_normals: bool,
pub include_colors: bool,
pub vertex_property_order: Option<Vec<String>>,
}
impl Default for PlyWriteOptions {
fn default() -> Self {
Self {
format: PlyFormat::Ascii,
comments: Vec::new(),
obj_info: Vec::new(),
custom_vertex_properties: Vec::new(),
custom_face_properties: Vec::new(),
include_normals: true,
include_colors: false,
vertex_property_order: None,
}
}
}
impl PlyWriteOptions {
pub fn ascii() -> Self {
Self {
format: PlyFormat::Ascii,
..Default::default()
}
}
pub fn binary_little_endian() -> Self {
Self {
format: PlyFormat::BinaryLittleEndian,
..Default::default()
}
}
pub fn binary_big_endian() -> Self {
Self {
format: PlyFormat::BinaryBigEndian,
..Default::default()
}
}
pub fn with_comment<S: Into<String>>(mut self, comment: S) -> Self {
self.comments.push(comment.into());
self
}
pub fn with_obj_info<S: Into<String>>(mut self, info: S) -> Self {
self.obj_info.push(info.into());
self
}
pub fn with_normals(mut self, include: bool) -> Self {
self.include_normals = include;
self
}
pub fn with_colors(mut self, include: bool) -> Self {
self.include_colors = include;
self
}
pub fn with_vertex_property_order(mut self, order: Vec<String>) -> Self {
self.vertex_property_order = Some(order);
self
}
pub fn with_custom_vertex_property<S: Into<String>>(
mut self,
name: S,
values: Vec<PlyValue>,
) -> Self {
self.custom_vertex_properties.push((name.into(), values));
self
}
}
pub struct RobustPlyWriter;
impl RobustPlyWriter {
pub fn write_point_cloud<P: AsRef<Path>>(
cloud: &PointCloud<Point3f>,
path: P,
options: &PlyWriteOptions,
) -> Result<()> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
Self::write_point_cloud_to_writer(cloud, &mut writer, options)
}
pub fn write_point_cloud_to_writer<W: std::io::Write>(
cloud: &PointCloud<Point3f>,
writer: &mut W,
options: &PlyWriteOptions,
) -> Result<()> {
let mut ply_data = PlyData {
header: PlyHeader {
format: options.format,
version: "1.0".to_string(),
elements: Vec::new(),
comments: options.comments.clone(),
obj_info: options.obj_info.clone(),
},
elements: HashMap::new(),
};
let mut vertex_properties = Vec::new();
let mut vertex_data = Vec::new();
vertex_properties.push(PlyProperty {
name: "x".to_string(),
property_type: PlyPropertyType::Float,
});
vertex_properties.push(PlyProperty {
name: "y".to_string(),
property_type: PlyPropertyType::Float,
});
vertex_properties.push(PlyProperty {
name: "z".to_string(),
property_type: PlyPropertyType::Float,
});
for (prop_name, _) in &options.custom_vertex_properties {
if let Some(first_values) = options
.custom_vertex_properties
.iter()
.find(|(name, _)| name == prop_name)
.map(|(_, values)| values)
{
if let Some(first_value) = first_values.first() {
let prop_type = Self::value_to_property_type(first_value);
vertex_properties.push(PlyProperty {
name: prop_name.clone(),
property_type: prop_type,
});
}
}
}
if let Some(order) = &options.vertex_property_order {
vertex_properties.sort_by_key(|prop| {
order
.iter()
.position(|name| name == &prop.name)
.unwrap_or(order.len())
});
}
for (i, point) in cloud.iter().enumerate() {
let mut vertex_instance = HashMap::new();
vertex_instance.insert("x".to_string(), PlyValue::Float(point.x));
vertex_instance.insert("y".to_string(), PlyValue::Float(point.y));
vertex_instance.insert("z".to_string(), PlyValue::Float(point.z));
for (prop_name, values) in &options.custom_vertex_properties {
if i < values.len() {
vertex_instance.insert(prop_name.clone(), values[i].clone());
}
}
vertex_data.push(vertex_instance);
}
ply_data.header.elements.push(PlyElement {
name: "vertex".to_string(),
count: cloud.len(),
properties: vertex_properties,
});
ply_data.elements.insert("vertex".to_string(), vertex_data);
Self::write_ply_data(writer, &ply_data)
}
pub fn write_mesh<P: AsRef<Path>>(
mesh: &TriangleMesh,
path: P,
options: &PlyWriteOptions,
) -> Result<()> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
Self::write_mesh_to_writer(mesh, &mut writer, options)
}
pub fn write_mesh_to_writer<W: std::io::Write>(
mesh: &TriangleMesh,
writer: &mut W,
options: &PlyWriteOptions,
) -> Result<()> {
let mut ply_data = PlyData {
header: PlyHeader {
format: options.format,
version: "1.0".to_string(),
elements: Vec::new(),
comments: options.comments.clone(),
obj_info: options.obj_info.clone(),
},
elements: HashMap::new(),
};
let mut vertex_properties = Vec::new();
let mut vertex_data = Vec::new();
vertex_properties.push(PlyProperty {
name: "x".to_string(),
property_type: PlyPropertyType::Float,
});
vertex_properties.push(PlyProperty {
name: "y".to_string(),
property_type: PlyPropertyType::Float,
});
vertex_properties.push(PlyProperty {
name: "z".to_string(),
property_type: PlyPropertyType::Float,
});
if options.include_normals && mesh.normals.is_some() {
vertex_properties.push(PlyProperty {
name: "nx".to_string(),
property_type: PlyPropertyType::Float,
});
vertex_properties.push(PlyProperty {
name: "ny".to_string(),
property_type: PlyPropertyType::Float,
});
vertex_properties.push(PlyProperty {
name: "nz".to_string(),
property_type: PlyPropertyType::Float,
});
}
for (prop_name, _) in &options.custom_vertex_properties {
if let Some(first_values) = options
.custom_vertex_properties
.iter()
.find(|(name, _)| name == prop_name)
.map(|(_, values)| values)
{
if let Some(first_value) = first_values.first() {
let prop_type = Self::value_to_property_type(first_value);
vertex_properties.push(PlyProperty {
name: prop_name.clone(),
property_type: prop_type,
});
}
}
}
if let Some(order) = &options.vertex_property_order {
vertex_properties.sort_by_key(|prop| {
order
.iter()
.position(|name| name == &prop.name)
.unwrap_or(order.len())
});
}
for (i, vertex) in mesh.vertices.iter().enumerate() {
let mut vertex_instance = HashMap::new();
vertex_instance.insert("x".to_string(), PlyValue::Float(vertex.x));
vertex_instance.insert("y".to_string(), PlyValue::Float(vertex.y));
vertex_instance.insert("z".to_string(), PlyValue::Float(vertex.z));
if options.include_normals {
if let Some(normals) = &mesh.normals {
if i < normals.len() {
vertex_instance.insert("nx".to_string(), PlyValue::Float(normals[i].x));
vertex_instance.insert("ny".to_string(), PlyValue::Float(normals[i].y));
vertex_instance.insert("nz".to_string(), PlyValue::Float(normals[i].z));
}
}
}
for (prop_name, values) in &options.custom_vertex_properties {
if i < values.len() {
vertex_instance.insert(prop_name.clone(), values[i].clone());
}
}
vertex_data.push(vertex_instance);
}
ply_data.header.elements.push(PlyElement {
name: "vertex".to_string(),
count: mesh.vertices.len(),
properties: vertex_properties,
});
ply_data.elements.insert("vertex".to_string(), vertex_data);
if !mesh.faces.is_empty() {
let face_properties = vec![PlyProperty {
name: "vertex_indices".to_string(),
property_type: PlyPropertyType::List(
Box::new(PlyPropertyType::UChar),
Box::new(PlyPropertyType::Int),
),
}];
let mut face_data = Vec::new();
for face in &mesh.faces {
let mut face_instance = HashMap::new();
let indices = vec![
PlyValue::Int(face[0] as i32),
PlyValue::Int(face[1] as i32),
PlyValue::Int(face[2] as i32),
];
face_instance.insert("vertex_indices".to_string(), PlyValue::List(indices));
face_data.push(face_instance);
}
ply_data.header.elements.push(PlyElement {
name: "face".to_string(),
count: mesh.faces.len(),
properties: face_properties,
});
ply_data.elements.insert("face".to_string(), face_data);
}
Self::write_ply_data(writer, &ply_data)
}
fn write_ply_data<W: std::io::Write>(writer: &mut W, ply_data: &PlyData) -> Result<()> {
Self::write_header(writer, &ply_data.header)?;
match ply_data.header.format {
PlyFormat::Ascii => Self::write_ascii_data(writer, ply_data)?,
PlyFormat::BinaryLittleEndian => {
Self::write_binary_data::<LittleEndian, _>(writer, ply_data)?
}
PlyFormat::BinaryBigEndian => {
Self::write_binary_data::<BigEndian, _>(writer, ply_data)?
}
}
Ok(())
}
fn write_header<W: std::io::Write>(writer: &mut W, header: &PlyHeader) -> Result<()> {
writeln!(writer, "ply")?;
let format_str = match header.format {
PlyFormat::Ascii => "ascii",
PlyFormat::BinaryLittleEndian => "binary_little_endian",
PlyFormat::BinaryBigEndian => "binary_big_endian",
};
writeln!(writer, "format {} {}", format_str, header.version)?;
for comment in &header.comments {
writeln!(writer, "comment {}", comment)?;
}
for info in &header.obj_info {
writeln!(writer, "obj_info {}", info)?;
}
for element in &header.elements {
writeln!(writer, "element {} {}", element.name, element.count)?;
for property in &element.properties {
Self::write_property_definition(writer, property)?;
}
}
writeln!(writer, "end_header")?;
Ok(())
}
fn write_property_definition<W: std::io::Write>(
writer: &mut W,
property: &PlyProperty,
) -> Result<()> {
match &property.property_type {
PlyPropertyType::List(count_type, item_type) => {
let count_str = Self::property_type_to_string(count_type);
let item_str = Self::property_type_to_string(item_type);
writeln!(
writer,
"property list {} {} {}",
count_str, item_str, property.name
)?;
}
_ => {
let type_str = Self::property_type_to_string(&property.property_type);
writeln!(writer, "property {} {}", type_str, property.name)?;
}
}
Ok(())
}
fn property_type_to_string(prop_type: &PlyPropertyType) -> &'static str {
match prop_type {
PlyPropertyType::Char => "char",
PlyPropertyType::UChar => "uchar",
PlyPropertyType::Short => "short",
PlyPropertyType::UShort => "ushort",
PlyPropertyType::Int => "int",
PlyPropertyType::UInt => "uint",
PlyPropertyType::Float => "float",
PlyPropertyType::Double => "double",
PlyPropertyType::List(_, _) => "list", }
}
fn write_ascii_data<W: std::io::Write>(writer: &mut W, ply_data: &PlyData) -> Result<()> {
for element_def in &ply_data.header.elements {
if let Some(element_data) = ply_data.elements.get(&element_def.name) {
for instance in element_data {
let mut values = Vec::new();
for property in &element_def.properties {
if let Some(value) = instance.get(&property.name) {
Self::format_ascii_value(value, &mut values)?;
}
}
writeln!(writer, "{}", values.join(" "))?;
}
}
}
Ok(())
}
fn format_ascii_value(value: &PlyValue, output: &mut Vec<String>) -> Result<()> {
match value {
PlyValue::Char(v) => output.push(v.to_string()),
PlyValue::UChar(v) => output.push(v.to_string()),
PlyValue::Short(v) => output.push(v.to_string()),
PlyValue::UShort(v) => output.push(v.to_string()),
PlyValue::Int(v) => output.push(v.to_string()),
PlyValue::UInt(v) => output.push(v.to_string()),
PlyValue::Float(v) => output.push(v.to_string()),
PlyValue::Double(v) => output.push(v.to_string()),
PlyValue::List(values) => {
output.push(values.len().to_string());
for item in values {
Self::format_ascii_value(item, output)?;
}
}
}
Ok(())
}
fn write_binary_data<E: byteorder::ByteOrder, W: std::io::Write>(
writer: &mut W,
ply_data: &PlyData,
) -> Result<()> {
for element_def in &ply_data.header.elements {
if let Some(element_data) = ply_data.elements.get(&element_def.name) {
for instance in element_data {
for property in &element_def.properties {
if let Some(value) = instance.get(&property.name) {
Self::write_binary_value::<E, _>(writer, value)?;
}
}
}
}
}
Ok(())
}
fn write_binary_value<E: byteorder::ByteOrder, W: std::io::Write>(
writer: &mut W,
value: &PlyValue,
) -> Result<()> {
use byteorder::WriteBytesExt;
match value {
PlyValue::Char(v) => writer.write_i8(*v)?,
PlyValue::UChar(v) => writer.write_u8(*v)?,
PlyValue::Short(v) => writer.write_i16::<E>(*v)?,
PlyValue::UShort(v) => writer.write_u16::<E>(*v)?,
PlyValue::Int(v) => writer.write_i32::<E>(*v)?,
PlyValue::UInt(v) => writer.write_u32::<E>(*v)?,
PlyValue::Float(v) => writer.write_f32::<E>(*v)?,
PlyValue::Double(v) => writer.write_f64::<E>(*v)?,
PlyValue::List(values) => {
writer.write_u8(values.len() as u8)?;
for item in values {
Self::write_binary_value::<E, _>(writer, item)?;
}
}
}
Ok(())
}
fn value_to_property_type(value: &PlyValue) -> PlyPropertyType {
match value {
PlyValue::Char(_) => PlyPropertyType::Char,
PlyValue::UChar(_) => PlyPropertyType::UChar,
PlyValue::Short(_) => PlyPropertyType::Short,
PlyValue::UShort(_) => PlyPropertyType::UShort,
PlyValue::Int(_) => PlyPropertyType::Int,
PlyValue::UInt(_) => PlyPropertyType::UInt,
PlyValue::Float(_) => PlyPropertyType::Float,
PlyValue::Double(_) => PlyPropertyType::Double,
PlyValue::List(values) => {
let item_type = if let Some(first_item) = values.first() {
Self::value_to_property_type(first_item)
} else {
PlyPropertyType::Int
};
PlyPropertyType::List(Box::new(PlyPropertyType::UChar), Box::new(item_type))
}
}
}
}
impl RobustPlyReader {
pub fn read_ply_data<R: BufRead>(reader: &mut R) -> Result<PlyData> {
let header = Self::read_header(reader)?;
let elements = Self::read_elements(reader, &header)?;
Ok(PlyData { header, elements })
}
pub fn read_ply_file<P: AsRef<Path>>(path: P) -> Result<PlyData> {
let path = path.as_ref();
#[cfg(feature = "io-mmap")]
{
if let Some(ply_data) = Self::try_read_ply_mmap(path)? {
return Ok(ply_data);
}
}
let file = File::open(path)?;
let mut reader = BufReader::new(file);
Self::read_ply_data(&mut reader)
}
#[cfg(feature = "io-mmap")]
fn try_read_ply_mmap<P: AsRef<Path>>(path: P) -> Result<Option<PlyData>> {
let path = path.as_ref();
if !crate::mmap::should_use_mmap(path) {
return Ok(None);
}
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let header = Self::read_header(&mut reader)?;
match header.format {
PlyFormat::BinaryLittleEndian | PlyFormat::BinaryBigEndian => {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut header_size = 0;
let mut line = String::new();
loop {
let line_start = header_size;
line.clear();
let bytes_read = reader.read_line(&mut line)?;
if bytes_read == 0 {
return Err(Error::InvalidData(
"Unexpected end of file in header".to_string(),
));
}
header_size += bytes_read;
if line.trim() == "end_header" {
break;
}
}
if let Some(mut mmap_reader) = MmapReader::new(path)? {
mmap_reader.seek(header_size)?;
let elements = Self::read_elements_mmap(&mut mmap_reader, &header)?;
return Ok(Some(PlyData { header, elements }));
}
}
PlyFormat::Ascii => {
return Ok(None);
}
}
Ok(None)
}
#[cfg(feature = "io-mmap")]
fn read_elements_mmap(
reader: &mut MmapReader,
header: &PlyHeader,
) -> Result<HashMap<String, Vec<HashMap<String, PlyValue>>>> {
let mut elements = HashMap::new();
for element_def in &header.elements {
let mut element_data = Vec::with_capacity(crate::capacity_hint(element_def.count));
for _ in 0..element_def.count {
let mut instance = HashMap::new();
match header.format {
PlyFormat::BinaryLittleEndian => {
for property in &element_def.properties {
let value = Self::read_binary_property_value_mmap_le(
reader,
&property.property_type,
)?;
instance.insert(property.name.clone(), value);
}
}
PlyFormat::BinaryBigEndian => {
for property in &element_def.properties {
let value = Self::read_binary_property_value_mmap_be(
reader,
&property.property_type,
)?;
instance.insert(property.name.clone(), value);
}
}
PlyFormat::Ascii => {
return Err(Error::InvalidData(
"ASCII format should not use mmap reader".to_string(),
));
}
}
element_data.push(instance);
}
elements.insert(element_def.name.clone(), element_data);
}
Ok(elements)
}
#[cfg(feature = "io-mmap")]
fn read_binary_property_value_mmap_le(
reader: &mut MmapReader,
property_type: &PlyPropertyType,
) -> Result<PlyValue> {
match property_type {
PlyPropertyType::Char => Ok(PlyValue::Char(reader.read_u8()? as i8)),
PlyPropertyType::UChar => Ok(PlyValue::UChar(reader.read_u8()?)),
PlyPropertyType::Short => Ok(PlyValue::Short(reader.read_u16_le()? as i16)),
PlyPropertyType::UShort => Ok(PlyValue::UShort(reader.read_u16_le()?)),
PlyPropertyType::Int => Ok(PlyValue::Int(reader.read_u32_le()? as i32)),
PlyPropertyType::UInt => Ok(PlyValue::UInt(reader.read_u32_le()?)),
PlyPropertyType::Float => Ok(PlyValue::Float(reader.read_f32_le()?)),
PlyPropertyType::Double => Ok(PlyValue::Double(reader.read_f64_le()?)),
PlyPropertyType::List(count_type, item_type) => {
let count_value = Self::read_binary_property_value_mmap_le(reader, count_type)?;
let count = count_value.as_usize()?;
let mut list = Vec::with_capacity(crate::capacity_hint(count));
for _ in 0..count {
let item = Self::read_binary_property_value_mmap_le(reader, item_type)?;
list.push(item);
}
Ok(PlyValue::List(list))
}
}
}
#[cfg(feature = "io-mmap")]
fn read_binary_property_value_mmap_be(
reader: &mut MmapReader,
property_type: &PlyPropertyType,
) -> Result<PlyValue> {
match property_type {
PlyPropertyType::Char => Ok(PlyValue::Char(reader.read_u8()? as i8)),
PlyPropertyType::UChar => Ok(PlyValue::UChar(reader.read_u8()?)),
PlyPropertyType::Short => Ok(PlyValue::Short(reader.read_u16_be()? as i16)),
PlyPropertyType::UShort => Ok(PlyValue::UShort(reader.read_u16_be()?)),
PlyPropertyType::Int => Ok(PlyValue::Int(reader.read_u32_be()? as i32)),
PlyPropertyType::UInt => Ok(PlyValue::UInt(reader.read_u32_be()?)),
PlyPropertyType::Float => Ok(PlyValue::Float(reader.read_f32_be()?)),
PlyPropertyType::Double => Ok(PlyValue::Double(reader.read_f64_be()?)),
PlyPropertyType::List(count_type, item_type) => {
let count_value = Self::read_binary_property_value_mmap_be(reader, count_type)?;
let count = count_value.as_usize()?;
let mut list = Vec::with_capacity(crate::capacity_hint(count));
for _ in 0..count {
let item = Self::read_binary_property_value_mmap_be(reader, item_type)?;
list.push(item);
}
Ok(PlyValue::List(list))
}
}
}
fn read_header<R: BufRead>(reader: &mut R) -> Result<PlyHeader> {
let mut format = None;
let mut version = "1.0".to_string();
let mut elements = Vec::new();
let mut comments = Vec::new();
let mut obj_info = Vec::new();
let mut line = String::new();
reader.read_line(&mut line)?;
if line.trim() != "ply" {
return Err(Error::InvalidData(
"Not a PLY file - missing magic number".to_string(),
));
}
loop {
line.clear();
if reader.read_line(&mut line)? == 0 {
return Err(Error::InvalidData(
"Unexpected end of file in header".to_string(),
));
}
let line = line.trim();
if line == "end_header" {
break;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
match parts[0] {
"format" => {
if parts.len() < 3 {
return Err(Error::InvalidData("Invalid format line".to_string()));
}
format = Some(match parts[1] {
"ascii" => PlyFormat::Ascii,
"binary_little_endian" => PlyFormat::BinaryLittleEndian,
"binary_big_endian" => PlyFormat::BinaryBigEndian,
_ => {
return Err(Error::InvalidData(format!("Unknown format: {}", parts[1])))
}
});
version = parts[2].to_string();
}
"comment" => {
if parts.len() > 1 {
comments.push(parts[1..].join(" "));
}
}
"obj_info" => {
if parts.len() > 1 {
obj_info.push(parts[1..].join(" "));
}
}
"element" => {
if parts.len() < 3 {
return Err(Error::InvalidData("Invalid element line".to_string()));
}
let name = parts[1].to_string();
let count: usize = parts[2]
.parse()
.map_err(|_| Error::InvalidData("Invalid element count".to_string()))?;
elements.push(PlyElement {
name,
count,
properties: Vec::new(),
});
}
"property" => {
if elements.is_empty() {
return Err(Error::InvalidData("Property without element".to_string()));
}
let property = Self::parse_property(&parts[1..])?;
elements.last_mut().unwrap().properties.push(property);
}
_ => {
}
}
}
let format =
format.ok_or_else(|| Error::InvalidData("Missing format specification".to_string()))?;
Ok(PlyHeader {
format,
version,
elements,
comments,
obj_info,
})
}
fn parse_property(parts: &[&str]) -> Result<PlyProperty> {
if parts.is_empty() {
return Err(Error::InvalidData("Empty property definition".to_string()));
}
let property_type = if parts[0] == "list" {
if parts.len() < 4 {
return Err(Error::InvalidData(
"Invalid list property definition".to_string(),
));
}
let count_type = Self::parse_scalar_type(parts[1])?;
let item_type = Self::parse_scalar_type(parts[2])?;
PlyPropertyType::List(Box::new(count_type), Box::new(item_type))
} else {
if parts.len() < 2 {
return Err(Error::InvalidData(
"Invalid property definition".to_string(),
));
}
Self::parse_scalar_type(parts[0])?
};
let name = parts.last().unwrap().to_string();
Ok(PlyProperty {
name,
property_type,
})
}
fn parse_scalar_type(type_str: &str) -> Result<PlyPropertyType> {
match type_str {
"char" | "int8" => Ok(PlyPropertyType::Char),
"uchar" | "uint8" => Ok(PlyPropertyType::UChar),
"short" | "int16" => Ok(PlyPropertyType::Short),
"ushort" | "uint16" => Ok(PlyPropertyType::UShort),
"int" | "int32" => Ok(PlyPropertyType::Int),
"uint" | "uint32" => Ok(PlyPropertyType::UInt),
"float" | "float32" => Ok(PlyPropertyType::Float),
"double" | "float64" => Ok(PlyPropertyType::Double),
_ => Err(Error::InvalidData(format!(
"Unknown property type: {}",
type_str
))),
}
}
fn read_elements<R: BufRead>(
reader: &mut R,
header: &PlyHeader,
) -> Result<HashMap<String, Vec<HashMap<String, PlyValue>>>> {
let mut elements = HashMap::new();
for element_def in &header.elements {
let mut element_data = Vec::with_capacity(crate::capacity_hint(element_def.count));
for _ in 0..element_def.count {
element_data.push(Self::read_record(reader, header.format, element_def)?);
}
elements.insert(element_def.name.clone(), element_data);
}
Ok(elements)
}
fn read_record<R: BufRead>(
reader: &mut R,
format: PlyFormat,
element: &PlyElement,
) -> Result<HashMap<String, PlyValue>> {
let mut instance = HashMap::new();
match format {
PlyFormat::Ascii => {
let mut line = String::new();
reader.read_line(&mut line)?;
let values = line.trim().split_whitespace().collect::<Vec<_>>();
let mut value_idx = 0;
for property in &element.properties {
let value = Self::read_ascii_property_value(
&values,
&mut value_idx,
&property.property_type,
)?;
instance.insert(property.name.clone(), value);
}
}
PlyFormat::BinaryLittleEndian => {
for property in &element.properties {
let value = Self::read_binary_property_value::<LittleEndian, _>(
reader,
&property.property_type,
)?;
instance.insert(property.name.clone(), value);
}
}
PlyFormat::BinaryBigEndian => {
for property in &element.properties {
let value = Self::read_binary_property_value::<BigEndian, _>(
reader,
&property.property_type,
)?;
instance.insert(property.name.clone(), value);
}
}
}
Ok(instance)
}
fn read_ascii_property_value(
values: &[&str],
value_idx: &mut usize,
property_type: &PlyPropertyType,
) -> Result<PlyValue> {
if *value_idx >= values.len() {
return Err(Error::InvalidData("Not enough values in line".to_string()));
}
match property_type {
PlyPropertyType::Char => {
let val = values[*value_idx]
.parse::<i8>()
.map_err(|_| Error::InvalidData("Invalid char value".to_string()))?;
*value_idx += 1;
Ok(PlyValue::Char(val))
}
PlyPropertyType::UChar => {
let val = values[*value_idx]
.parse::<u8>()
.map_err(|_| Error::InvalidData("Invalid uchar value".to_string()))?;
*value_idx += 1;
Ok(PlyValue::UChar(val))
}
PlyPropertyType::Short => {
let val = values[*value_idx]
.parse::<i16>()
.map_err(|_| Error::InvalidData("Invalid short value".to_string()))?;
*value_idx += 1;
Ok(PlyValue::Short(val))
}
PlyPropertyType::UShort => {
let val = values[*value_idx]
.parse::<u16>()
.map_err(|_| Error::InvalidData("Invalid ushort value".to_string()))?;
*value_idx += 1;
Ok(PlyValue::UShort(val))
}
PlyPropertyType::Int => {
let val = values[*value_idx]
.parse::<i32>()
.map_err(|_| Error::InvalidData("Invalid int value".to_string()))?;
*value_idx += 1;
Ok(PlyValue::Int(val))
}
PlyPropertyType::UInt => {
let val = values[*value_idx]
.parse::<u32>()
.map_err(|_| Error::InvalidData("Invalid uint value".to_string()))?;
*value_idx += 1;
Ok(PlyValue::UInt(val))
}
PlyPropertyType::Float => {
let val = values[*value_idx]
.parse::<f32>()
.map_err(|_| Error::InvalidData("Invalid float value".to_string()))?;
*value_idx += 1;
Ok(PlyValue::Float(val))
}
PlyPropertyType::Double => {
let val = values[*value_idx]
.parse::<f64>()
.map_err(|_| Error::InvalidData("Invalid double value".to_string()))?;
*value_idx += 1;
Ok(PlyValue::Double(val))
}
PlyPropertyType::List(count_type, item_type) => {
let count_value = Self::read_ascii_property_value(values, value_idx, count_type)?;
let count = count_value.as_usize()?;
let mut list = Vec::with_capacity(crate::capacity_hint(count));
for _ in 0..count {
let item = Self::read_ascii_property_value(values, value_idx, item_type)?;
list.push(item);
}
Ok(PlyValue::List(list))
}
}
}
fn read_binary_property_value<E: byteorder::ByteOrder, R: Read>(
reader: &mut R,
property_type: &PlyPropertyType,
) -> Result<PlyValue> {
match property_type {
PlyPropertyType::Char => Ok(PlyValue::Char(reader.read_i8()?)),
PlyPropertyType::UChar => Ok(PlyValue::UChar(reader.read_u8()?)),
PlyPropertyType::Short => Ok(PlyValue::Short(reader.read_i16::<E>()?)),
PlyPropertyType::UShort => Ok(PlyValue::UShort(reader.read_u16::<E>()?)),
PlyPropertyType::Int => Ok(PlyValue::Int(reader.read_i32::<E>()?)),
PlyPropertyType::UInt => Ok(PlyValue::UInt(reader.read_u32::<E>()?)),
PlyPropertyType::Float => Ok(PlyValue::Float(reader.read_f32::<E>()?)),
PlyPropertyType::Double => Ok(PlyValue::Double(reader.read_f64::<E>()?)),
PlyPropertyType::List(count_type, item_type) => {
let count_value = Self::read_binary_property_value::<E, _>(reader, count_type)?;
let count = count_value.as_usize()?;
let mut list = Vec::with_capacity(crate::capacity_hint(count));
for _ in 0..count {
let item = Self::read_binary_property_value::<E, _>(reader, item_type)?;
list.push(item);
}
Ok(PlyValue::List(list))
}
}
}
}
impl PlyValue {
pub fn as_f32(&self) -> Result<f32> {
match self {
PlyValue::Char(v) => Ok(*v as f32),
PlyValue::UChar(v) => Ok(*v as f32),
PlyValue::Short(v) => Ok(*v as f32),
PlyValue::UShort(v) => Ok(*v as f32),
PlyValue::Int(v) => Ok(*v as f32),
PlyValue::UInt(v) => Ok(*v as f32),
PlyValue::Float(v) => Ok(*v),
PlyValue::Double(v) => Ok(*v as f32),
_ => Err(Error::InvalidData("Cannot convert list to f32".to_string())),
}
}
pub fn as_usize(&self) -> Result<usize> {
match self {
PlyValue::UChar(v) => Ok(*v as usize),
PlyValue::UShort(v) => Ok(*v as usize),
PlyValue::UInt(v) => Ok(*v as usize),
PlyValue::Int(v) if *v >= 0 => Ok(*v as usize),
_ => Err(Error::InvalidData(
"Cannot convert value to usize".to_string(),
)),
}
}
pub fn as_usize_list(&self) -> Result<Vec<usize>> {
match self {
PlyValue::List(values) => values.iter().map(|v| v.as_usize()).collect(),
_ => Err(Error::InvalidData("Value is not a list".to_string())),
}
}
}
pub struct PlyReader;
pub struct PlyWriter;
fn read_positions_fast(path: &Path) -> Result<Option<Vec<Point3f>>> {
use crate::records::{read_ascii_xyz, read_binary_xyz, BinaryXyz, Scalar};
let mut reader = BufReader::new(File::open(path)?);
let header = RobustPlyReader::read_header(&mut reader)?;
let Some(vertex) = header.elements.first().filter(|e| e.name == "vertex") else {
return Ok(None);
};
let mut offset = 0;
let mut found = [None; 3];
for (column, property) in vertex.properties.iter().enumerate() {
let scalar = match property.property_type {
PlyPropertyType::Char => Scalar::I8,
PlyPropertyType::UChar => Scalar::U8,
PlyPropertyType::Short => Scalar::I16,
PlyPropertyType::UShort => Scalar::U16,
PlyPropertyType::Int => Scalar::I32,
PlyPropertyType::UInt => Scalar::U32,
PlyPropertyType::Float => Scalar::F32,
PlyPropertyType::Double => Scalar::F64,
PlyPropertyType::List(..) => return Ok(None),
};
if let Some(axis) = ["x", "y", "z"].iter().position(|n| *n == property.name) {
found[axis] = Some((column, offset, scalar));
}
offset += scalar.size();
}
let [Some(x), Some(y), Some(z)] = found else {
return Err(Error::InvalidData(
"PLY vertex element is missing x, y or z".to_string(),
));
};
let points = match header.format {
PlyFormat::Ascii => read_ascii_xyz(&mut reader, [x.0, y.0, z.0], vertex.count)?,
PlyFormat::BinaryLittleEndian | PlyFormat::BinaryBigEndian => {
let layout = BinaryXyz {
record_size: offset,
xyz: [(x.1, x.2), (y.1, y.2), (z.1, z.2)],
big_endian: header.format == PlyFormat::BinaryBigEndian,
};
read_binary_xyz(&mut reader, &layout, vertex.count)?
}
};
Ok(Some(points))
}
impl crate::registry::PointCloudReader for PlyReader {
fn read_point_cloud(&self, path: &Path) -> Result<PointCloud<Point3f>> {
if let Some(points) = read_positions_fast(path)? {
return Ok(PointCloud::from_points(points));
}
let ply_data = RobustPlyReader::read_ply_file(path)?;
let mut points = Vec::new();
if let Some(vertex_elements) = ply_data.elements.get("vertex") {
for vertex in vertex_elements {
let x = vertex
.get("x")
.ok_or_else(|| Error::InvalidData("Missing x coordinate".to_string()))?
.as_f32()?;
let y = vertex
.get("y")
.ok_or_else(|| Error::InvalidData("Missing y coordinate".to_string()))?
.as_f32()?;
let z = vertex
.get("z")
.ok_or_else(|| Error::InvalidData("Missing z coordinate".to_string()))?
.as_f32()?;
points.push(Point3f::new(x, y, z));
}
}
Ok(PointCloud::from_points(points))
}
fn can_read(&self, path: &Path) -> bool {
if let Ok(mut file) = File::open(path) {
let mut header = [0u8; 4];
if let Ok(_) = file.read(&mut header) {
return header.starts_with(b"ply");
}
}
false
}
fn format_name(&self) -> &'static str {
"ply"
}
}
impl crate::registry::MeshReader for PlyReader {
fn read_mesh(&self, path: &Path) -> Result<TriangleMesh> {
let ply_data = RobustPlyReader::read_ply_file(path)?;
let mut vertices = Vec::new();
if let Some(vertex_elements) = ply_data.elements.get("vertex") {
for vertex in vertex_elements {
let x = vertex
.get("x")
.ok_or_else(|| Error::InvalidData("Missing x coordinate".to_string()))?
.as_f32()?;
let y = vertex
.get("y")
.ok_or_else(|| Error::InvalidData("Missing y coordinate".to_string()))?
.as_f32()?;
let z = vertex
.get("z")
.ok_or_else(|| Error::InvalidData("Missing z coordinate".to_string()))?
.as_f32()?;
vertices.push(Point3f::new(x, y, z));
}
}
let mut faces = Vec::new();
if let Some(face_elements) = ply_data.elements.get("face") {
for face in face_elements {
let indices = if let Some(vertex_indices) = face.get("vertex_indices") {
vertex_indices.as_usize_list()?
} else if let Some(vertex_index) = face.get("vertex_index") {
vertex_index.as_usize_list()?
} else {
return Err(Error::InvalidData(
"Face missing vertex indices".to_string(),
));
};
if indices.len() >= 3 {
faces.push([indices[0], indices[1], indices[2]]);
}
if indices.len() == 4 {
faces.push([indices[0], indices[2], indices[3]]);
}
}
}
let normals = if let Some(vertex_elements) = ply_data.elements.get("vertex") {
let mut normals = Vec::new();
let mut has_normals = true;
for vertex in vertex_elements {
if let (Some(nx), Some(ny), Some(nz)) =
(vertex.get("nx"), vertex.get("ny"), vertex.get("nz"))
{
normals.push(Vector3f::new(nx.as_f32()?, ny.as_f32()?, nz.as_f32()?));
} else {
has_normals = false;
break;
}
}
if has_normals {
Some(normals)
} else {
None
}
} else {
None
};
let mut mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
if let Some(normals) = normals {
mesh.set_normals(normals);
}
Ok(mesh)
}
fn can_read(&self, path: &Path) -> bool {
if let Ok(mut file) = File::open(path) {
let mut header = [0u8; 4];
if let Ok(_) = file.read(&mut header) {
return header.starts_with(b"ply");
}
}
false
}
fn format_name(&self) -> &'static str {
"ply"
}
}
impl crate::registry::PointCloudWriter for PlyWriter {
fn write_point_cloud(&self, cloud: &PointCloud<Point3f>, path: &Path) -> Result<()> {
use ply_rs::{
ply::{
Addable, DefaultElement, ElementDef, Ply, Property, PropertyDef, PropertyType,
ScalarType,
},
writer::Writer,
};
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
let mut ply = Ply::<DefaultElement>::new();
let mut vertex_element = ElementDef::new("vertex".to_string());
vertex_element.count = cloud.len();
vertex_element.properties.add(PropertyDef::new(
"x".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
vertex_element.properties.add(PropertyDef::new(
"y".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
vertex_element.properties.add(PropertyDef::new(
"z".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
ply.header.elements.add(vertex_element);
let mut vertices = Vec::new();
for point in &cloud.points {
let mut vertex = DefaultElement::new();
vertex.insert("x".to_string(), Property::Float(point.x));
vertex.insert("y".to_string(), Property::Float(point.y));
vertex.insert("z".to_string(), Property::Float(point.z));
vertices.push(vertex);
}
ply.payload.insert("vertex".to_string(), vertices);
let writer_instance = Writer::new();
writer_instance.write_ply(&mut writer, &mut ply)?;
Ok(())
}
fn format_name(&self) -> &'static str {
"ply"
}
}
impl crate::registry::MeshWriter for PlyWriter {
fn write_mesh(&self, mesh: &TriangleMesh, path: &Path) -> Result<()> {
use ply_rs::{
ply::{
Addable, DefaultElement, ElementDef, Ply, Property, PropertyDef, PropertyType,
ScalarType,
},
writer::Writer,
};
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
let mut ply = Ply::<DefaultElement>::new();
let mut vertex_element = ElementDef::new("vertex".to_string());
vertex_element.count = mesh.vertices.len();
vertex_element.properties.add(PropertyDef::new(
"x".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
vertex_element.properties.add(PropertyDef::new(
"y".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
vertex_element.properties.add(PropertyDef::new(
"z".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
if mesh.normals.is_some() {
vertex_element.properties.add(PropertyDef::new(
"nx".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
vertex_element.properties.add(PropertyDef::new(
"ny".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
vertex_element.properties.add(PropertyDef::new(
"nz".to_string(),
PropertyType::Scalar(ScalarType::Float),
));
}
ply.header.elements.add(vertex_element);
let mut vertices = Vec::new();
for (i, point) in mesh.vertices.iter().enumerate() {
let mut vertex = DefaultElement::new();
vertex.insert("x".to_string(), Property::Float(point.x));
vertex.insert("y".to_string(), Property::Float(point.y));
vertex.insert("z".to_string(), Property::Float(point.z));
if let Some(ref normals) = mesh.normals {
if i < normals.len() {
vertex.insert("nx".to_string(), Property::Float(normals[i].x));
vertex.insert("ny".to_string(), Property::Float(normals[i].y));
vertex.insert("nz".to_string(), Property::Float(normals[i].z));
}
}
vertices.push(vertex);
}
ply.payload.insert("vertex".to_string(), vertices);
if !mesh.faces.is_empty() {
let mut face_element = ElementDef::new("face".to_string());
face_element.count = mesh.faces.len();
face_element.properties.add(PropertyDef::new(
"vertex_indices".to_string(),
PropertyType::List(ScalarType::UChar, ScalarType::Int),
));
ply.header.elements.add(face_element);
let mut faces = Vec::new();
for face in &mesh.faces {
let mut face_data = DefaultElement::new();
let indices = vec![face[0] as i32, face[1] as i32, face[2] as i32];
face_data.insert("vertex_indices".to_string(), Property::ListInt(indices));
faces.push(face_data);
}
ply.payload.insert("face".to_string(), faces);
}
let writer_instance = Writer::new();
writer_instance.write_ply(&mut writer, &mut ply)?;
Ok(())
}
fn format_name(&self) -> &'static str {
"ply"
}
}
impl PointCloudReader for PlyReader {
fn read_point_cloud<P: AsRef<Path>>(path: P) -> Result<PointCloud<Point3f>> {
let reader = PlyReader;
crate::registry::PointCloudReader::read_point_cloud(&reader, path.as_ref())
}
}
impl MeshReader for PlyReader {
fn read_mesh<P: AsRef<Path>>(path: P) -> Result<TriangleMesh> {
let reader = PlyReader;
crate::registry::MeshReader::read_mesh(&reader, path.as_ref())
}
}
impl PointCloudWriter for PlyWriter {
fn write_point_cloud<P: AsRef<Path>>(cloud: &PointCloud<Point3f>, path: P) -> Result<()> {
let writer = PlyWriter;
crate::registry::PointCloudWriter::write_point_cloud(&writer, cloud, path.as_ref())
}
}
impl MeshWriter for PlyWriter {
fn write_mesh<P: AsRef<Path>>(mesh: &TriangleMesh, path: P) -> Result<()> {
let writer = PlyWriter;
crate::registry::MeshWriter::write_mesh(&writer, mesh, path.as_ref())
}
}
fn open_at_element<P: AsRef<Path>>(
path: P,
element: &str,
) -> Result<(BufReader<File>, PlyHeader, usize)> {
let mut reader = BufReader::new(File::open(path)?);
let header = RobustPlyReader::read_header(&mut reader)?;
let index = header
.elements
.iter()
.position(|e| e.name == element)
.ok_or_else(|| Error::InvalidData(format!("No {element} element found in PLY file")))?;
for skipped in &header.elements[..index] {
for _ in 0..skipped.count {
RobustPlyReader::read_record(&mut reader, header.format, skipped)?;
}
}
Ok((reader, header, index))
}
pub struct PlyStreamingReader {
reader: BufReader<File>,
format: PlyFormat,
vertex: PlyElement,
current_index: usize,
}
impl PlyStreamingReader {
pub fn new<P: AsRef<Path>>(path: P, _chunk_size: usize) -> Result<Self> {
let (reader, header, index) = open_at_element(path, "vertex")?;
Ok(Self {
reader,
format: header.format,
vertex: header.elements[index].clone(),
current_index: 0,
})
}
}
impl Iterator for PlyStreamingReader {
type Item = Result<Point3f>;
fn next(&mut self) -> Option<Self::Item> {
if self.current_index >= self.vertex.count {
return None;
}
self.current_index += 1;
let point = RobustPlyReader::read_record(&mut self.reader, self.format, &self.vertex)
.and_then(|record| {
let coord = |name: &str| {
record
.get(name)
.ok_or_else(|| Error::InvalidData(format!("Missing {name} coordinate")))?
.as_f32()
};
Ok(Point3f::new(coord("x")?, coord("y")?, coord("z")?))
});
if point.is_err() {
self.current_index = self.vertex.count; }
Some(point)
}
}
pub struct PlyMeshStreamingReader {
reader: BufReader<File>,
format: PlyFormat,
face: PlyElement,
vertex_count: usize,
current_index: usize,
}
impl PlyMeshStreamingReader {
pub fn new<P: AsRef<Path>>(path: P, _chunk_size: usize) -> Result<Self> {
let (reader, header, index) = open_at_element(path, "face")?;
let vertex_count = header
.elements
.iter()
.find(|e| e.name == "vertex")
.ok_or_else(|| Error::InvalidData("No vertex element found in PLY file".to_string()))?
.count;
Ok(Self {
reader,
format: header.format,
face: header.elements[index].clone(),
vertex_count,
current_index: 0,
})
}
}
impl Iterator for PlyMeshStreamingReader {
type Item = Result<[usize; 3]>;
fn next(&mut self) -> Option<Self::Item> {
if self.current_index >= self.face.count {
return None;
}
self.current_index += 1;
let face = RobustPlyReader::read_record(&mut self.reader, self.format, &self.face)
.and_then(|record| {
let indices = record
.get("vertex_indices")
.or_else(|| record.get("vertex_index"))
.ok_or_else(|| Error::InvalidData("Face has no vertex indices".to_string()))?
.as_usize_list()?;
let [a, b, c] = indices[..] else {
return Err(Error::InvalidData(
"Only triangular faces supported".to_string(),
));
};
if a.max(b).max(c) >= self.vertex_count {
return Err(Error::InvalidData("Face index out of range".to_string()));
}
Ok([a, b, c])
});
if face.is_err() {
self.current_index = self.face.count; }
Some(face)
}
}
#[cfg(test)]
mod streaming_tests {
use super::*;
fn write_temp(name: &str, bytes: &[u8]) -> std::path::PathBuf {
let path =
std::env::temp_dir().join(format!("threecrate_{name}_{}.ply", std::process::id()));
std::fs::write(&path, bytes).unwrap();
path
}
#[test]
fn streams_binary_ply_with_extra_properties() {
let mut bytes = b"ply\nformat binary_little_endian 1.0\nelement vertex 2\n\
property double x\nproperty double y\nproperty double z\n\
property float nx\nproperty float ny\nproperty float nz\n\
property uchar red\nproperty uchar green\nproperty uchar blue\n\
element face 1\nproperty list uchar int vertex_indices\nend_header\n"
.to_vec();
for (p, n) in [([1.0f64, 2.0, 3.0], 0.0f32), ([4.0, 5.0, 6.0], 1.0)] {
p.iter().for_each(|v| bytes.extend(v.to_le_bytes()));
[n, n, n].iter().for_each(|v| bytes.extend(v.to_le_bytes()));
bytes.extend([10u8, 20, 30]);
}
bytes.push(3);
[0i32, 1, 1]
.iter()
.for_each(|v| bytes.extend(v.to_le_bytes()));
let path = write_temp("stream_bin", &bytes);
let points: Vec<Point3f> = PlyStreamingReader::new(&path, 1)
.unwrap()
.collect::<Result<_>>()
.unwrap();
assert_eq!(
points,
vec![Point3f::new(1.0, 2.0, 3.0), Point3f::new(4.0, 5.0, 6.0)]
);
let faces: Vec<[usize; 3]> = PlyMeshStreamingReader::new(&path, 1)
.unwrap()
.collect::<Result<_>>()
.unwrap();
assert_eq!(faces, vec![[0, 1, 1]]);
std::fs::remove_file(path).ok();
}
#[test]
fn streams_ascii_with_reordered_properties_and_rejects_bad_faces() {
let text = "ply\nformat ascii 1.0\nelement vertex 2\nproperty float z\nproperty float x\n\
property float y\nelement face 1\nproperty list uchar int vertex_indices\nend_header\n\
3 1 2\n6 4 5\n3 0 1 7\n";
let path = write_temp("stream_ascii", text.as_bytes());
let points: Vec<Point3f> = PlyStreamingReader::new(&path, 8)
.unwrap()
.collect::<Result<_>>()
.unwrap();
assert_eq!(
points,
vec![Point3f::new(1.0, 2.0, 3.0), Point3f::new(4.0, 5.0, 6.0)]
);
let faces: Vec<Result<[usize; 3]>> =
PlyMeshStreamingReader::new(&path, 8).unwrap().collect();
assert_eq!(faces.len(), 1);
assert!(faces[0].is_err(), "index 7 is out of range");
std::fs::remove_file(path).ok();
}
}