1mod glb;
11mod json;
12mod obj;
13mod ply;
14mod stl;
15
16use std::ffi::OsString;
17use std::fmt;
18use std::fs::{self, File, OpenOptions};
19use std::io::{BufWriter, Write};
20use std::path::{Path, PathBuf};
21
22#[derive(Debug, Clone, PartialEq)]
24pub struct MeshData {
25 pub positions: Vec<f64>,
27 pub indices: Vec<u32>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Error(String);
34
35impl Error {
36 pub fn new(message: impl Into<String>) -> Self {
38 Error(message.into())
39 }
40}
41
42impl fmt::Display for Error {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str(&self.0)
45 }
46}
47
48impl std::error::Error for Error {}
49
50impl From<std::io::Error> for Error {
51 fn from(e: std::io::Error) -> Self {
52 Error(e.to_string())
53 }
54}
55
56pub type Result<T> = std::result::Result<T, Error>;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Format {
62 Stl,
64 Obj,
66 Ply,
68 Glb,
70}
71
72impl Format {
73 pub fn from_path(path: &Path) -> Result<Format> {
78 let extension = path
79 .extension()
80 .map(|e| e.to_string_lossy().to_ascii_lowercase());
81 match extension.as_deref() {
82 Some("stl") => Ok(Format::Stl),
83 Some("obj") => Ok(Format::Obj),
84 Some("ply") => Ok(Format::Ply),
85 Some("glb") => Ok(Format::Glb),
86 Some("gltf") => Err(Error::new(format!(
87 "{}: JSON glTF (.gltf) is not supported, use binary glTF (.glb)",
88 path.display()
89 ))),
90 _ => Err(Error::new(format!(
91 "{}: unknown file format (expected .stl, .obj, .ply or .glb)",
92 path.display()
93 ))),
94 }
95 }
96
97 pub fn has_ascii_form(self) -> bool {
99 matches!(self, Format::Stl | Format::Ply)
100 }
101}
102
103pub fn parse(format: Format, bytes: &[u8]) -> Result<MeshData> {
109 match format {
110 Format::Stl => stl::parse(bytes),
111 Format::Obj => obj::parse(bytes),
112 Format::Ply => ply::parse(bytes),
113 Format::Glb => glb::parse(bytes),
114 }
115}
116
117pub fn write_mesh<W: Write>(
126 data: &MeshData,
127 format: Format,
128 ascii: bool,
129 out: &mut W,
130) -> Result<()> {
131 let data = compacted(data)?;
132 match format {
133 Format::Stl => stl::write(&data, ascii, out),
134 Format::Obj => obj::write(&data, out),
135 Format::Ply => ply::write(&data, ascii, out),
136 Format::Glb => glb::write(&data, out),
137 }
138}
139
140fn compacted(data: &MeshData) -> Result<MeshData> {
142 if data.indices.is_empty() {
143 return Err(Error::new("the mesh has no triangles"));
144 }
145 let vertices = data.positions.len() / 3;
146 const UNUSED: u32 = u32::MAX;
147 let mut map = try_vec(vertices, UNUSED)?;
148 for &i in &data.indices {
149 let Some(slot) = map.get_mut(i as usize) else {
150 return Err(Error::new(format!(
151 "vertex {i} is past the {vertices} vertices"
152 )));
153 };
154 *slot = 0;
155 }
156 let mut positions = with_capacity(data.positions.len())?;
157 let mut next = 0u32;
158 for (v, slot) in map.iter_mut().enumerate() {
159 if *slot != UNUSED {
160 *slot = next;
161 next += 1;
162 positions.extend_from_slice(&data.positions[3 * v..3 * v + 3]);
163 }
164 }
165 let mut indices = with_capacity(data.indices.len())?;
166 indices.extend(data.indices.iter().map(|&i| map[i as usize]));
167 Ok(MeshData { positions, indices })
168}
169
170pub fn atomic_write(
177 path: &Path,
178 write: impl FnOnce(&mut BufWriter<File>) -> Result<()>,
179) -> Result<()> {
180 let target = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
181 let describe = |e: std::io::Error| Error::new(format!("{}: {e}", path.display()));
182 let name = target
183 .file_name()
184 .ok_or_else(|| Error::new(format!("{}: not a file name", path.display())))?;
185 let dir = match target.parent() {
186 Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
187 _ => PathBuf::from("."),
188 };
189 let mut temporary = OsString::from(".");
190 temporary.push(name);
191 temporary.push(format!(".{}.tmp", std::process::id()));
192 let temporary = dir.join(temporary);
193 let file = OpenOptions::new()
194 .write(true)
195 .create_new(true)
196 .open(&temporary)
197 .map_err(describe)?;
198 let finish = || -> Result<()> {
199 let mut out = BufWriter::with_capacity(1 << 20, file);
200 write(&mut out)?;
201 let file = out.into_inner().map_err(|e| describe(e.into_error()))?;
202 file.sync_all().map_err(describe)?;
203 drop(file);
204 fs::rename(&temporary, &target).map_err(describe)
205 };
206 let result = finish();
207 if result.is_err() {
208 let _ = fs::remove_file(&temporary);
209 }
210 result
211}
212
213fn out_of_memory() -> Error {
214 Error::new("out of memory")
215}
216
217fn with_capacity<T>(n: usize) -> Result<Vec<T>> {
219 let mut v = Vec::new();
220 v.try_reserve_exact(n).map_err(|_| out_of_memory())?;
221 Ok(v)
222}
223
224fn try_vec<T: Clone>(n: usize, value: T) -> Result<Vec<T>> {
226 let mut v = with_capacity(n)?;
227 v.resize(n, value);
228 Ok(v)
229}
230
231fn push_all<T: Copy>(v: &mut Vec<T>, values: &[T]) -> Result<()> {
233 v.try_reserve(values.len()).map_err(|_| out_of_memory())?;
234 v.extend_from_slice(values);
235 Ok(())
236}
237
238fn next_token<'a>(bytes: &'a [u8], at: &mut usize) -> Option<&'a [u8]> {
240 let start = *at + bytes[*at..].iter().position(|b| !b.is_ascii_whitespace())?;
241 let len = bytes[start..]
242 .iter()
243 .position(|b| b.is_ascii_whitespace())
244 .unwrap_or(bytes.len() - start);
245 *at = start + len;
246 Some(&bytes[start..start + len])
247}
248
249fn finite_number(token: &[u8]) -> Option<f64> {
251 let value: f64 = std::str::from_utf8(token).ok()?.parse().ok()?;
252 value.is_finite().then_some(value)
253}
254
255fn shown(token: &[u8]) -> String {
257 let text = String::from_utf8_lossy(&token[..token.len().min(40)]);
258 let cut = if token.len() > 40 { "..." } else { "" };
259 format!("\"{}{cut}\"", text.escape_debug())
260}
261
262fn to_f32(value: f64, format: &str) -> Result<f32> {
264 let single = value as f32;
265 if single.is_finite() {
266 Ok(single)
267 } else {
268 Err(Error::new(format!(
269 "{format}: coordinate {value:e} does not fit its 32-bit floats"
270 )))
271 }
272}
273
274fn check_mesh(data: MeshData, format: &str) -> Result<MeshData> {
276 if data.indices.is_empty() {
277 return Err(Error::new(format!("{format}: the file has no faces")));
278 }
279 let vertices = data.positions.len() / 3;
280 if vertices > u32::MAX as usize {
281 return Err(Error::new(format!(
282 "{format}: {vertices} vertices is too many for 32-bit indices"
283 )));
284 }
285 if let Some(i) = data.indices.iter().find(|&&i| i as usize >= vertices) {
286 return Err(Error::new(format!(
287 "{format}: face index {i} is past the {vertices} vertices"
288 )));
289 }
290 if !data.positions.iter().all(|x| x.is_finite()) {
291 return Err(Error::new(format!(
292 "{format}: a vertex coordinate is not a finite number"
293 )));
294 }
295 Ok(data)
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 fn temp_path(name: &str) -> PathBuf {
304 let dir = std::env::temp_dir().join(format!("fast-mesh-cli-{}-{name}", std::process::id()));
305 fs::create_dir_all(&dir).unwrap();
306 dir.join(name)
307 }
308
309 fn remove(path: &Path) {
310 let _ = fs::remove_dir_all(path.parent().unwrap());
311 }
312
313 #[test]
314 fn atomic_write_keeps_the_old_file_when_the_writer_fails_midway() {
315 let path = temp_path("midway.stl");
316 fs::write(&path, b"old").unwrap();
317 let result = atomic_write(&path, |out| {
318 out.write_all(&[7u8; 4 << 20])?;
319 Err(Error::new("disk went away"))
320 });
321 assert_eq!(result.unwrap_err().to_string(), "disk went away");
322 assert_eq!(fs::read(&path).unwrap(), b"old");
323 let names: Vec<_> = fs::read_dir(path.parent().unwrap())
324 .unwrap()
325 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
326 .filter(|n| n.contains("midway"))
327 .collect();
328 assert_eq!(names, ["midway.stl"], "no temporary file left");
329 remove(&path);
330 }
331
332 #[test]
333 fn atomic_write_replaces() {
334 let path = temp_path("replace.stl");
335 fs::write(&path, b"old").unwrap();
336 atomic_write(&path, |out| Ok(out.write_all(b"new")?)).unwrap();
337 assert_eq!(fs::read(&path).unwrap(), b"new");
338 remove(&path);
339 }
340
341 #[test]
342 fn format_from_path() {
343 assert_eq!(
344 Format::from_path(Path::new("a/b.STL")).unwrap(),
345 Format::Stl
346 );
347 assert_eq!(Format::from_path(Path::new("b.Glb")).unwrap(), Format::Glb);
348 assert!(Format::from_path(Path::new("b")).is_err());
349 let message = Format::from_path(Path::new("b.gltf")).unwrap_err();
350 assert!(message.to_string().contains(".glb"));
351 }
352
353 #[test]
354 fn compaction_keeps_vertex_order() {
355 let data = MeshData {
356 positions: vec![
357 9.0, 9.0, 9.0, 0.0, 0.0, 0.0, 8.0, 8.0, 8.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0,
358 ],
359 indices: vec![4, 1, 3],
360 };
361 let out = compacted(&data).unwrap();
362 assert_eq!(out.positions, [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
363 assert_eq!(out.indices, [2, 0, 1]);
364 }
365
366 #[test]
367 fn tokens() {
368 let mut at = 0;
369 let bytes = b" ab\tc\n\n";
370 assert_eq!(next_token(bytes, &mut at), Some(&b"ab"[..]));
371 assert_eq!(next_token(bytes, &mut at), Some(&b"c"[..]));
372 assert_eq!(next_token(bytes, &mut at), None);
373 assert_eq!(next_token(b"", &mut 0), None);
374 }
375}