use std::{cmp, ffi::OsStr, fmt, fs, io, path::Path};
use crate::{utils::bytes::starts_with, Scene};
type Reader<B> = fn(&Path) -> io::Result<B>;
pub struct Loader<B = Vec<u8>> {
reader: Reader<B>,
merge_meshes: bool,
#[cfg(feature = "stl")]
stl_parse_color: bool,
}
fn default_reader(path: &Path) -> io::Result<Vec<u8>> {
fs::read(path)
}
impl Default for Loader<Vec<u8>> {
fn default() -> Self {
Self {
reader: default_reader,
merge_meshes: false,
#[cfg(feature = "stl")]
stl_parse_color: false,
}
}
}
impl<B: AsRef<[u8]>> Loader<B> {
#[must_use]
pub fn merge_meshes(mut self, enable: bool) -> Self {
self.merge_meshes = enable;
self
}
#[must_use]
pub fn custom_reader(mut self, reader: Reader<B>) -> Self {
self.reader = reader;
self
}
#[must_use]
pub fn with_custom_reader(reader: Reader<B>) -> Self {
Self {
reader,
merge_meshes: false,
#[cfg(feature = "stl")]
stl_parse_color: false,
}
}
pub fn load<P: AsRef<Path>>(&self, path: P) -> io::Result<Scene> {
self.load_with_reader(path.as_ref(), self.reader)
}
pub fn load_with_reader<P: AsRef<Path>, F: FnMut(&Path) -> io::Result<B>>(
&self,
path: P,
mut reader: F,
) -> io::Result<Scene> {
let path = path.as_ref();
self.load_from_slice_with_reader(reader(path)?.as_ref(), path, reader)
}
pub fn load_from_slice<P: AsRef<Path>>(&self, bytes: &[u8], path: P) -> io::Result<Scene> {
self.load_from_slice_with_reader(bytes, path.as_ref(), self.reader)
}
pub fn load_from_slice_with_reader<P: AsRef<Path>, F: FnMut(&Path) -> io::Result<B>>(
&self,
bytes: &[u8],
path: P,
#[allow(unused_variables)] reader: F,
) -> io::Result<Scene> {
let path = path.as_ref();
match detect_file_type(path, bytes) {
#[cfg(feature = "stl")]
FileType::Stl => self.load_stl_from_slice(bytes, path),
#[cfg(not(feature = "stl"))]
FileType::Stl => Err(io::Error::new(
io::ErrorKind::Unsupported,
"'stl' feature of mesh-loader must be enabled to parse STL file ({path:?})",
)),
#[cfg(feature = "collada")]
FileType::Collada => self.load_collada_from_slice(bytes, path),
#[cfg(not(feature = "collada"))]
FileType::Collada => Err(io::Error::new(
io::ErrorKind::Unsupported,
"'collada' feature of mesh-loader must be enabled to parse COLLADA file ({path:?})",
)),
#[cfg(feature = "obj")]
FileType::Obj => self.load_obj_from_slice_with_reader(bytes, path, reader),
#[cfg(not(feature = "obj"))]
FileType::Obj => Err(io::Error::new(
io::ErrorKind::Unsupported,
"'obj' feature of mesh-loader must be enabled to parse OBJ file ({path:?})",
)),
FileType::Unknown => Err(io::Error::new(
io::ErrorKind::Unsupported,
"unsupported or unrecognized file type {path:?}",
)),
}
}
#[cfg(feature = "stl")]
pub fn load_stl<P: AsRef<Path>>(&self, path: P) -> io::Result<Scene> {
let path = path.as_ref();
self.load_stl_from_slice((self.reader)(path)?.as_ref(), path)
}
#[cfg(feature = "stl")]
pub fn load_stl_from_slice<P: AsRef<Path>>(&self, bytes: &[u8], path: P) -> io::Result<Scene> {
let scene =
crate::stl::from_slice_internal(bytes, Some(path.as_ref()), self.stl_parse_color)?;
Ok(self.post_process(scene))
}
#[cfg(feature = "stl")]
#[must_use]
pub fn stl_parse_color(mut self, enable: bool) -> Self {
self.stl_parse_color = enable;
self
}
#[cfg(feature = "collada")]
pub fn load_collada<P: AsRef<Path>>(&self, path: P) -> io::Result<Scene> {
let path = path.as_ref();
self.load_collada_from_slice((self.reader)(path)?.as_ref(), path)
}
#[cfg(feature = "collada")]
pub fn load_collada_from_slice<P: AsRef<Path>>(
&self,
bytes: &[u8],
path: P,
) -> io::Result<Scene> {
let scene = crate::collada::from_slice_internal(bytes, Some(path.as_ref()))?;
Ok(self.post_process(scene))
}
#[cfg(feature = "obj")]
pub fn load_obj<P: AsRef<Path>>(&self, path: P) -> io::Result<Scene> {
self.load_obj_with_reader(path.as_ref(), self.reader)
}
#[cfg(feature = "obj")]
pub fn load_obj_from_slice<P: AsRef<Path>>(&self, bytes: &[u8], path: P) -> io::Result<Scene> {
self.load_obj_from_slice_with_reader(bytes, path.as_ref(), self.reader)
}
#[cfg(feature = "obj")]
pub fn load_obj_with_reader<P: AsRef<Path>, F: FnMut(&Path) -> io::Result<B>>(
&self,
path: P,
mut reader: F,
) -> io::Result<Scene> {
let path = path.as_ref();
self.load_obj_from_slice_with_reader(reader(path)?.as_ref(), path, reader)
}
#[cfg(feature = "obj")]
pub fn load_obj_from_slice_with_reader<P: AsRef<Path>, F: FnMut(&Path) -> io::Result<B>>(
&self,
bytes: &[u8],
path: P,
reader: F,
) -> io::Result<Scene> {
let scene = crate::obj::from_slice(bytes, Some(path.as_ref()), reader)?;
Ok(self.post_process(scene))
}
#[cfg(any(feature = "collada", feature = "obj", feature = "stl"))]
fn post_process(&self, mut scene: Scene) -> Scene {
if self.merge_meshes && scene.meshes.len() != 1 {
scene.meshes = vec![crate::Mesh::merge(scene.meshes)];
scene.materials = vec![crate::Material::default()];
}
scene
}
}
impl fmt::Debug for Loader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut d = f.debug_struct("Loader");
d.field("merge_meshes", &self.merge_meshes);
#[cfg(feature = "stl")]
d.field("stl_parse_color", &self.stl_parse_color);
d.finish_non_exhaustive()
}
}
enum FileType {
Stl,
Collada,
Obj,
Unknown,
}
fn detect_file_type(path: &Path, bytes: &[u8]) -> FileType {
match path.extension().and_then(OsStr::to_str) {
Some("stl" | "STL") => return FileType::Stl,
Some("dae" | "DAE") => return FileType::Collada,
Some("obj" | "OBJ") => return FileType::Obj,
_ => {}
}
let mut s = &bytes[..cmp::min(bytes.len(), 1024)];
while let Some((&c, s_next)) = s.split_first() {
match c {
b's' => {
if starts_with(s_next, &b"solid"[1..]) {
return FileType::Stl;
}
}
b'<' => {
if starts_with(s, b"<COLLADA") {
return FileType::Collada;
}
}
_ => {}
}
s = s_next;
}
FileType::Unknown
}