1use crate::{FixError, LoadError, WriteError, resolve_buffers, safe_external_buffer_path};
28use std::ops::Range;
29use std::path::Path;
30
31const ROTATION_ELEMENT_BYTES: usize = 16;
32const QUAT_NORM_TOLERANCE: f32 = 1e-3;
33
34#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub struct TrackFix {
38 pub clip: String,
40 pub bone: String,
42 pub fixed_keys: usize,
44}
45
46#[derive(Debug, Clone, Default)]
51#[non_exhaustive]
52pub struct FixReport {
53 pub tracks: Vec<TrackFix>,
55 pub skipped: Vec<String>,
58}
59
60impl FixReport {
61 pub fn total_fixed(&self) -> usize {
63 self.tracks.iter().map(|t| t.fixed_keys).sum()
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69#[non_exhaustive]
70pub enum Repair {
71 QuatNorm,
73 QuatFlip,
75}
76
77impl Repair {
78 pub const ALL: &'static [Self] = &[Self::QuatNorm, Self::QuatFlip];
80
81 pub fn id(self) -> &'static str {
84 match self {
85 Self::QuatNorm => "quat-norm",
86 Self::QuatFlip => "quat-flip",
87 }
88 }
89
90 pub fn from_id(id: &str) -> Option<Self> {
92 Self::ALL.iter().copied().find(|repair| repair.id() == id)
93 }
94}
95
96pub struct FixSession {
102 original: Vec<u8>,
103 gltf: gltf::Gltf,
104 buffers: Vec<Vec<u8>>,
105}
106
107impl FixSession {
108 pub fn read(input: &Path) -> Result<Self, FixError> {
115 let original = std::fs::read(input).map_err(|source| LoadError::Io {
116 path: input.display().to_string(),
117 source,
118 })?;
119 crate::validate_glb_framing(&original)?;
120 let gltf = gltf::Gltf::from_slice(&original).map_err(LoadError::from)?;
121 crate::validate_animation_channels(gltf.document.as_json())?;
122
123 let buffers = resolve_buffers(&gltf, input.parent())?;
128
129 Ok(Self {
130 original,
131 gltf,
132 buffers,
133 })
134 }
135
136 pub fn apply(&mut self, repair: Repair) -> FixReport {
141 match repair {
142 Repair::QuatNorm => self.apply_quat_norm(),
143 Repair::QuatFlip => self.apply_quat_flip(),
144 }
145 }
146
147 pub fn apply_to_path(
156 input: &Path,
157 output: &Path,
158 repair: Repair,
159 ) -> Result<FixReport, FixError> {
160 let mut session = Self::read(input)?;
161 let report = session.apply(repair);
162 session.write(input, output)?;
163 Ok(report)
164 }
165
166 pub fn inspect(input: &Path, repair: Repair) -> Result<FixReport, FixError> {
173 let mut session = Self::read(input)?;
174 Ok(session.apply(repair))
175 }
176
177 fn apply_quat_norm(&mut self) -> FixReport {
178 self.repair_rotation_tracks(|buffer, layout| {
179 if layout.cubic {
180 let needs_repair = (0..layout.keys).any(|k| {
181 let value_element = k * layout.per_key + layout.value_offset;
182 let q = read_rotation(buffer, layout, value_element);
183 if !q.iter().all(|v| v.is_finite()) {
184 return false;
185 }
186 let len = q.iter().map(|v| v * v).sum::<f32>().sqrt();
187 len <= f32::EPSILON || (len - 1.0).abs() > QUAT_NORM_TOLERANCE
188 });
189 return (
190 0,
191 needs_repair.then_some(
192 "cubic rotation output (quat-norm skipped to preserve tangents)",
193 ),
194 );
195 }
196
197 let mut fixed = 0usize;
198 let mut skipped = None;
199 for k in 0..layout.keys {
200 let value_element = k * layout.per_key + layout.value_offset;
201 let q = read_rotation(buffer, layout, value_element);
202 if !q.iter().all(|v| v.is_finite()) {
203 skipped = Some("non-finite rotation key");
204 continue;
205 }
206 let len = q.iter().map(|v| v * v).sum::<f32>().sqrt();
207 if len <= f32::EPSILON {
208 skipped = Some("zero-length rotation key");
209 continue;
210 }
211 if (len - 1.0).abs() > QUAT_NORM_TOLERANCE {
212 write_rotation(buffer, layout, value_element, q.map(|v| v / len));
213 fixed += 1;
214 }
215 }
216 (fixed, skipped)
217 })
218 }
219
220 fn apply_quat_flip(&mut self) -> FixReport {
221 self.repair_rotation_tracks(|buffer, layout| {
222 let mut prev: Option<[f32; 4]> = None;
223 let mut flipped = 0usize;
224 for k in 0..layout.keys {
225 let value_element = k * layout.per_key + layout.value_offset;
226 let q = read_rotation(buffer, layout, value_element);
227 if let Some(p) = prev {
228 let dot: f32 = p.iter().zip(&q).map(|(a, b)| a * b).sum();
229 if dot < 0.0 {
230 for e in (k * layout.per_key)..(k * layout.per_key + layout.per_key) {
231 let negated = read_rotation(buffer, layout, e).map(|v| -v);
232 write_rotation(buffer, layout, e, negated);
233 }
234 flipped += 1;
235 prev = Some(q.map(|v| -v));
236 continue;
237 }
238 }
239 prev = Some(q);
240 }
241 (flipped, None)
242 })
243 }
244
245 fn repair_rotation_tracks(
246 &mut self,
247 mut repair: impl FnMut(&mut [u8], RotationLayout) -> (usize, Option<&'static str>),
248 ) -> FixReport {
249 let mut report = FixReport::default();
250 let gltf = &self.gltf;
251 let buffers = &mut self.buffers;
252 for animation in gltf.animations() {
253 let clip = animation.name().unwrap_or("<unnamed>").to_string();
254 for channel in animation.channels() {
255 if channel.target().property() != gltf::animation::Property::Rotation {
256 continue;
257 }
258 let bone = channel
259 .target()
260 .node()
261 .name()
262 .unwrap_or("<unnamed>")
263 .to_string();
264 let sampler = channel.sampler();
265 let cubic = sampler.interpolation() == gltf::animation::Interpolation::CubicSpline;
266 let accessor = sampler.output();
267 if accessor.sparse().is_some() {
268 report
269 .skipped
270 .push(format!("{clip}/{bone}: sparse accessor"));
271 continue;
272 }
273 if accessor.data_type() != gltf::accessor::DataType::F32
274 || accessor.dimensions() != gltf::accessor::Dimensions::Vec4
275 {
276 report.skipped.push(format!(
277 "{clip}/{bone}: quantized rotation output ({:?})",
278 accessor.data_type()
279 ));
280 continue;
281 }
282 let Some(view) = accessor.view() else {
283 report
284 .skipped
285 .push(format!("{clip}/{bone}: accessor without view"));
286 continue;
287 };
288 let buffer_index = view.buffer().index();
289 if matches!(
290 view.buffer().source(),
291 gltf::buffer::Source::Uri(uri) if uri.starts_with("data:")
292 ) {
293 report.skipped.push(format!(
294 "{clip}/{bone}: data-URI buffer (convert to .glb first)"
295 ));
296 continue;
297 }
298 let stride = view.stride().unwrap_or(16);
299 let Some(start) = view.offset().checked_add(accessor.offset()) else {
300 report
301 .skipped
302 .push(format!("{clip}/{bone}: accessor byte offset overflow"));
303 continue;
304 };
305 let Some(buffer) = buffers.get_mut(buffer_index) else {
306 report
307 .skipped
308 .push(format!("{clip}/{bone}: missing buffer {buffer_index}"));
309 continue;
310 };
311
312 let (per_key, value_offset) = if cubic { (3usize, 1usize) } else { (1, 0) };
316 if accessor.count() % per_key != 0 {
317 report
318 .skipped
319 .push(format!("{clip}/{bone}: malformed cubic rotation accessor"));
320 continue;
321 }
322 let Some(range) =
323 accessor_byte_range(start, stride, accessor.count(), ROTATION_ELEMENT_BYTES)
324 else {
325 report
326 .skipped
327 .push(format!("{clip}/{bone}: accessor byte range overflow"));
328 continue;
329 };
330 if range.end > buffer.len() {
331 report.skipped.push(format!(
332 "{clip}/{bone}: accessor byte range {}..{} outside buffer length {}",
333 range.start,
334 range.end,
335 buffer.len()
336 ));
337 continue;
338 }
339 let layout = RotationLayout {
340 start,
341 stride,
342 keys: accessor.count() / per_key,
343 per_key,
344 value_offset,
345 cubic,
346 };
347 let (fixed_keys, skipped) = repair(buffer, layout);
348 if let Some(reason) = skipped {
349 report.skipped.push(format!("{clip}/{bone}: {reason}"));
350 }
351 if fixed_keys > 0 {
352 report.tracks.push(TrackFix {
353 clip: clip.clone(),
354 bone,
355 fixed_keys,
356 });
357 }
358 }
359 }
360 report
361 }
362
363 pub fn write(&self, input: &Path, output: &Path) -> Result<(), FixError> {
371 write_patched(input, output, &self.original, &self.gltf, &self.buffers)
372 }
373}
374
375#[derive(Clone, Copy)]
376struct RotationLayout {
377 start: usize,
378 stride: usize,
379 keys: usize,
380 per_key: usize,
381 value_offset: usize,
382 cubic: bool,
383}
384
385fn read_rotation(buffer: &[u8], layout: RotationLayout, element: usize) -> [f32; 4] {
386 let at = layout.start + element * layout.stride;
387 let mut q = [0f32; 4];
388 for (c, slot) in q.iter_mut().enumerate() {
389 let o = at + c * 4;
390 *slot = f32::from_le_bytes(buffer[o..o + 4].try_into().expect("slice has four bytes"));
391 }
392 q
393}
394
395fn write_rotation(buffer: &mut [u8], layout: RotationLayout, element: usize, q: [f32; 4]) {
396 let at = layout.start + element * layout.stride;
397 for (c, v) in q.iter().enumerate() {
398 let o = at + c * 4;
399 buffer[o..o + 4].copy_from_slice(&v.to_le_bytes());
400 }
401}
402
403fn write_patched(
406 input: &Path,
407 output: &Path,
408 original: &[u8],
409 gltf: &gltf::Gltf,
410 buffers: &[Vec<u8>],
411) -> Result<(), FixError> {
412 let io_err = |path: &Path| {
413 let path = path.display().to_string();
414 move |source: std::io::Error| {
415 FixError::Write(WriteError::Io {
416 path: path.clone(),
417 source,
418 })
419 }
420 };
421
422 if original.starts_with(b"glTF") {
423 let json_len = read_u32_le(original, 12)?;
426 let bin_chunk_start = 12usize
427 .checked_add(8)
428 .and_then(|n| n.checked_add(json_len))
429 .ok_or_else(|| LoadError::Buffer("malformed GLB chunk length overflow".into()))?;
430 if bin_chunk_start > original.len() {
431 return Err(LoadError::Buffer("malformed GLB JSON chunk length".into()).into());
432 }
433 let mut out = original[..bin_chunk_start].to_vec();
434 if bin_chunk_start < original.len() {
435 let bin_len = read_u32_le(original, bin_chunk_start)?;
436 let bin_header_end = bin_chunk_start
437 .checked_add(8)
438 .ok_or_else(|| LoadError::Buffer("malformed GLB BIN chunk overflow".into()))?;
439 if bin_header_end > original.len() {
440 return Err(LoadError::Buffer("malformed GLB BIN chunk header".into()).into());
441 }
442 out.extend_from_slice(&original[bin_chunk_start..bin_header_end]);
443 let bin = gltf
447 .buffers()
448 .position(|b| matches!(b.source(), gltf::buffer::Source::Bin))
449 .and_then(|i| buffers.get(i))
450 .map(Vec::as_slice)
451 .unwrap_or(&[]);
452 if bin.len() > bin_len {
453 return Err(LoadError::Buffer(format!(
454 "patched BIN chunk length {} exceeds original length {bin_len}",
455 bin.len()
456 ))
457 .into());
458 }
459 out.extend_from_slice(bin);
460 let padding_start = bin_header_end
462 .checked_add(bin.len())
463 .ok_or_else(|| LoadError::Buffer("malformed GLB BIN chunk overflow".into()))?;
464 if padding_start > original.len() {
465 return Err(LoadError::Buffer("malformed GLB BIN chunk length".into()).into());
466 }
467 out.extend_from_slice(&original[padding_start..]);
468 }
469 std::fs::write(output, out).map_err(io_err(output))?;
470 return write_uri_buffers(output, gltf, buffers);
474 }
475
476 if input != output {
481 std::fs::copy(input, output).map_err(io_err(output))?;
482 }
483 write_uri_buffers(output, gltf, buffers)
484}
485
486fn write_uri_buffers(
489 output: &Path,
490 gltf: &gltf::Gltf,
491 buffers: &[Vec<u8>],
492) -> Result<(), FixError> {
493 for (buffer, data) in gltf.buffers().zip(buffers) {
494 if let gltf::buffer::Source::Uri(uri) = buffer.source() {
495 if uri.starts_with("data:") {
496 continue; }
498 let path = output
499 .parent()
500 .unwrap_or(Path::new("."))
501 .join(safe_external_buffer_path(uri)?);
502 std::fs::write(&path, data).map_err(|source| {
503 FixError::Write(WriteError::Io {
504 path: path.display().to_string(),
505 source,
506 })
507 })?;
508 }
509 }
510 Ok(())
511}
512
513fn accessor_byte_range(
514 start: usize,
515 stride: usize,
516 element_count: usize,
517 element_bytes: usize,
518) -> Option<Range<usize>> {
519 if stride < element_bytes {
520 return None;
521 }
522 if element_count == 0 {
523 return Some(start..start);
524 }
525 let last = element_count.checked_sub(1)?;
526 let last_start = start.checked_add(last.checked_mul(stride)?)?;
527 Some(start..last_start.checked_add(element_bytes)?)
528}
529
530fn read_u32_le(bytes: &[u8], offset: usize) -> Result<usize, LoadError> {
531 let end = offset
532 .checked_add(4)
533 .ok_or_else(|| LoadError::Buffer("malformed GLB offset overflow".into()))?;
534 let word = bytes
535 .get(offset..end)
536 .ok_or_else(|| LoadError::Buffer("malformed GLB chunk header".into()))?;
537 Ok(u32::from_le_bytes(word.try_into().expect("slice has four bytes")) as usize)
538}