1use spatialrust_scene::TriangleMesh;
4
5use crate::{InterchangeError, InterchangeResult};
6
7pub fn export_triangle_mesh_gltf_json(mesh: &TriangleMesh) -> InterchangeResult<String> {
9 if mesh.positions.len() % 3 != 0 {
10 return Err(InterchangeError::InvalidConfiguration(
11 "mesh positions length must be a multiple of 3".into(),
12 ));
13 }
14 if mesh.indices.len() % 3 != 0 {
15 return Err(InterchangeError::InvalidConfiguration(
16 "mesh indices length must be a multiple of 3".into(),
17 ));
18 }
19 let pos_bytes = f32_slice_as_bytes(&mesh.positions);
20 let pos_b64 = base64_encode(&pos_bytes);
21 let idx_bytes: Vec<u8> = mesh.indices.iter().flat_map(|v| v.to_le_bytes()).collect();
22 let idx_b64 = base64_encode(&idx_bytes);
23 let vertex_count = mesh.vertex_count();
24 let index_count = mesh.indices.len();
25 Ok(format!(
27 r#"{{"asset":{{"version":"2.0","generator":"spatialrust-interchange"}},"buffers":[{{"byteLength":{pos_len},"uri":"data:application/octet-stream;base64,{pos_b64}"}},{{"byteLength":{idx_len},"uri":"data:application/octet-stream;base64,{idx_b64}"}}],"bufferViews":[{{"buffer":0,"byteOffset":0,"byteLength":{pos_len},"target":34962}},{{"buffer":1,"byteOffset":0,"byteLength":{idx_len},"target":34963}}],"accessors":[{{"bufferView":0,"componentType":5126,"count":{vertex_count},"type":"VEC3"}},{{"bufferView":1,"componentType":5125,"count":{index_count},"type":"SCALAR"}}],"meshes":[{{"primitives":[{{"attributes":{{"POSITION":0}},"indices":1}}}}],"nodes":[{{"mesh":0}}],"scenes":[{{"nodes":[0]}}],"scene":0}}"#,
28 pos_len = mesh.positions.len() * 4,
29 idx_len = idx_bytes.len(),
30 pos_b64 = pos_b64,
31 idx_b64 = idx_b64,
32 vertex_count = vertex_count,
33 index_count = index_count,
34 ))
35}
36
37pub fn import_triangle_mesh_gltf_json(json: &str) -> InterchangeResult<(usize, usize)> {
42 let mesh = decode_triangle_mesh_gltf_json(json)?;
43 Ok((mesh.vertex_count(), mesh.indices.len()))
44}
45
46pub fn decode_triangle_mesh_gltf_json(json: &str) -> InterchangeResult<TriangleMesh> {
52 if !json.contains(r#""version":"2.0""#) {
53 return Err(InterchangeError::InvalidConfiguration(
54 "missing glTF 2.0 asset version".into(),
55 ));
56 }
57 let vertex_count = extract_accessor_count(json, "VEC3")?;
58 let index_count = extract_accessor_count(json, "SCALAR")?;
59 let buffers = extract_embedded_buffers(json)?;
60 if buffers.len() < 2 {
61 return Err(InterchangeError::InvalidConfiguration(
62 "glTF mesh requires embedded position and index buffers".into(),
63 ));
64 }
65 let position_bytes = base64_decode(buffers[0])?;
66 let index_bytes = base64_decode(buffers[1])?;
67 let expected_position_bytes = vertex_count
68 .checked_mul(3)
69 .and_then(|count| count.checked_mul(std::mem::size_of::<f32>()))
70 .ok_or_else(|| {
71 InterchangeError::InvalidConfiguration("position byte count overflow".into())
72 })?;
73 let expected_index_bytes =
74 index_count.checked_mul(std::mem::size_of::<u32>()).ok_or_else(|| {
75 InterchangeError::InvalidConfiguration("index byte count overflow".into())
76 })?;
77 if position_bytes.len() != expected_position_bytes {
78 return Err(InterchangeError::InvalidConfiguration(format!(
79 "position buffer has {} bytes; expected {}",
80 position_bytes.len(),
81 expected_position_bytes
82 )));
83 }
84 if index_bytes.len() != expected_index_bytes {
85 return Err(InterchangeError::InvalidConfiguration(format!(
86 "index buffer has {} bytes; expected {}",
87 index_bytes.len(),
88 expected_index_bytes
89 )));
90 }
91
92 let mut positions = Vec::with_capacity(vertex_count * 3);
93 for chunk in position_bytes.chunks_exact(4) {
94 positions.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
95 }
96 let mut indices = Vec::with_capacity(index_count);
97 for chunk in index_bytes.chunks_exact(4) {
98 let index = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
99 if usize::try_from(index).map_or(true, |index| index >= vertex_count) {
100 return Err(InterchangeError::InvalidConfiguration(format!(
101 "mesh index {} is outside {} vertices",
102 index, vertex_count
103 )));
104 }
105 indices.push(index);
106 }
107 Ok(TriangleMesh { positions, indices })
108}
109
110fn extract_accessor_count(json: &str, value_type: &str) -> InterchangeResult<usize> {
111 let marker = format!(r#""type":"{value_type}""#);
112 let idx = json
113 .find(&marker)
114 .ok_or_else(|| InterchangeError::InvalidConfiguration(format!("missing {marker}")))?;
115 let before = &json[..idx];
116 let key = "\"count\":";
117 let count_idx = before
118 .rfind(key)
119 .ok_or_else(|| InterchangeError::InvalidConfiguration("missing accessor count".into()))?;
120 let rest = &before[count_idx + key.len()..];
121 let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
122 digits.parse().map_err(|_| {
123 InterchangeError::InvalidConfiguration("accessor count is not an integer".into())
124 })
125}
126
127fn extract_embedded_buffers(json: &str) -> InterchangeResult<Vec<&str>> {
128 let marker = r#""uri":"data:application/octet-stream;base64,"#;
129 let buffers: Vec<_> =
130 json.split(marker).skip(1).filter_map(|rest| rest.split('"').next()).collect();
131 if buffers.iter().any(|buffer| buffer.is_empty() && !json.contains(r#""byteLength":0"#)) {
132 return Err(InterchangeError::InvalidConfiguration(
133 "embedded glTF buffer URI is empty or malformed".into(),
134 ));
135 }
136 Ok(buffers)
137}
138
139fn base64_decode(value: &str) -> InterchangeResult<Vec<u8>> {
140 if value.len() % 4 != 0 {
141 return Err(InterchangeError::InvalidConfiguration(
142 "base64 buffer length must be a multiple of four".into(),
143 ));
144 }
145 let mut output = Vec::with_capacity(value.len() / 4 * 3);
146 for (chunk_index, chunk) in value.as_bytes().chunks_exact(4).enumerate() {
147 let a = base64_value(chunk[0]).ok_or_else(|| invalid_base64(chunk_index))?;
148 let b = base64_value(chunk[1]).ok_or_else(|| invalid_base64(chunk_index))?;
149 let c = if chunk[2] == b'=' {
150 0
151 } else {
152 base64_value(chunk[2]).ok_or_else(|| invalid_base64(chunk_index))?
153 };
154 let d = if chunk[3] == b'=' {
155 0
156 } else {
157 base64_value(chunk[3]).ok_or_else(|| invalid_base64(chunk_index))?
158 };
159 output.push((a << 2) | (b >> 4));
160 if chunk[2] != b'=' {
161 output.push((b << 4) | (c >> 2));
162 }
163 if chunk[3] != b'=' {
164 if chunk[2] == b'=' {
165 return Err(invalid_base64(chunk_index));
166 }
167 output.push((c << 6) | d);
168 }
169 if (chunk[2] == b'=' || chunk[3] == b'=') && chunk_index + 1 != value.len() / 4 {
170 return Err(invalid_base64(chunk_index));
171 }
172 }
173 Ok(output)
174}
175
176fn base64_value(byte: u8) -> Option<u8> {
177 match byte {
178 b'A'..=b'Z' => Some(byte - b'A'),
179 b'a'..=b'z' => Some(byte - b'a' + 26),
180 b'0'..=b'9' => Some(byte - b'0' + 52),
181 b'+' => Some(62),
182 b'/' => Some(63),
183 _ => None,
184 }
185}
186
187fn invalid_base64(chunk_index: usize) -> InterchangeError {
188 InterchangeError::InvalidConfiguration(format!("invalid base64 buffer at chunk {chunk_index}"))
189}
190
191fn f32_slice_as_bytes(values: &[f32]) -> Vec<u8> {
192 values.iter().flat_map(|v| v.to_le_bytes()).collect()
193}
194
195fn base64_encode(bytes: &[u8]) -> String {
196 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
197 let mut out = String::new();
198 for chunk in bytes.chunks(3) {
199 let b0 = chunk[0] as u32;
200 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
201 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
202 let triple = (b0 << 16) | (b1 << 8) | b2;
203 out.push(TABLE[((triple >> 18) & 63) as usize] as char);
204 out.push(TABLE[((triple >> 12) & 63) as usize] as char);
205 if chunk.len() > 1 {
206 out.push(TABLE[((triple >> 6) & 63) as usize] as char);
207 } else {
208 out.push('=');
209 }
210 if chunk.len() > 2 {
211 out.push(TABLE[(triple & 63) as usize] as char);
212 } else {
213 out.push('=');
214 }
215 }
216 out
217}
218
219#[cfg(test)]
220mod tests {
221 use super::{
222 decode_triangle_mesh_gltf_json, export_triangle_mesh_gltf_json,
223 import_triangle_mesh_gltf_json,
224 };
225 use spatialrust_scene::TriangleMesh;
226
227 #[test]
228 fn roundtrips_counts() {
229 let mesh = TriangleMesh {
230 positions: vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
231 indices: vec![0, 1, 2],
232 };
233 let json = export_triangle_mesh_gltf_json(&mesh).unwrap();
234 let (vertices, indices) = import_triangle_mesh_gltf_json(&json).unwrap();
235 assert_eq!(vertices, 3);
236 assert_eq!(indices, 3);
237 assert_eq!(decode_triangle_mesh_gltf_json(&json).unwrap(), mesh);
238 }
239
240 #[test]
241 fn rejects_invalid_embedded_index() {
242 let mesh = TriangleMesh { positions: vec![0.0, 0.0, 0.0], indices: vec![] };
243 let json = export_triangle_mesh_gltf_json(&mesh).unwrap();
244 let invalid = json.replace("\"byteLength\":0", "\"byteLength\":4");
245 assert!(decode_triangle_mesh_gltf_json(&invalid).is_err());
246 }
247}