Skip to main content

oxiphysics_io/gltf/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6use super::functions::escape_json;
7#[allow(unused_imports)]
8use super::functions::*;
9
10/// Light type for the `KHR_lights_punctual` extension.
11#[derive(Debug, Clone, PartialEq)]
12pub enum LightType {
13    /// Omnidirectional point light.
14    Point,
15    /// Directional parallel light.
16    Directional,
17    /// Spot light with cone angles.
18    Spot {
19        /// Inner cone half-angle (radians).
20        inner_cone_angle: f32,
21        /// Outer cone half-angle (radians).
22        outer_cone_angle: f32,
23    },
24}
25/// Camera projection type.
26#[derive(Debug, Clone)]
27pub enum CameraType {
28    /// Perspective projection.
29    Perspective {
30        /// Vertical field of view in radians.
31        yfov: f32,
32        /// Near clip plane.
33        znear: f32,
34        /// Far clip plane (None = infinite).
35        zfar: Option<f32>,
36        /// Aspect ratio (width / height). None = use viewport.
37        aspect_ratio: Option<f32>,
38    },
39    /// Orthographic projection.
40    Orthographic {
41        /// Half-width of the view volume.
42        xmag: f32,
43        /// Half-height of the view volume.
44        ymag: f32,
45        /// Near clip plane.
46        znear: f32,
47        /// Far clip plane.
48        zfar: f32,
49    },
50}
51/// A validation issue found in a glTF scene.
52#[derive(Debug, Clone)]
53pub struct ValidationIssue {
54    /// Human-readable description of the issue.
55    pub message: String,
56    /// True for errors (must fix), false for warnings (should fix).
57    pub is_error: bool,
58}
59impl ValidationIssue {
60    pub(crate) fn error(msg: impl Into<String>) -> Self {
61        ValidationIssue {
62            message: msg.into(),
63            is_error: true,
64        }
65    }
66    pub(crate) fn warning(msg: impl Into<String>) -> Self {
67        ValidationIssue {
68            message: msg.into(),
69            is_error: false,
70        }
71    }
72}
73/// A single animation channel.
74#[derive(Debug, Clone)]
75pub struct GltfAnimationChannel {
76    /// The channel target.
77    pub target: GltfAnimationTarget,
78    /// Interpolation method: "LINEAR", "STEP", "CUBICSPLINE".
79    pub interpolation: String,
80    /// Input (time) keyframe values.
81    pub input_times: Vec<f32>,
82    /// Output keyframe values (flattened; e.g., 3 floats per VEC3 keyframe).
83    pub output_values: Vec<f32>,
84}
85/// A single keyframe channel for a joint.
86#[derive(Debug, Clone)]
87pub struct JointChannel {
88    /// Index into the skeleton's joints array.
89    pub joint_index: usize,
90    /// Target path: "translation", "rotation", or "scale".
91    pub path: String,
92    /// Input (time) values.
93    pub times: Vec<f32>,
94    /// Output values (flattened; e.g., 3 floats/VEC3 for translation).
95    pub values: Vec<f32>,
96    /// Interpolation: "LINEAR", "STEP", or "CUBICSPLINE".
97    pub interpolation: String,
98}
99/// Material properties for a glTF primitive.
100#[derive(Debug, Clone)]
101pub struct GltfMaterial {
102    /// Material name.
103    pub name: String,
104    /// Base color factor \[r, g, b, a\] (PBR metallic-roughness).
105    pub base_color_factor: [f32; 4],
106    /// Metallic factor (0.0 = dielectric, 1.0 = metal).
107    pub metallic_factor: f32,
108    /// Roughness factor (0.0 = smooth, 1.0 = rough).
109    pub roughness_factor: f32,
110    /// Emissive factor \[r, g, b\].
111    pub emissive_factor: [f32; 3],
112    /// Alpha mode: "OPAQUE", "MASK", or "BLEND".
113    pub alpha_mode: String,
114    /// Alpha cutoff (only used when alpha_mode is "MASK").
115    pub alpha_cutoff: f32,
116    /// Whether the material is double-sided.
117    pub double_sided: bool,
118}
119impl GltfMaterial {
120    /// Check if the material is fully opaque (alpha = 1.0 and OPAQUE mode).
121    pub fn is_opaque(&self) -> bool {
122        self.alpha_mode == "OPAQUE" && (self.base_color_factor[3] - 1.0).abs() < 1e-6
123    }
124    /// Check if the material is emissive (any emissive factor > 0).
125    pub fn is_emissive(&self) -> bool {
126        self.emissive_factor.iter().any(|&v| v > 0.0)
127    }
128}
129/// A joint (bone) in a skeleton hierarchy.
130#[derive(Debug, Clone)]
131pub struct Joint {
132    /// Human-readable name.
133    pub name: String,
134    /// Translation \[x, y, z\].
135    pub translation: [f64; 3],
136    /// Rotation quaternion \[x, y, z, w\].
137    pub rotation: [f64; 4],
138    /// Scale \[x, y, z\].
139    pub scale: [f64; 3],
140    /// Child joint indices.
141    pub children: Vec<usize>,
142    /// Inverse bind matrix stored in column-major order (16 floats).
143    pub inverse_bind_matrix: [f32; 16],
144}
145/// A light source (KHR_lights_punctual extension).
146#[derive(Debug, Clone)]
147pub struct SceneLight {
148    /// Light name.
149    pub name: String,
150    /// Light type.
151    pub light_type: LightType,
152    /// Linear RGB color.
153    pub color: [f32; 3],
154    /// Intensity (candela for point/spot, lux for directional).
155    pub intensity: f32,
156}
157impl SceneLight {
158    /// Return the light type string as used in glTF JSON.
159    pub fn type_string(&self) -> &str {
160        match &self.light_type {
161            LightType::Point => "point",
162            LightType::Directional => "directional",
163            LightType::Spot { .. } => "spot",
164        }
165    }
166}
167/// A single draw primitive: positions, normals, and indices.
168pub struct GltfPrimitive {
169    /// Per-vertex positions.
170    pub positions: Vec<[f32; 3]>,
171    /// Per-vertex normals.
172    pub normals: Vec<[f32; 3]>,
173    /// Triangle indices.
174    pub indices: Vec<u32>,
175}
176impl GltfPrimitive {
177    /// Compute the axis-aligned bounding box of positions.
178    /// Returns `(min, max)` as `[f32; 3]` arrays.
179    pub fn bounding_box(&self) -> ([f32; 3], [f32; 3]) {
180        let mut min = [f32::INFINITY; 3];
181        let mut max = [f32::NEG_INFINITY; 3];
182        for p in &self.positions {
183            for i in 0..3 {
184                if p[i] < min[i] {
185                    min[i] = p[i];
186                }
187                if p[i] > max[i] {
188                    max[i] = p[i];
189                }
190            }
191        }
192        (min, max)
193    }
194    /// Return the number of triangles (indices / 3).
195    pub fn triangle_count(&self) -> usize {
196        self.indices.len() / 3
197    }
198    /// Return the number of vertices.
199    pub fn vertex_count(&self) -> usize {
200        self.positions.len()
201    }
202    /// Extract triangle vertex positions as triplets.
203    pub fn extract_triangles(&self) -> Vec<[[f32; 3]; 3]> {
204        let mut tris = Vec::new();
205        let mut i = 0;
206        while i + 2 < self.indices.len() {
207            let a = self.indices[i] as usize;
208            let b = self.indices[i + 1] as usize;
209            let c = self.indices[i + 2] as usize;
210            if a < self.positions.len() && b < self.positions.len() && c < self.positions.len() {
211                tris.push([self.positions[a], self.positions[b], self.positions[c]]);
212            }
213            i += 3;
214        }
215        tris
216    }
217}
218/// A named mesh containing one or more primitives.
219pub struct GltfMesh {
220    /// Human-readable mesh name.
221    pub name: String,
222    /// List of draw primitives.
223    pub primitives: Vec<GltfPrimitive>,
224}
225impl GltfMesh {
226    /// Total vertex count across all primitives.
227    pub fn total_vertex_count(&self) -> usize {
228        self.primitives.iter().map(|p| p.vertex_count()).sum()
229    }
230    /// Total triangle count across all primitives.
231    pub fn total_triangle_count(&self) -> usize {
232        self.primitives.iter().map(|p| p.triangle_count()).sum()
233    }
234}
235/// Element type (SCALAR, VEC2, VEC3, VEC4, MAT4, …).
236#[allow(dead_code)]
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum AccessorType {
239    /// Single scalar value.
240    Scalar,
241    /// 2-component vector.
242    Vec2,
243    /// 3-component vector.
244    Vec3,
245    /// 4-component vector.
246    Vec4,
247    /// 2×2 column-major matrix.
248    Mat2,
249    /// 3×3 column-major matrix.
250    Mat3,
251    /// 4×4 column-major matrix.
252    Mat4,
253}
254impl AccessorType {
255    /// Number of components per element.
256    pub fn num_components(self) -> usize {
257        match self {
258            AccessorType::Scalar => 1,
259            AccessorType::Vec2 => 2,
260            AccessorType::Vec3 => 3,
261            AccessorType::Vec4 => 4,
262            AccessorType::Mat2 => 4,
263            AccessorType::Mat3 => 9,
264            AccessorType::Mat4 => 16,
265        }
266    }
267    /// glTF JSON string representation.
268    pub fn as_str(self) -> &'static str {
269        match self {
270            AccessorType::Scalar => "SCALAR",
271            AccessorType::Vec2 => "VEC2",
272            AccessorType::Vec3 => "VEC3",
273            AccessorType::Vec4 => "VEC4",
274            AccessorType::Mat2 => "MAT2",
275            AccessorType::Mat3 => "MAT3",
276            AccessorType::Mat4 => "MAT4",
277        }
278    }
279}
280/// A skeleton animation: name + per-joint channels.
281#[derive(Debug, Clone)]
282pub struct SkeletonAnimation {
283    /// Animation name.
284    pub name: String,
285    /// Per-joint keyframe channels.
286    pub joint_channels: Vec<JointChannel>,
287}
288impl SkeletonAnimation {
289    /// Duration of the animation (max time across all channels).
290    pub fn duration(&self) -> f32 {
291        self.joint_channels
292            .iter()
293            .filter_map(|c| c.times.last().copied())
294            .fold(0.0f32, f32::max)
295    }
296    /// Total keyframe count across all channels.
297    pub fn total_keyframes(&self) -> usize {
298        self.joint_channels.iter().map(|c| c.times.len()).sum()
299    }
300}
301/// A buffer view defines a slice of a binary buffer.
302#[derive(Debug, Clone)]
303pub struct GltfBufferView {
304    /// Index into the buffers array.
305    pub buffer: usize,
306    /// Byte offset into the buffer.
307    pub byte_offset: usize,
308    /// Length in bytes.
309    pub byte_length: usize,
310    /// Optional byte stride (for interleaved data).
311    pub byte_stride: Option<usize>,
312}
313/// A typed array view over a raw byte buffer.
314///
315/// Mirrors the glTF `accessor` concept — a window into a `bufferView` with
316/// a specific element type and component type.
317#[allow(dead_code)]
318pub struct TypedAccessor {
319    /// Human-readable label.
320    pub name: String,
321    /// Index of the owning buffer view.
322    pub buffer_view_index: usize,
323    /// Byte offset within the buffer view.
324    pub byte_offset: usize,
325    /// Component type (FLOAT, UNSIGNED_INT, …).
326    pub component_type: ComponentType,
327    /// Element type (SCALAR, VEC3, …).
328    pub accessor_type: AccessorType,
329    /// Number of elements (not bytes, not components).
330    pub count: usize,
331    /// Optional min values per component (length = num_components).
332    pub min_values: Vec<f64>,
333    /// Optional max values per component.
334    pub max_values: Vec<f64>,
335}
336impl TypedAccessor {
337    /// Create a new accessor.
338    #[allow(clippy::too_many_arguments)]
339    pub fn new(
340        name: impl Into<String>,
341        buffer_view_index: usize,
342        byte_offset: usize,
343        component_type: ComponentType,
344        accessor_type: AccessorType,
345        count: usize,
346    ) -> Self {
347        Self {
348            name: name.into(),
349            buffer_view_index,
350            byte_offset,
351            component_type,
352            accessor_type,
353            count,
354            min_values: Vec::new(),
355            max_values: Vec::new(),
356        }
357    }
358    /// Total byte length of this accessor's data.
359    pub fn byte_length(&self) -> usize {
360        self.count * self.accessor_type.num_components() * self.component_type.byte_size()
361    }
362    /// Serialize to a glTF JSON object fragment (returns a JSON object string).
363    pub fn to_json(&self) -> String {
364        format!(
365            r#"{{ "bufferView": {bv}, "byteOffset": {bo}, "componentType": {ct}, "type": "{at}", "count": {c} }}"#,
366            bv = self.buffer_view_index,
367            bo = self.byte_offset,
368            ct = self.component_type.component_type_code(),
369            at = self.accessor_type.as_str(),
370            c = self.count,
371        )
372    }
373    /// Decode this accessor's data as Vec`f32` from a raw byte buffer.
374    ///
375    /// Only valid when `component_type == Float`.
376    pub fn decode_f32(&self, buffer: &[u8]) -> Option<Vec<f32>> {
377        if self.component_type != ComponentType::Float {
378            return None;
379        }
380        let start = self.byte_offset;
381        let len = self.byte_length();
382        let end = start + len;
383        if end > buffer.len() {
384            return None;
385        }
386        let slice = &buffer[start..end];
387        let floats: Vec<f32> = slice
388            .chunks_exact(4)
389            .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
390            .collect();
391        Some(floats)
392    }
393    /// Decode as Vec`u32` indices (only valid for UNSIGNED_INT accessors).
394    pub fn decode_u32(&self, buffer: &[u8]) -> Option<Vec<u32>> {
395        if self.component_type != ComponentType::UnsignedInt {
396            return None;
397        }
398        let start = self.byte_offset;
399        let len = self.byte_length();
400        let end = start + len;
401        if end > buffer.len() {
402            return None;
403        }
404        let slice = &buffer[start..end];
405        let indices: Vec<u32> = slice
406            .chunks_exact(4)
407            .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
408            .collect();
409        Some(indices)
410    }
411}
412/// An animation channel target.
413#[derive(Debug, Clone)]
414pub struct GltfAnimationTarget {
415    /// Index of the target node.
416    pub node: usize,
417    /// Path: "translation", "rotation", "scale", or "weights".
418    pub path: String,
419}
420/// A node in a glTF scene graph.
421pub struct GltfNode {
422    /// Human-readable name of the node.
423    pub name: String,
424    /// Optional index into the meshes array.
425    pub mesh: Option<usize>,
426    /// Translation vector \[x, y, z\].
427    pub translation: [f64; 3],
428    /// Rotation quaternion \[x, y, z, w\].
429    pub rotation: [f64; 4],
430    /// Scale vector \[x, y, z\].
431    pub scale: [f64; 3],
432    /// Child node indices for scene graph traversal.
433    pub children: Vec<usize>,
434}
435/// Interpolation mode for animation channels.
436#[allow(dead_code)]
437#[derive(Debug, Clone, PartialEq, Eq)]
438pub enum Interpolation {
439    /// Linear interpolation between keyframes.
440    Linear,
441    /// Hold value until next keyframe.
442    Step,
443    /// Cubic spline (requires tangents).
444    CubicSpline,
445}
446impl Interpolation {
447    /// glTF JSON string.
448    pub fn as_str(&self) -> &'static str {
449        match self {
450            Interpolation::Linear => "LINEAR",
451            Interpolation::Step => "STEP",
452            Interpolation::CubicSpline => "CUBICSPLINE",
453        }
454    }
455}
456/// A skeleton: a collection of joints.
457pub struct Skeleton {
458    /// List of joints.
459    pub joints: Vec<Joint>,
460}
461impl Skeleton {
462    /// Create an empty skeleton.
463    pub fn new() -> Self {
464        Skeleton { joints: Vec::new() }
465    }
466    /// Add a joint and return its index.
467    pub fn add_joint(&mut self, joint: Joint) -> usize {
468        let idx = self.joints.len();
469        self.joints.push(joint);
470        idx
471    }
472    /// Return the number of joints.
473    pub fn joint_count(&self) -> usize {
474        self.joints.len()
475    }
476    /// Export a `SkeletonAnimation` as a simplified glTF animation JSON object.
477    ///
478    /// Produces a JSON string suitable for embedding in a `"animations"` array.
479    pub fn export_animation_json(&self, anim: &SkeletonAnimation) -> String {
480        let mut out = String::new();
481        out.push_str("{\n");
482        out.push_str(&format!("  \"name\": \"{}\",\n", escape_json(&anim.name)));
483        out.push_str("  \"channels\": [\n");
484        for (ci, ch) in anim.joint_channels.iter().enumerate() {
485            let joint_name = self
486                .joints
487                .get(ch.joint_index)
488                .map(|j| j.name.as_str())
489                .unwrap_or("unknown");
490            out.push_str("    {\n");
491            out.push_str(&format!(
492                "      \"joint\": \"{}\",\n",
493                escape_json(joint_name)
494            ));
495            out.push_str(&format!("      \"path\": \"{}\",\n", escape_json(&ch.path)));
496            out.push_str(&format!(
497                "      \"interpolation\": \"{}\",\n",
498                escape_json(&ch.interpolation)
499            ));
500            let times_str: Vec<String> = ch.times.iter().map(|t| format!("{t}")).collect();
501            out.push_str(&format!("      \"times\": [{}]\n", times_str.join(", ")));
502            if ci + 1 < anim.joint_channels.len() {
503                out.push_str("    },\n");
504            } else {
505                out.push_str("    }\n");
506            }
507        }
508        out.push_str("  ]\n");
509        out.push('}');
510        out
511    }
512}
513/// A camera in the scene.
514#[derive(Debug, Clone)]
515pub struct SceneCamera {
516    /// Camera name.
517    pub name: String,
518    /// Projection type.
519    pub camera_type: CameraType,
520}
521/// Component type codes used in glTF accessors.
522#[allow(dead_code)]
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524pub enum ComponentType {
525    /// Unsigned byte (1 byte).
526    UnsignedByte = 5121,
527    /// Unsigned short (2 bytes).
528    UnsignedShort = 5123,
529    /// Unsigned int (4 bytes).
530    UnsignedInt = 5125,
531    /// Float (4 bytes).
532    Float = 5126,
533}
534impl ComponentType {
535    /// Byte size of one component.
536    pub fn byte_size(self) -> usize {
537        match self {
538            ComponentType::UnsignedByte => 1,
539            ComponentType::UnsignedShort => 2,
540            ComponentType::UnsignedInt => 4,
541            ComponentType::Float => 4,
542        }
543    }
544    /// glTF JSON numeric value.
545    pub fn component_type_code(self) -> u32 {
546        self as u32
547    }
548}
549/// Builder for a PBR metallic-roughness material description.
550///
551/// Generates a glTF-compatible JSON material object fragment.
552#[derive(Debug, Clone)]
553pub struct PbrMaterialBuilder {
554    /// Material name.
555    pub name: String,
556    /// Base colour factor \[R, G, B, A\] in linear sRGB.
557    pub base_color_factor: [f32; 4],
558    /// Metallic factor in \[0.0, 1.0\].
559    pub metallic_factor: f32,
560    /// Roughness factor in \[0.0, 1.0\].
561    pub roughness_factor: f32,
562    /// Emissive colour factor \[R, G, B\].
563    pub emissive_factor: [f32; 3],
564    /// Double-sided rendering.
565    pub double_sided: bool,
566    /// Alpha mode: "OPAQUE", "MASK", or "BLEND".
567    pub alpha_mode: String,
568    /// Alpha cutoff (for MASK mode).
569    pub alpha_cutoff: f32,
570}
571impl PbrMaterialBuilder {
572    /// Create a new builder with default values.
573    pub fn new(name: impl Into<String>) -> Self {
574        Self {
575            name: name.into(),
576            ..Default::default()
577        }
578    }
579    /// Set the base colour factor.
580    pub fn base_color(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
581        self.base_color_factor = [r, g, b, a];
582        self
583    }
584    /// Set metallic and roughness factors.
585    pub fn metallic_roughness(mut self, metallic: f32, roughness: f32) -> Self {
586        self.metallic_factor = metallic;
587        self.roughness_factor = roughness;
588        self
589    }
590    /// Set emissive factor.
591    pub fn emissive(mut self, r: f32, g: f32, b: f32) -> Self {
592        self.emissive_factor = [r, g, b];
593        self
594    }
595    /// Enable double-sided rendering.
596    pub fn double_sided(mut self, ds: bool) -> Self {
597        self.double_sided = ds;
598        self
599    }
600    /// Set alpha mode.
601    pub fn alpha_mode(mut self, mode: impl Into<String>) -> Self {
602        self.alpha_mode = mode.into();
603        self
604    }
605    /// Build as a JSON string (glTF material object).
606    pub fn to_json(&self) -> String {
607        let bc = &self.base_color_factor;
608        let em = &self.emissive_factor;
609        format!(
610            r#"{{
611  "name": "{name}",
612  "pbrMetallicRoughness": {{
613    "baseColorFactor": [{r:.6}, {g:.6}, {b:.6}, {a:.6}],
614    "metallicFactor": {mf:.6},
615    "roughnessFactor": {rf:.6}
616  }},
617  "emissiveFactor": [{er:.6}, {eg:.6}, {eb:.6}],
618  "doubleSided": {ds},
619  "alphaMode": "{am}",
620  "alphaCutoff": {ac:.6}
621}}"#,
622            name = self.name,
623            r = bc[0],
624            g = bc[1],
625            b = bc[2],
626            a = bc[3],
627            mf = self.metallic_factor,
628            rf = self.roughness_factor,
629            er = em[0],
630            eg = em[1],
631            eb = em[2],
632            ds = self.double_sided,
633            am = self.alpha_mode,
634            ac = self.alpha_cutoff,
635        )
636    }
637    /// Convert to a [`GltfMaterial`] instance (for use in a [`GltfScene`]).
638    pub fn build(&self) -> GltfMaterial {
639        GltfMaterial {
640            name: self.name.clone(),
641            base_color_factor: self.base_color_factor,
642            metallic_factor: self.metallic_factor,
643            roughness_factor: self.roughness_factor,
644            emissive_factor: self.emissive_factor,
645            double_sided: self.double_sided,
646            alpha_mode: self.alpha_mode.clone(),
647            alpha_cutoff: self.alpha_cutoff,
648        }
649    }
650}
651/// Builder for a single animation channel (translation / rotation / scale).
652#[allow(dead_code)]
653pub struct AnimationChannelBuilder {
654    /// Target node index.
655    pub node_index: usize,
656    /// Target path: "translation", "rotation", or "scale".
657    pub path: String,
658    /// Interpolation mode.
659    pub interpolation: Interpolation,
660    /// Ordered keyframes (should be sorted by time).
661    pub keyframes: Vec<Keyframe>,
662}
663impl AnimationChannelBuilder {
664    /// Create a new channel builder.
665    pub fn new(node_index: usize, path: impl Into<String>, interpolation: Interpolation) -> Self {
666        Self {
667            node_index,
668            path: path.into(),
669            interpolation,
670            keyframes: Vec::new(),
671        }
672    }
673    /// Append a keyframe.
674    pub fn push(mut self, time: f32, value: Vec<f32>) -> Self {
675        self.keyframes.push(Keyframe::new(time, value));
676        self
677    }
678    /// Duration of the channel (time of the last keyframe, or 0 if empty).
679    pub fn duration(&self) -> f32 {
680        self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
681    }
682    /// Number of keyframes.
683    pub fn len(&self) -> usize {
684        self.keyframes.len()
685    }
686    /// True if there are no keyframes.
687    pub fn is_empty(&self) -> bool {
688        self.keyframes.is_empty()
689    }
690    /// Collect all time stamps.
691    pub fn times(&self) -> Vec<f32> {
692        self.keyframes.iter().map(|k| k.time).collect()
693    }
694    /// Collect all value payloads flattened into one Vec.
695    pub fn values_flat(&self) -> Vec<f32> {
696        self.keyframes
697            .iter()
698            .flat_map(|k| k.value.iter().copied())
699            .collect()
700    }
701    /// Serialize sampler + channel pair to JSON fragments.
702    pub fn to_json_fragments(
703        &self,
704        sampler_index: usize,
705        times_accessor: usize,
706        values_accessor: usize,
707    ) -> (String, String) {
708        let sampler = format!(
709            r#"{{ "input": {ti}, "interpolation": "{interp}", "output": {vi} }}"#,
710            ti = times_accessor,
711            interp = self.interpolation.as_str(),
712            vi = values_accessor,
713        );
714        let channel = format!(
715            r#"{{ "sampler": {si}, "target": {{ "node": {ni}, "path": "{path}" }} }}"#,
716            si = sampler_index,
717            ni = self.node_index,
718            path = self.path,
719        );
720        (sampler, channel)
721    }
722}
723/// A named animation containing multiple channels.
724#[derive(Debug, Clone)]
725pub struct GltfAnimation {
726    /// Animation name.
727    pub name: String,
728    /// Animation channels.
729    pub channels: Vec<GltfAnimationChannel>,
730}
731impl GltfAnimation {
732    /// Total duration of the animation (max input time across all channels).
733    pub fn duration(&self) -> f32 {
734        self.channels
735            .iter()
736            .filter_map(|ch| ch.input_times.last().copied())
737            .fold(0.0f32, f32::max)
738    }
739    /// Total number of keyframes across all channels.
740    pub fn total_keyframe_count(&self) -> usize {
741        self.channels.iter().map(|ch| ch.input_times.len()).sum()
742    }
743}
744/// A single keyframe with a time stamp and a value payload.
745#[allow(dead_code)]
746#[derive(Debug, Clone)]
747pub struct Keyframe {
748    /// Time in seconds.
749    pub time: f32,
750    /// Value payload (length depends on the target path).
751    pub value: Vec<f32>,
752}
753impl Keyframe {
754    /// Create a new keyframe.
755    pub fn new(time: f32, value: Vec<f32>) -> Self {
756        Self { time, value }
757    }
758}
759/// An accessor describes how to read typed data from a buffer view.
760#[derive(Debug, Clone)]
761pub struct GltfAccessor {
762    /// Index into the buffer views array.
763    pub buffer_view: usize,
764    /// Byte offset within the buffer view.
765    pub byte_offset: usize,
766    /// Component type (5126 = FLOAT, 5123 = UNSIGNED_SHORT, 5125 = UNSIGNED_INT).
767    pub component_type: u32,
768    /// Number of elements.
769    pub count: usize,
770    /// Type string ("SCALAR", "VEC2", "VEC3", "VEC4", "MAT4").
771    pub element_type: String,
772}
773impl GltfAccessor {
774    /// Number of components per element.
775    pub fn components_per_element(&self) -> usize {
776        match self.element_type.as_str() {
777            "SCALAR" => 1,
778            "VEC2" => 2,
779            "VEC3" => 3,
780            "VEC4" => 4,
781            "MAT2" => 4,
782            "MAT3" => 9,
783            "MAT4" => 16,
784            _ => 1,
785        }
786    }
787    /// Size of a single component in bytes.
788    pub fn component_size(&self) -> usize {
789        match self.component_type {
790            5120 => 1,
791            5121 => 1,
792            5122 => 2,
793            5123 => 2,
794            5125 => 4,
795            5126 => 4,
796            _ => 4,
797        }
798    }
799    /// Total byte length of data described by this accessor.
800    pub fn byte_length(&self) -> usize {
801        self.count * self.components_per_element() * self.component_size()
802    }
803}
804/// A morph target (blend shape) that stores position deltas.
805#[derive(Debug, Clone)]
806pub struct MorphTarget {
807    /// Human-readable name of the morph target.
808    pub name: String,
809    /// Per-vertex position deltas applied at weight = 1.0.
810    pub position_deltas: Vec<[f32; 3]>,
811}
812impl MorphTarget {
813    /// Apply this morph target to a set of base positions with the given weight.
814    ///
815    /// Returns a new `Vec` with blended positions.
816    pub fn apply(&self, base_positions: &[[f32; 3]], weight: f32) -> Vec<[f32; 3]> {
817        let n = base_positions.len().min(self.position_deltas.len());
818        let mut out = base_positions.to_vec();
819        for i in 0..n {
820            out[i][0] += self.position_deltas[i][0] * weight;
821            out[i][1] += self.position_deltas[i][1] * weight;
822            out[i][2] += self.position_deltas[i][2] * weight;
823        }
824        out
825    }
826}
827/// A glTF 2.0 scene: a collection of nodes and meshes.
828pub struct GltfScene {
829    /// Scene nodes.
830    pub nodes: Vec<GltfNode>,
831    /// Scene meshes.
832    pub meshes: Vec<GltfMesh>,
833    /// Accessors.
834    pub accessors: Vec<GltfAccessor>,
835    /// Buffer views.
836    pub buffer_views: Vec<GltfBufferView>,
837    /// Animations.
838    pub animations: Vec<GltfAnimation>,
839    /// Materials.
840    pub materials: Vec<GltfMaterial>,
841    /// Cameras.
842    pub cameras: Vec<SceneCamera>,
843    /// Lights (KHR_lights_punctual).
844    pub lights: Vec<SceneLight>,
845}
846impl GltfScene {
847    /// Create an empty scene.
848    pub fn new() -> Self {
849        GltfScene {
850            nodes: Vec::new(),
851            meshes: Vec::new(),
852            accessors: Vec::new(),
853            buffer_views: Vec::new(),
854            animations: Vec::new(),
855            materials: Vec::new(),
856            cameras: Vec::new(),
857            lights: Vec::new(),
858        }
859    }
860    /// Add a camera and return its index.
861    pub fn add_camera(&mut self, cam: SceneCamera) -> usize {
862        let idx = self.cameras.len();
863        self.cameras.push(cam);
864        idx
865    }
866    /// Add a light and return its index.
867    pub fn add_light(&mut self, light: SceneLight) -> usize {
868        let idx = self.lights.len();
869        self.lights.push(light);
870        idx
871    }
872    /// Serialize the scene as a glTF 2.0 JSON string including cameras and lights.
873    ///
874    /// Cameras are written in the `cameras` array.
875    /// Lights use the `KHR_lights_punctual` extension.
876    pub fn to_json_with_hierarchy(&self) -> String {
877        let mut out = self.to_json();
878        if out.ends_with('}') {
879            out.pop();
880        }
881        out.push_str(",\n");
882        if !self.cameras.is_empty() {
883            out.push_str("  \"cameras\": [\n");
884            for (ci, cam) in self.cameras.iter().enumerate() {
885                out.push_str("    {\n");
886                out.push_str(&format!(
887                    "      \"name\": \"{}\",\n",
888                    escape_json(&cam.name)
889                ));
890                match &cam.camera_type {
891                    CameraType::Perspective {
892                        yfov,
893                        znear,
894                        zfar,
895                        aspect_ratio,
896                    } => {
897                        out.push_str("      \"type\": \"perspective\",\n");
898                        out.push_str("      \"perspective\": {\n");
899                        out.push_str(&format!("        \"yfov\": {},\n", yfov));
900                        out.push_str(&format!("        \"znear\": {}", znear));
901                        if let Some(zf) = zfar {
902                            out.push_str(&format!(",\n        \"zfar\": {}", zf));
903                        }
904                        if let Some(ar) = aspect_ratio {
905                            out.push_str(&format!(",\n        \"aspectRatio\": {}", ar));
906                        }
907                        out.push_str("\n      }\n");
908                    }
909                    CameraType::Orthographic {
910                        xmag,
911                        ymag,
912                        znear,
913                        zfar,
914                    } => {
915                        out.push_str("      \"type\": \"orthographic\",\n");
916                        out.push_str("      \"orthographic\": {\n");
917                        out.push_str(&format!("        \"xmag\": {},\n", xmag));
918                        out.push_str(&format!("        \"ymag\": {},\n", ymag));
919                        out.push_str(&format!("        \"znear\": {},\n", znear));
920                        out.push_str(&format!("        \"zfar\": {}\n", zfar));
921                        out.push_str("      }\n");
922                    }
923                }
924                if ci + 1 < self.cameras.len() {
925                    out.push_str("    },\n");
926                } else {
927                    out.push_str("    }\n");
928                }
929            }
930            out.push_str("  ],\n");
931        }
932        if !self.lights.is_empty() {
933            out.push_str("  \"extensions\": {\n");
934            out.push_str("    \"KHR_lights_punctual\": {\n");
935            out.push_str("      \"lights\": [\n");
936            for (li, light) in self.lights.iter().enumerate() {
937                out.push_str("        {\n");
938                out.push_str(&format!(
939                    "          \"name\": \"{}\",\n",
940                    escape_json(&light.name)
941                ));
942                out.push_str(&format!(
943                    "          \"type\": \"{}\",\n",
944                    light.type_string()
945                ));
946                out.push_str(&format!(
947                    "          \"color\": [{}, {}, {}],\n",
948                    light.color[0], light.color[1], light.color[2]
949                ));
950                out.push_str(&format!("          \"intensity\": {}", light.intensity));
951                if let LightType::Spot {
952                    inner_cone_angle,
953                    outer_cone_angle,
954                } = &light.light_type
955                {
956                    out.push_str(
957                        &format!(
958                            ",\n          \"spot\": {{ \"innerConeAngle\": {}, \"outerConeAngle\": {} }}",
959                            inner_cone_angle, outer_cone_angle
960                        ),
961                    );
962                }
963                out.push('\n');
964                if li + 1 < self.lights.len() {
965                    out.push_str("        },\n");
966                } else {
967                    out.push_str("        }\n");
968                }
969            }
970            out.push_str("      ]\n");
971            out.push_str("    }\n");
972            out.push_str("  }\n");
973        } else {
974            if out.ends_with(",\n") {
975                out.truncate(out.len() - 2);
976                out.push('\n');
977            }
978        }
979        out.push('}');
980        out
981    }
982    /// Add a mesh to the scene and return its index.
983    pub fn add_mesh(&mut self, mesh: GltfMesh) -> usize {
984        let idx = self.meshes.len();
985        self.meshes.push(mesh);
986        idx
987    }
988    /// Add a node to the scene.
989    pub fn add_node(&mut self, node: GltfNode) {
990        self.nodes.push(node);
991    }
992    /// Return the number of meshes in the scene.
993    pub fn mesh_count(&self) -> usize {
994        self.meshes.len()
995    }
996    /// Return the number of nodes in the scene.
997    pub fn node_count(&self) -> usize {
998        self.nodes.len()
999    }
1000    /// Traverse the scene graph depth-first, calling `visitor` with
1001    /// `(node_index, depth, accumulated_translation)` for each node.
1002    pub fn traverse_depth_first<F>(&self, mut visitor: F)
1003    where
1004        F: FnMut(usize, usize, [f64; 3]),
1005    {
1006        let mut is_child = vec![false; self.nodes.len()];
1007        for node in &self.nodes {
1008            for &child_idx in &node.children {
1009                if child_idx < is_child.len() {
1010                    is_child[child_idx] = true;
1011                }
1012            }
1013        }
1014        let mut stack: Vec<(usize, usize, [f64; 3])> = Vec::new();
1015        for i in (0..self.nodes.len()).rev() {
1016            if !is_child[i] {
1017                stack.push((i, 0, [0.0, 0.0, 0.0]));
1018            }
1019        }
1020        while let Some((idx, depth, parent_translation)) = stack.pop() {
1021            if idx >= self.nodes.len() {
1022                continue;
1023            }
1024            let node = &self.nodes[idx];
1025            let accumulated = [
1026                parent_translation[0] + node.translation[0],
1027                parent_translation[1] + node.translation[1],
1028                parent_translation[2] + node.translation[2],
1029            ];
1030            visitor(idx, depth, accumulated);
1031            for &child_idx in node.children.iter().rev() {
1032                stack.push((child_idx, depth + 1, accumulated));
1033            }
1034        }
1035    }
1036    /// Collect all mesh primitives in the scene with their associated node names.
1037    pub fn collect_mesh_primitives(&self) -> Vec<(&str, &GltfPrimitive)> {
1038        let mut result = Vec::new();
1039        for node in &self.nodes {
1040            if let Some(mesh_idx) = node.mesh
1041                && let Some(mesh) = self.meshes.get(mesh_idx)
1042            {
1043                for prim in &mesh.primitives {
1044                    result.push((node.name.as_str(), prim));
1045                }
1046            }
1047        }
1048        result
1049    }
1050    /// Add a material and return its index.
1051    pub fn add_material(&mut self, mat: GltfMaterial) -> usize {
1052        let idx = self.materials.len();
1053        self.materials.push(mat);
1054        idx
1055    }
1056    /// Add an animation and return its index.
1057    pub fn add_animation(&mut self, anim: GltfAnimation) -> usize {
1058        let idx = self.animations.len();
1059        self.animations.push(anim);
1060        idx
1061    }
1062    /// Find all nodes that reference a given mesh index.
1063    pub fn nodes_using_mesh(&self, mesh_idx: usize) -> Vec<usize> {
1064        self.nodes
1065            .iter()
1066            .enumerate()
1067            .filter_map(|(i, n)| {
1068                if n.mesh == Some(mesh_idx) {
1069                    Some(i)
1070                } else {
1071                    None
1072                }
1073            })
1074            .collect()
1075    }
1076    /// Return the total vertex count across all meshes.
1077    pub fn total_vertex_count(&self) -> usize {
1078        self.meshes.iter().map(|m| m.total_vertex_count()).sum()
1079    }
1080    /// Return the total triangle count across all meshes.
1081    pub fn total_triangle_count(&self) -> usize {
1082        self.meshes.iter().map(|m| m.total_triangle_count()).sum()
1083    }
1084    /// Serialize the scene as a glTF 2.0 JSON string.
1085    ///
1086    /// Accessor and bufferView entries are written as stubs (no binary buffer).
1087    pub fn to_json(&self) -> String {
1088        let mut out = String::new();
1089        out.push_str("{\n");
1090        out.push_str("  \"asset\": {\n");
1091        out.push_str("    \"version\": \"2.0\",\n");
1092        out.push_str("    \"generator\": \"OxiPhysics glTF writer\"\n");
1093        out.push_str("  },\n");
1094        out.push_str("  \"scene\": 0,\n");
1095        let node_indices: Vec<String> = (0..self.nodes.len()).map(|i| i.to_string()).collect();
1096        out.push_str("  \"scenes\": [\n    {\n      \"nodes\": [");
1097        out.push_str(&node_indices.join(", "));
1098        out.push_str("]\n    }\n  ],\n");
1099        out.push_str("  \"nodes\": [\n");
1100        for (i, node) in self.nodes.iter().enumerate() {
1101            out.push_str("    {\n");
1102            out.push_str(&format!(
1103                "      \"name\": \"{}\",\n",
1104                escape_json(&node.name)
1105            ));
1106            if let Some(mesh_idx) = node.mesh {
1107                out.push_str(&format!("      \"mesh\": {},\n", mesh_idx));
1108            }
1109            if !node.children.is_empty() {
1110                let children_str: Vec<String> =
1111                    node.children.iter().map(|c| c.to_string()).collect();
1112                out.push_str(&format!(
1113                    "      \"children\": [{}],\n",
1114                    children_str.join(", ")
1115                ));
1116            }
1117            out.push_str(&format!(
1118                "      \"translation\": [{}, {}, {}],\n",
1119                node.translation[0], node.translation[1], node.translation[2]
1120            ));
1121            out.push_str(&format!(
1122                "      \"rotation\": [{}, {}, {}, {}],\n",
1123                node.rotation[0], node.rotation[1], node.rotation[2], node.rotation[3]
1124            ));
1125            out.push_str(&format!(
1126                "      \"scale\": [{}, {}, {}]\n",
1127                node.scale[0], node.scale[1], node.scale[2]
1128            ));
1129            if i + 1 < self.nodes.len() {
1130                out.push_str("    },\n");
1131            } else {
1132                out.push_str("    }\n");
1133            }
1134        }
1135        out.push_str("  ],\n");
1136        out.push_str("  \"meshes\": [\n");
1137        for (mi, mesh) in self.meshes.iter().enumerate() {
1138            out.push_str("    {\n");
1139            out.push_str(&format!(
1140                "      \"name\": \"{}\",\n",
1141                escape_json(&mesh.name)
1142            ));
1143            out.push_str("      \"primitives\": [\n");
1144            for (pi, _prim) in mesh.primitives.iter().enumerate() {
1145                let pos_acc = pi * 3;
1146                let nrm_acc = pi * 3 + 1;
1147                let idx_acc = pi * 3 + 2;
1148                out.push_str("        {\n");
1149                out.push_str("          \"attributes\": {\n");
1150                out.push_str(&format!("            \"POSITION\": {},\n", pos_acc));
1151                out.push_str(&format!("            \"NORMAL\": {}\n", nrm_acc));
1152                out.push_str("          },\n");
1153                out.push_str(&format!("          \"indices\": {}\n", idx_acc));
1154                if pi + 1 < mesh.primitives.len() {
1155                    out.push_str("        },\n");
1156                } else {
1157                    out.push_str("        }\n");
1158                }
1159            }
1160            out.push_str("      ]\n");
1161            if mi + 1 < self.meshes.len() {
1162                out.push_str("    },\n");
1163            } else {
1164                out.push_str("    }\n");
1165            }
1166        }
1167        out.push_str("  ],\n");
1168        if !self.materials.is_empty() {
1169            out.push_str("  \"materials\": [\n");
1170            for (mi, mat) in self.materials.iter().enumerate() {
1171                out.push_str("    {\n");
1172                out.push_str(&format!(
1173                    "      \"name\": \"{}\",\n",
1174                    escape_json(&mat.name)
1175                ));
1176                out.push_str("      \"pbrMetallicRoughness\": {\n");
1177                out.push_str(&format!(
1178                    "        \"baseColorFactor\": [{}, {}, {}, {}],\n",
1179                    mat.base_color_factor[0],
1180                    mat.base_color_factor[1],
1181                    mat.base_color_factor[2],
1182                    mat.base_color_factor[3]
1183                ));
1184                out.push_str(&format!(
1185                    "        \"metallicFactor\": {},\n",
1186                    mat.metallic_factor
1187                ));
1188                out.push_str(&format!(
1189                    "        \"roughnessFactor\": {}\n",
1190                    mat.roughness_factor
1191                ));
1192                out.push_str("      },\n");
1193                out.push_str(&format!(
1194                    "      \"emissiveFactor\": [{}, {}, {}],\n",
1195                    mat.emissive_factor[0], mat.emissive_factor[1], mat.emissive_factor[2]
1196                ));
1197                out.push_str(&format!("      \"alphaMode\": \"{}\",\n", mat.alpha_mode));
1198                out.push_str(&format!("      \"doubleSided\": {}\n", mat.double_sided));
1199                if mi + 1 < self.materials.len() {
1200                    out.push_str("    },\n");
1201                } else {
1202                    out.push_str("    }\n");
1203                }
1204            }
1205            out.push_str("  ],\n");
1206        }
1207        out.push_str("  \"accessors\": [],\n");
1208        out.push_str("  \"bufferViews\": [],\n");
1209        out.push_str("  \"buffers\": []\n");
1210        out.push('}');
1211        out
1212    }
1213}
1214/// A mesh primitive extended with morph targets and blend weights.
1215pub struct MorphPrimitive {
1216    /// The base geometry.
1217    pub base: GltfPrimitive,
1218    /// List of morph targets.
1219    pub targets: Vec<MorphTarget>,
1220    /// Current blend weights (one per target).
1221    pub weights: Vec<f32>,
1222}
1223impl MorphPrimitive {
1224    /// Set the blend weight for a given target index (clamped to \[0, 1\]).
1225    pub fn set_weight(&mut self, target_idx: usize, weight: f32) {
1226        if target_idx < self.weights.len() {
1227            self.weights[target_idx] = weight.clamp(0.0, 1.0);
1228        }
1229    }
1230    /// Compute the blended positions using all target weights.
1231    pub fn blend(&self) -> Vec<[f32; 3]> {
1232        let mut result = self.base.positions.clone();
1233        for (target, &w) in self.targets.iter().zip(self.weights.iter()) {
1234            let n = result.len().min(target.position_deltas.len());
1235            for i in 0..n {
1236                result[i][0] += target.position_deltas[i][0] * w;
1237                result[i][1] += target.position_deltas[i][1] * w;
1238                result[i][2] += target.position_deltas[i][2] * w;
1239            }
1240        }
1241        result
1242    }
1243}
1244/// Writer that serialises a `GltfScene` into the GLB binary format.
1245///
1246/// GLB layout:
1247/// - 12-byte header: magic (4), version (4), total_length (4)
1248/// - JSON chunk:     chunk_length (4), chunk_type "JSON" (4), JSON payload (aligned to 4 bytes)
1249/// - BIN chunk (if any binary data): chunk_length (4), chunk_type "BIN\0" (4), data
1250pub struct GlbWriter {
1251    /// Whether to include a (empty) BIN chunk even when there is no binary data.
1252    pub include_empty_bin: bool,
1253}
1254impl GlbWriter {
1255    /// Create a new `GlbWriter`.
1256    pub fn new() -> Self {
1257        GlbWriter {
1258            include_empty_bin: false,
1259        }
1260    }
1261    /// Serialise the scene into GLB bytes.
1262    pub fn write(&self, scene: &GltfScene) -> Vec<u8> {
1263        let json = scene.to_json();
1264        let json_bytes = json.as_bytes();
1265        let json_len = json_bytes.len();
1266        let json_padded_len = (json_len + 3) & !3;
1267        let json_padding = json_padded_len - json_len;
1268        let chunk_header_size = 8usize;
1269        let header_size = 12usize;
1270        let total_len = header_size
1271            + chunk_header_size
1272            + json_padded_len
1273            + if self.include_empty_bin {
1274                chunk_header_size
1275            } else {
1276                0
1277            };
1278        let mut out = Vec::with_capacity(total_len);
1279        out.extend_from_slice(b"glTF");
1280        out.extend_from_slice(&2u32.to_le_bytes());
1281        out.extend_from_slice(&(total_len as u32).to_le_bytes());
1282        out.extend_from_slice(&(json_padded_len as u32).to_le_bytes());
1283        out.extend_from_slice(&0x4E4F534Au32.to_le_bytes());
1284        out.extend_from_slice(json_bytes);
1285        #[allow(clippy::same_item_push)]
1286        for _ in 0..json_padding {
1287            out.push(0x20);
1288        }
1289        if self.include_empty_bin {
1290            out.extend_from_slice(&0u32.to_le_bytes());
1291            out.extend_from_slice(&0x004E4942u32.to_le_bytes());
1292        }
1293        out
1294    }
1295}