Skip to main content

khora_io/asset/decoders/mesh/
dispatcher.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Mesh format dispatcher.
16//!
17//! The single canonical entry-point that the editor and runtime register
18//! under the `"mesh"` slot. Sniffs the bytes (gltf binary magic, gltf JSON,
19//! or OBJ text) and delegates to the appropriate inner decoder.
20//!
21//! Lives next to the per-format decoders (`gltf.rs`, `obj.rs`) which stay
22//! public — a downstream crate can compose its own dispatcher (for example
23//! to add FBX) and register that instead, without forking `khora-io`.
24
25use std::error::Error;
26use std::sync::Arc;
27
28use khora_core::renderer::api::scene::Mesh;
29
30use crate::asset::AssetDecoder;
31
32use super::{GltfDecoder, GltfResourceResolver, NoOpResourceResolver, ObjDecoder};
33
34/// Default mesh dispatcher: delegates to gltf or obj based on byte sniffing.
35///
36/// Intentionally **not** registered via `inventory::submit!` — this slot has
37/// multiple competing implementations (gltf vs obj vs eventual FBX) and the
38/// choice should be visible at the consumer's call site:
39///
40/// ```ignore
41/// svc.register_decoder::<Mesh>("mesh", MeshDispatcher::default());
42/// ```
43#[derive(Clone)]
44pub struct MeshDispatcher {
45    gltf: GltfDecoder,
46    obj: ObjDecoder,
47}
48
49impl MeshDispatcher {
50    /// Creates a dispatcher with the given gltf resource resolver.
51    pub fn new(resolver: Arc<dyn GltfResourceResolver>) -> Self {
52        Self {
53            gltf: GltfDecoder::new(resolver),
54            obj: ObjDecoder,
55        }
56    }
57
58    /// Returns true when `bytes` looks like binary glTF (`glTF` magic).
59    fn is_glb(bytes: &[u8]) -> bool {
60        bytes.len() >= 4 && &bytes[..4] == b"glTF"
61    }
62
63    /// Returns true when `bytes` parses as JSON starting with a `{` after
64    /// optional whitespace — the standard gltf 2.0 JSON form.
65    fn looks_like_gltf_json(bytes: &[u8]) -> bool {
66        for &b in bytes.iter().take(64) {
67            match b {
68                b' ' | b'\t' | b'\r' | b'\n' => continue,
69                b'{' => return true,
70                _ => return false,
71            }
72        }
73        false
74    }
75}
76
77impl Default for MeshDispatcher {
78    /// Default dispatcher uses [`NoOpResourceResolver`] for gltf. Suitable
79    /// for self-contained `.glb` files (the binary chunk is embedded) and
80    /// `.obj` files (no external resources at all). For `.gltf` files that
81    /// reference external `.bin` / texture buffers, supply a project-aware
82    /// resolver via [`MeshDispatcher::new`] — `ProjectVfs::open` does this
83    /// automatically with a [`FileSystemResolver`] rooted at the project's
84    /// `assets/` directory.
85    ///
86    /// [`FileSystemResolver`]: super::FileSystemResolver
87    fn default() -> Self {
88        Self::new(Arc::new(NoOpResourceResolver))
89    }
90}
91
92impl AssetDecoder<Mesh> for MeshDispatcher {
93    fn load(&self, bytes: &[u8]) -> Result<Mesh, Box<dyn Error + Send + Sync>> {
94        if Self::is_glb(bytes) || Self::looks_like_gltf_json(bytes) {
95            self.gltf.load(bytes)
96        } else {
97            self.obj.load(bytes)
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn sniffs_glb_magic() {
108        assert!(MeshDispatcher::is_glb(b"glTF\x02\x00\x00\x00"));
109    }
110
111    #[test]
112    fn sniffs_gltf_json() {
113        assert!(MeshDispatcher::looks_like_gltf_json(b"  {\"asset\":..."));
114        assert!(MeshDispatcher::looks_like_gltf_json(b"\n\t {\"asset\""));
115        assert!(!MeshDispatcher::looks_like_gltf_json(
116            b"o foo\nv 1.0 0.0 0.0"
117        ));
118    }
119}