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}