Skip to main content

draco_io/
traits.rs

1//! Common traits for geometry readers and writers.
2//!
3//! These traits define the shared mesh and point-cloud API for the OBJ, PLY,
4//! STL, and FBX adapters in this crate. They intentionally do not model a lossless
5//! scene document; full glTF scene workflows use `draco-gltf::Document` and
6//! `draco-gltf::Import` instead.
7//!
8//! # Usage
9//!
10//! Import the trait to access its methods:
11//!
12//! ```no_run
13//! # #[cfg(feature = "obj-writer")]
14//! # fn main() -> Result<(), std::io::Error> {
15//! use draco_io::{Writer, ObjWriter};
16//! # let mesh = draco_core::mesh::Mesh::new();
17//!
18//! let mut writer = ObjWriter::new();
19//! writer.add_mesh(&mesh, Some("Name"))?;  // Calls trait method
20//! writer.write("output.obj")?;
21//! # Ok(())
22//! # }
23//! # #[cfg(not(feature = "obj-writer"))]
24//! # fn main() {}
25//! ```
26//!
27//! This enables generic functions:
28//!
29//! ```no_run
30//! use std::io;
31//! use draco_core::mesh::Mesh;
32//! use draco_io::Writer;
33//!
34//! fn save<W: Writer>(mut w: W, mesh: &Mesh) -> io::Result<()> {
35//!     w.add_mesh(mesh, Some("Model"))?;
36//!     w.write("output.ext")
37//! }
38//! ```
39
40use std::io::{self, Write};
41use std::path::Path;
42
43// Used by the writer-side helpers below, which carry this same gate.
44#[cfg(any(
45    feature = "obj-writer",
46    feature = "ply-writer",
47    feature = "stl-writer",
48    feature = "fbx-writer"
49))]
50use draco_core::geometry_indices::PointIndex;
51use draco_core::mesh::Mesh;
52
53/// Common interface for geometry writers.
54///
55/// The mesh-format writers in this crate implement this trait:
56///
57/// ```no_run
58/// use std::io;
59/// use draco_core::mesh::Mesh;
60/// use draco_io::Writer;
61///
62/// fn write_mesh<W: Writer>(mut writer: W, mesh: &Mesh) -> io::Result<()> {
63///     writer.add_mesh(mesh, Some("MyMesh"))?;
64///     writer.write("output.ext")
65/// }
66/// ```
67pub trait Writer: Sized {
68    /// Create a new writer instance.
69    fn new() -> Self;
70
71    /// Add a mesh to be written.
72    ///
73    /// # Arguments
74    /// * `mesh` - The mesh to add
75    /// * `name` - Optional name for the mesh (if format supports naming)
76    ///
77    /// # Returns
78    /// * `Ok(())` on success
79    /// * `Err` if the format cannot handle this mesh (e.g., compression failure)
80    fn add_mesh(&mut self, mesh: &Mesh, name: Option<&str>) -> io::Result<()>;
81
82    /// Write all added meshes to a file.
83    ///
84    /// # Arguments
85    /// * `path` - Output file path
86    fn write<P: AsRef<Path>>(&self, path: P) -> io::Result<()>;
87
88    /// Get the number of meshes/vertices added.
89    fn vertex_count(&self) -> usize;
90
91    /// Get the number of faces added (if applicable).
92    fn face_count(&self) -> usize {
93        0
94    }
95}
96
97/// Common interface for geometry readers.
98///
99/// The mesh-format readers in this crate implement this trait:
100///
101/// ```no_run
102/// use std::io;
103/// use draco_core::mesh::Mesh;
104/// use draco_io::Reader;
105///
106/// fn load_mesh<R: Reader>(path: &str) -> io::Result<Mesh> {
107///     let mut reader = R::open(path)?;
108///     reader.read_mesh()
109/// }
110/// ```
111pub trait Reader: Sized {
112    /// Open a file for reading.
113    ///
114    /// # Arguments
115    /// * `path` - Input file path
116    fn open<P: AsRef<Path>>(path: P) -> io::Result<Self>;
117
118    /// Read multiple meshes (a scene) from the file.
119    ///
120    /// Formats that represent scenes or multiple mesh primitives should implement
121    /// this method and return all meshes in the file or scene.
122    fn read_meshes(&mut self) -> io::Result<Vec<Mesh>>;
123
124    /// Read a single mesh from the file.
125    ///
126    /// Default implementation returns the first mesh from `read_meshes()`.
127    fn read_mesh(&mut self) -> io::Result<Mesh> {
128        let meshes = self.read_meshes()?;
129        if let Some(m) = meshes.into_iter().next() {
130            Ok(m)
131        } else {
132            Err(io::Error::new(io::ErrorKind::InvalidData, "No mesh found"))
133        }
134    }
135}
136
137/// Common interface for readers that can be constructed from in-memory bytes.
138///
139/// This complements [`Reader::open`] for callers that already have file bytes
140/// loaded, or that are working in browser/embedded environments without direct
141/// filesystem access.
142pub trait ReadFromBytes: Sized {
143    /// Create a reader from a complete file payload.
144    fn from_bytes(bytes: &[u8]) -> io::Result<Self>;
145}
146
147/// Common interface for writers that can emit a complete file payload.
148///
149/// This complements [`Writer::write`] for callers that need to send bytes over
150/// the network, store them in an archive, or run roundtrips without temporary
151/// files.
152pub trait WriteToBytes: Writer {
153    /// Write all added data into a byte vector.
154    fn write_to_vec(&self) -> io::Result<Vec<u8>>;
155
156    /// Write all added data into an arbitrary byte sink.
157    fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
158        writer.write_all(&self.write_to_vec()?)
159    }
160}
161
162/// Extended writer trait for point cloud support.
163///
164/// Writers that can output point clouds (without faces) implement this trait.
165pub trait PointCloudWriter: Writer {
166    /// Add raw point positions.
167    fn add_points(&mut self, points: &[[f32; 3]]);
168
169    /// Add a single point.
170    fn add_point(&mut self, point: [f32; 3]) {
171        self.add_points(&[point]);
172    }
173}
174
175/// Extended reader trait for point cloud support.
176///
177/// Readers that can read point clouds implement this trait.
178pub trait PointCloudReader: Reader {
179    /// Read point positions only (no faces or topology).
180    fn read_points(&mut self) -> io::Result<Vec<[f32; 3]>>;
181}
182
183/// Refuses a mesh whose attribute values a writer would read past the end of.
184///
185/// Every mesh writer here reads attribute data as `point_index * byte_stride`,
186/// for `point_index` in `0..num_points`, through the panicking
187/// `DataBuffer::read` -- nine call sites across the four formats. That is
188/// sound exactly when each attribute holds at least `num_points` values, and
189/// nothing between a decoder and a writer re-checks it: the geometry arrives
190/// from `MeshDecoder`, whose counts come from a header, or from a caller that
191/// built it by hand.
192///
193/// One precondition, stated once, is what makes those nine reads provably in
194/// range. Checking at each read instead would mean deciding, nine times, what
195/// a writer should emit for a value that is not there -- and the honest answer
196/// is nothing, which is what this returns.
197///
198/// Attributes with an explicit point map are covered by the same bound: the
199/// writers index by point regardless, so a shorter value array is a mesh they
200/// cannot represent either way.
201#[cfg(any(
202    feature = "obj-writer",
203    feature = "ply-writer",
204    feature = "stl-writer",
205    feature = "fbx-writer"
206))]
207pub(crate) fn ensure_attributes_cover_points(mesh: &Mesh, format: &str) -> io::Result<()> {
208    let num_points = mesh.num_points();
209    for att_id in 0..mesh.num_attributes() {
210        let attribute = mesh.attribute(att_id);
211        if attribute.is_mapping_identity() {
212            if attribute.size() < num_points {
213                return Err(io::Error::new(
214                    io::ErrorKind::InvalidInput,
215                    format!(
216                        "{format} writer: attribute {att_id} ({:?}) holds {} values for {num_points} points",
217                        attribute.attribute_type(),
218                        attribute.size(),
219                    ),
220                ));
221            }
222            continue;
223        }
224        // With an explicit map the value count says nothing: several points
225        // legitimately name one value, which is what a reader's finalization
226        // and a Draco stream both produce. What has to hold is that every
227        // point names a value that exists.
228        for point in 0..num_points {
229            let value = attribute.mapped_index(PointIndex(point as u32)).0 as usize;
230            if value >= attribute.size() {
231                return Err(io::Error::new(
232                    io::ErrorKind::InvalidInput,
233                    format!(
234                        "{format} writer: attribute {att_id} ({:?}) maps point {point} to value {value} of {}",
235                        attribute.attribute_type(),
236                        attribute.size(),
237                    ),
238                ));
239            }
240        }
241    }
242    Ok(())
243}
244
245/// Where one point's value sits in an attribute's buffer.
246///
247/// Every writer here used to read `point * byte_stride`, which is the value's
248/// address only while the mapping is the identity. It is not: a reader merges
249/// values that repeat and a Draco stream carries the map it was written with,
250/// so several points share one value and the point index stops being an
251/// address. Upstream's writers resolve the map for the same reason.
252#[cfg(any(
253    feature = "obj-writer",
254    feature = "ply-writer",
255    feature = "stl-writer",
256    feature = "fbx-writer"
257))]
258pub(crate) fn value_offset(attribute: &draco_core::PointAttribute, point: usize) -> usize {
259    let value = if attribute.is_mapping_identity() {
260        point
261    } else {
262        attribute.mapped_index(PointIndex(point as u32)).0 as usize
263    };
264    value.saturating_mul(attribute.byte_stride() as usize)
265}
266
267/// Ends mesh construction with [`draco_core::mesh::Mesh::finalize`], reporting
268/// its refusal as the [`std::io::Error`] every reader in this crate returns.
269///
270/// The only way that pass fails is an attribute whose type its value
271/// deduplication does not cover, which is a property of the file just read --
272/// so `InvalidData` is the kind, not `Other`.
273///
274/// STL is absent from the gate and from the callers alike: the format carries
275/// no vertex identity, so welding would be that reader inventing one.
276#[cfg(any(feature = "obj-reader", feature = "ply-reader"))]
277pub(crate) fn finalize_mesh(mesh: &mut draco_core::mesh::Mesh) -> std::io::Result<()> {
278    mesh.finalize()
279        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string()))
280}