Skip to main content

inochi2d_parser/
owned.rs

1use std::{collections::HashMap, io::Read};
2
3use crate::json_extra::JsonExt;
4
5#[inline]
6fn read_n<R: Read, const N: usize>(data: &mut R) -> std::io::Result<[u8; N]> {
7    let mut buf = [0_u8; N];
8    data.read_exact(&mut buf)?;
9    Ok(buf)
10}
11
12#[inline]
13fn read_u8<R: Read>(data: &mut R) -> std::io::Result<u8> {
14    let buf = read_n::<_, 1>(data)?;
15    Ok(u8::from_ne_bytes(buf))
16}
17
18#[inline]
19fn read_be_u32<R: Read>(data: &mut R) -> std::io::Result<u32> {
20    let buf = read_n::<_, 4>(data)?;
21    Ok(u32::from_be_bytes(buf))
22}
23
24/// Reads `n` bytes growing the buffer incrementally, so a corrupt
25/// length field cannot trigger a huge upfront allocation.
26#[inline]
27fn read_vec<R: Read>(data: &mut R, n: usize) -> std::io::Result<Vec<u8>> {
28    let mut buf = Vec::new();
29    data.take(n as u64).read_to_end(&mut buf)?;
30    if buf.len() != n {
31        return Err(std::io::Error::new(
32            std::io::ErrorKind::UnexpectedEof,
33            format!("expected {} bytes, got {}", n, buf.len()),
34        ));
35    }
36    Ok(buf)
37}
38
39/// Root structure of the Inochi2D puppet model.
40/// Contains metadata, physics, node tree, animation parameters and visual organization.
41#[derive(Clone, Debug)]
42pub struct Puppet {
43    /// Creator, version and rights information.
44    pub meta: Meta,
45
46    /// Global physics configuration (gravity, scale).
47    pub physics: Physics,
48
49    /// Hierarchical node tree (root + recursive children).
50    pub nodes: Node,
51
52    /// Animatable parameters that control the puppet (sliders/dials).
53    pub params: HashMap<u32, Param>,
54
55    /// Parameter automation tracks (not implemented in this model).
56    pub automation: Automation,
57
58    /// Pre-recorded animation clips.
59    pub animations: HashMap<String, Animation>,
60
61    /// Node groups for visual organization in the editor.
62    /// The folders/hierarchies you see in the editor UI.
63    pub groups: Vec<Group>,
64
65    /// Extra vendor extension data.
66    pub vendors: Vec<VendorData>,
67
68    /// List of textures.
69    pub textures: Vec<Texture>,
70}
71
72impl Puppet {
73    /// Load a puppet from a file.
74    pub fn open<P>(path: P) -> std::io::Result<Self>
75    where
76        P: AsRef<std::path::Path>,
77    {
78        let mut file = std::fs::File::open(path)?;
79        Self::from_reader(&mut file)
80    }
81
82    /// Load a puppet from in-memory bytes.
83    pub fn from_bytes(bytes: &[u8]) -> std::io::Result<Self> {
84        let mut cursor = std::io::Cursor::new(bytes);
85        Self::from_reader(&mut cursor)
86    }
87
88    /// Load a puppet from any `Read`.
89    fn from_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
90        let magic = read_n::<_, 8>(reader)?;
91
92        if !magic.starts_with(b"TRNSRTS\0") {
93            return Err(std::io::Error::new(
94                std::io::ErrorKind::InvalidData,
95                "Invalid magic: expected TRNSRTS",
96            ));
97        }
98
99        let size = read_be_u32(reader)?;
100        let json_buffer = read_vec(reader, size as usize)?;
101
102        let json_data = std::str::from_utf8(&json_buffer)
103            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
104
105        let values = json::parse(json_data)
106            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
107
108        let mut puppet = Puppet::from_json(&values)?;
109
110        // TEX_SECT
111        let tex_magic = read_n::<_, 8>(reader)?;
112        if !tex_magic.starts_with(b"TEX_SECT") {
113            return Err(std::io::Error::new(
114                std::io::ErrorKind::InvalidData,
115                "Invalid magic: expected TEX_SECT",
116            ));
117        }
118
119        let texture_count = read_be_u32(reader)?;
120
121        for id in 0..texture_count {
122            let tex_len = read_be_u32(reader)?;
123            let format_byte = read_u8(reader)?;
124            let format = TextureFormat::from_byte(format_byte).ok_or_else(|| {
125                std::io::Error::new(
126                    std::io::ErrorKind::InvalidData,
127                    format!("Invalid texture format: {}", format_byte),
128                )
129            })?;
130
131            let tex_data = read_vec(reader, tex_len as usize)?;
132            let (width, height) = format.get_img_dim(&tex_data)?;
133
134            puppet
135                .textures
136                .push(Texture::new(id, width, height, format, tex_data));
137        }
138
139        // EXT_SECT (optional - if EOF, return without vendors)
140        let ext_magic = match read_n::<_, 8>(reader) {
141            Ok(m) => m,
142            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
143                return Ok(puppet);
144            }
145            Err(e) => return Err(e),
146        };
147
148        if !ext_magic.starts_with(b"EXT_SECT") {
149            return Err(std::io::Error::new(
150                std::io::ErrorKind::InvalidData,
151                "Invalid magic: expected EXT_SECT",
152            ));
153        }
154
155        let ext_count = read_be_u32(reader)?;
156
157        for _ in 0..ext_count {
158            let name_len = read_be_u32(reader)?;
159            let name_bytes = read_vec(reader, name_len as usize)?;
160            let name = String::from_utf8_lossy(&name_bytes).into_owned();
161
162            let payload_len = read_be_u32(reader)?;
163            let payload_bytes = read_vec(reader, payload_len as usize)?;
164            let data = json::parse(&String::from_utf8_lossy(&payload_bytes))
165                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
166
167            puppet.vendors.push(VendorData { name, data });
168        }
169
170        Ok(puppet)
171    }
172
173    fn from_json(root: &json::JsonValue) -> std::io::Result<Self> {
174        let missing = |field: &str| {
175            std::io::Error::new(
176                std::io::ErrorKind::InvalidData,
177                format!("missing '{}' in puppet JSON", field),
178            )
179        };
180        let meta_v = root.get("meta").ok_or_else(|| missing("meta"))?;
181        let physics_v = root.get("physics").ok_or_else(|| missing("physics"))?;
182        let nodes_v = root.get("nodes").ok_or_else(|| missing("nodes"))?;
183
184        Ok(Self {
185            meta: Meta::from_json(meta_v),
186            physics: Physics::from_json(physics_v),
187            nodes: Node::from_json(nodes_v),
188            params: parse_params(root),
189            automation: Automation {},
190            animations: parse_animations(root),
191            groups: parse_groups(root),
192            textures: Vec::new(),
193            vendors: Vec::new(),
194        })
195    }
196}
197
198/// Puppet metadata (creator, version, rights, contact).
199#[derive(Clone, Debug)]
200pub struct Meta {
201    /// Descriptive name of the puppet.
202    pub name: Option<String>,
203
204    /// Inochi2D format version used (e.g. "1.0").
205    pub version: String,
206
207    /// Rigger name (who built the skeleton).
208    pub rigger: Option<String>,
209
210    /// Name of the artist who created the visual assets.
211    pub artist: Option<String>,
212
213    /// Usage and distribution rights.
214    pub rights: Option<String>,
215
216    /// Model copyright.
217    pub copyright: Option<String>,
218
219    /// URL to usage license.
220    pub license_url: Option<String>,
221
222    /// Creator contact information.
223    pub contact: Option<String>,
224
225    /// Visual reference or link of the model.
226    pub reference: Option<String>,
227
228    /// Texture ID for UI thumbnail (index in the blob).
229    pub thumbnail_id: u32,
230
231    /// If true, preserves pixels during render (no smoothing).
232    pub preserve_pixels: bool,
233}
234
235impl Meta {
236    fn from_json(v: &json::JsonValue) -> Self {
237        Self {
238            name: v.get_str("name").map(str::to_owned),
239            version: v.get_str("version").unwrap_or("1.0").to_owned(),
240            rigger: v.get_str("rigger").map(str::to_owned),
241            artist: v.get_str("artist").map(str::to_owned),
242            rights: v.get_str("rights").map(str::to_owned),
243            copyright: v.get_str("copyright").map(str::to_owned),
244            license_url: v.get_str("licenseURL").map(str::to_owned),
245            contact: v.get_str("contact").map(str::to_owned),
246            reference: v.get_str("reference").map(str::to_owned),
247            thumbnail_id: v.get_u32("thumbnailId").unwrap_or(u32::MAX),
248            preserve_pixels: v.get_bool("preservePixels", false),
249        }
250    }
251}
252
253#[derive(Clone, Debug)]
254pub struct Physics {
255    pub pixels_per_meter: f32,
256    pub gravity: f32,
257}
258
259impl Physics {
260    fn from_json(v: &json::JsonValue) -> Self {
261        Self {
262            pixels_per_meter: v.get_f32("pixelsPerMeter", 1000.0),
263            gravity: v.get_f32("gravity", 9.8),
264        }
265    }
266}
267
268/// Node in the puppet's hierarchical tree.
269/// Can be visual (Part, Camera) or container (Composite, MeshGroup).
270#[derive(Clone, Debug, Default)]
271pub struct Node {
272    /// Global unique identifier of the node.
273    pub uuid: u32,
274
275    /// Readable node name (visible in editor).
276    pub name: String,
277
278    /// Specific node type and associated data (Part, Camera, etc).
279    pub type_node: NodeDataType,
280
281    /// If false, the node and its children are not rendered.
282    pub enabled: bool,
283
284    /// Z order (depth) in render.
285    /// Higher values = more in front.
286    pub zsort: f32,
287
288    /// Local transform (position, rotation, scale).
289    pub transform: Transform,
290
291    /// If true, the transform is not affected by parent.
292    /// Useful for UI or screen-fixed elements.
293    pub lock_to_root: bool,
294
295    /// Child nodes (recursive tree structure).
296    pub children: Vec<Node>,
297}
298
299impl Node {
300    /// Pre-order iterator over this node and all its descendants.
301    pub fn iter(&self) -> NodeIter<'_> {
302        NodeIter { stack: vec![self] }
303    }
304
305    /// Search the subtree (including `self`) for a node by UUID.
306    pub fn find_by_uuid(&self, uuid: u32) -> Option<&Node> {
307        self.iter().find(|n| n.uuid == uuid)
308    }
309
310    fn from_json(v: &json::JsonValue) -> Self {
311        let type_str = v.get_str("type").unwrap_or("generic");
312        Self {
313            uuid: v.get_u32("uuid").unwrap_or(u32::MAX),
314            name: v.get_str("name").unwrap_or("").to_owned(),
315            type_node: NodeDataType::from_json(type_str, v),
316            enabled: v.get_bool("enabled", true),
317            zsort: v.get_f32("zsort", 0.0),
318            transform: Transform::from_json(v),
319            lock_to_root: v.get_bool("lockToRoot", false),
320            children: v
321                .get_array("children")
322                .unwrap_or(&[])
323                .iter()
324                .map(Node::from_json)
325                .collect(),
326        }
327    }
328}
329
330/// Pre-order iterator over a node tree. Created by [`Node::iter`].
331pub struct NodeIter<'a> {
332    stack: Vec<&'a Node>,
333}
334
335impl<'a> Iterator for NodeIter<'a> {
336    type Item = &'a Node;
337
338    fn next(&mut self) -> Option<Self::Item> {
339        let node = self.stack.pop()?;
340        self.stack.extend(node.children.iter().rev());
341        Some(node)
342    }
343}
344
345/// Local transform of a node.
346/// Applied relative to parent node.
347#[derive(Debug, Clone, Default, Copy)]
348pub struct Transform {
349    /// Translation (x, y, z in pixels).
350    /// z typically 0.0, used only for relative depth.
351    pub translation: [f32; 3],
352
353    /// Rotation (x, y, z in radians).
354    /// Typically only z is used (2D rotation in XY plane).
355    pub rotation: [f32; 3],
356
357    /// Scale (sx, sy).
358    /// 1.0 = original size, <1.0 = smaller, >1.0 = larger.
359    pub scale: [f32; 2],
360}
361
362impl Transform {
363    fn from_json(v: &json::JsonValue) -> Self {
364        match v.get("transform") {
365            Some(t) => Self {
366                translation: t.get_vec3("trans").unwrap_or_default(),
367                rotation: t.get_vec3("rot").unwrap_or_default(),
368                scale: t.get_vec2("scale").unwrap_or([1.0, 1.0]),
369            },
370            None => Self::default(),
371        }
372    }
373}
374
375/// Supported node types in the tree.
376#[derive(Clone, Debug, Default)]
377pub enum NodeDataType {
378    /// Visual node with mesh and textures (face, limbs, etc).
379    Part(PartData),
380
381    /// Camera node (defines render viewport).
382    Camera(CameraData),
383
384    /// Simulated physics node (pendulum/spring).
385    SimplePhysics(SimplePhysicsData),
386
387    /// Visual container with blend mode and opacity.
388    Composite(CompositeData),
389
390    /// Node defining a mask for clipping descendants.
391    Mask(MaskData),
392
393    /// Group of meshes with dynamic deformation.
394    MeshGroup(MeshGroupData),
395
396    /// Generic node with no specific data (fallback).
397    #[default]
398    Generic,
399}
400
401impl NodeDataType {
402    fn from_json(type_str: &str, v: &json::JsonValue) -> Self {
403        match type_str.to_ascii_lowercase().as_str() {
404            "part" => Self::Part(PartData::from_json(v)),
405            "camera" => Self::Camera(CameraData::from_json(v)),
406            "simplephysics" => Self::SimplePhysics(SimplePhysicsData::from_json(v)),
407            "composite" => Self::Composite(CompositeData::from_json(v)),
408            "mask" => Self::Mask(MaskData::from_json(v)),
409            "meshgroup" => Self::MeshGroup(MeshGroupData::from_json(v)),
410            _ => Self::Generic,
411        }
412    }
413
414    pub fn as_part(&self) -> Option<&PartData> {
415        match self {
416            Self::Part(d) => Some(d),
417            _ => None,
418        }
419    }
420    pub fn as_composite(&self) -> Option<&CompositeData> {
421        match self {
422            Self::Composite(d) => Some(d),
423            _ => None,
424        }
425    }
426    pub fn as_mask(&self) -> Option<&MaskData> {
427        match self {
428            Self::Mask(d) => Some(d),
429            _ => None,
430        }
431    }
432    pub fn as_mesh_group(&self) -> Option<&MeshGroupData> {
433        match self {
434            Self::MeshGroup(d) => Some(d),
435            _ => None,
436        }
437    }
438    pub fn as_camera(&self) -> Option<&CameraData> {
439        match self {
440            Self::Camera(d) => Some(d),
441            _ => None,
442        }
443    }
444    pub fn as_simple_physics(&self) -> Option<&SimplePhysicsData> {
445        match self {
446            Self::SimplePhysics(d) => Some(d),
447            _ => None,
448        }
449    }
450    pub fn is_generic(&self) -> bool {
451        matches!(self, Self::Generic)
452    }
453}
454
455/// 3D geometry of a visual node.
456/// Vertices and UVs are stored as flat arrays for efficiency.
457#[derive(Debug, Default, Clone)]
458pub struct Mesh {
459    /// Vertex positions (flat array: [x1, y1, x2, y2, ...]).
460    /// Each pair = 2D coordinates of one vertex.
461    pub vertices: Vec<f32>,
462
463    /// Triangle indices (triples of indices into `vertices`).
464    /// Defines which vertices form each triangle for render.
465    pub indices: Vec<u32>,
466
467    /// UV coordinates (flat array: [u1, v1, u2, v2, ...]).
468    /// Texture mapping per vertex.
469    pub uvs: Vec<f32>,
470
471    /// Origin/pivot point (x, y in pixels).
472    /// Center of rotation and transform for the mesh.
473    pub origin: [f32; 2],
474}
475
476fn parse_mesh(v: &json::JsonValue) -> Option<Mesh> {
477    let mesh = v.get("mesh")?;
478    let verts = mesh.get_array("verts")?;
479    if verts.is_empty() {
480        return None;
481    }
482    let indices = mesh.get_array("indices")?;
483    let uvs = mesh.get_array("uvs")?;
484    let origin = mesh.get_vec2("origin")?;
485
486    Some(Mesh {
487        vertices: verts.iter().map(|v| v.as_f32().unwrap_or(0.0)).collect(),
488        indices: indices.iter().map(|i| i.as_u32().unwrap_or(0)).collect(),
489        uvs: uvs.iter().map(|v| v.as_f32().unwrap_or(0.0)).collect(),
490        origin,
491    })
492}
493
494/// Mask node data (defines clipping region).
495#[derive(Clone, Debug, Default)]
496pub struct MaskData {
497    /// Geometry defining the mask shape.
498    pub mesh: Option<Mesh>,
499}
500
501impl MaskData {
502    fn from_json(v: &json::JsonValue) -> Self {
503        Self {
504            mesh: parse_mesh(v),
505        }
506    }
507}
508
509#[derive(Debug, Clone, Default)]
510pub struct Mask {
511    pub source: u32,
512    pub mode: MaskMode,
513}
514
515/// Mask application mode.
516#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
517pub enum MaskMode {
518    /// Standard clipping (shows only inside the mask).
519    #[default]
520    Mask,
521
522    /// Dodge/inverse (shows only outside the mask).
523    Dodge,
524}
525
526fn parse_masks(v: &json::JsonValue) -> Vec<Mask> {
527    let Some(masks_val) = v.get("masks") else {
528        return Vec::new();
529    };
530    let arr = masks_val.as_array().unwrap_or(&[]);
531    let mut result = Vec::with_capacity(arr.len());
532    for m in arr {
533        let Some(obj) = m.as_object() else { continue };
534        let (Some(source), Some(mode)) = (obj.get("source"), obj.get("mode")) else {
535            continue;
536        };
537        result.push(Mask {
538            source: source.as_u32().unwrap_or(u32::MAX),
539            mode: match mode.as_str().unwrap_or("mask") {
540                v if v.eq_ignore_ascii_case("dodgemask") => MaskMode::Dodge,
541                _ => MaskMode::Mask,
542            },
543        });
544    }
545    result
546}
547
548/// Data for a visual node (renderable mesh with textures).
549#[derive(Clone, Debug, Default)]
550pub struct PartData {
551    /// Node geometry (vertices, indices, UVs, origin).
552    pub mesh: Option<Mesh>,
553
554    /// List of texture indices to render in this node.
555    ///
556    /// Multiple textures can be stacked (layers).
557    ///
558    /// Indices per texture slot: `[0] = Albedo, [1] = Emissive, [2] = BumpMap`
559    pub textures: [u32; 3],
560
561    /// Blend mode (Normal, Multiply, Screen, etc).
562    pub blend_mode: BlendMode,
563
564    /// Additive RGB tint (1.0, 1.0, 1.0 = no change).
565    pub tint: [f32; 3],
566
567    /// Screen tint (for screen light/color effects).
568    pub screen_tint: [f32; 3],
569
570    /// Emission strength (node glow/brightness).
571    pub emission_strength: f32,
572
573    /// Node mask (for masking).
574    pub mask: Vec<Mask>,
575
576    /// Threshold for alpha clipping (binary mask).
577    pub mask_threshold: f32,
578
579    /// Global opacity (0.0 = transparent, 1.0 = opaque).
580    pub opacity: f32,
581
582    /// Path in original PSD file (creation metadata).
583    pub psd_layer_path: Option<String>,
584}
585
586impl PartData {
587    fn from_json(v: &json::JsonValue) -> Self {
588        let textures_arr = v.get_array("textures").unwrap_or(&[]);
589        Self {
590            mesh: parse_mesh(v),
591            textures: [
592                textures_arr
593                    .first()
594                    .and_then(|t| t.as_u32())
595                    .unwrap_or(u32::MAX),
596                textures_arr
597                    .get(1)
598                    .and_then(|t| t.as_u32())
599                    .unwrap_or(u32::MAX),
600                textures_arr
601                    .get(2)
602                    .and_then(|t| t.as_u32())
603                    .unwrap_or(u32::MAX),
604            ],
605            blend_mode: BlendMode::from_str(v.get_str("blend_mode").unwrap_or("normal"))
606                .unwrap_or_default(),
607            tint: v.get_vec3("tint").unwrap_or([1.0, 1.0, 1.0]),
608            screen_tint: v.get_vec3("screenTint").unwrap_or([0.0, 0.0, 0.0]),
609            emission_strength: v.get_f32("emissionStrength", 0.0),
610            mask: parse_masks(v),
611            mask_threshold: v.get_f32("mask_threshold", 0.5),
612            opacity: v.get_f32("opacity", 1.0),
613            psd_layer_path: v.get_str("psdLayerPath").map(str::to_owned),
614        }
615    }
616}
617
618/// Camera node data (defines visible region).
619#[derive(Clone, Debug, Default)]
620pub struct CameraData {
621    /// Viewport in pixels (width, height).
622    /// Defines the visible render area.
623    pub viewport: [f32; 2],
624}
625
626impl CameraData {
627    fn from_json(v: &json::JsonValue) -> Self {
628        Self {
629            viewport: v.get_vec2("viewport").unwrap_or([1280.0, 720.0]),
630        }
631    }
632}
633
634/// Node data with physics simulation.
635/// Simulates behavior of chains/tails/accessories.
636#[derive(Clone, Debug, Default)]
637pub struct SimplePhysicsData {
638    /// UUID of the parameter controlling the physics output.
639    pub param: u32,
640
641    /// Simulation type (Pendulum or SpringPendulum).
642    pub model_type: PhysicsModelType,
643
644    /// How to map angle/length to output parameter.
645    pub map_mode: PhysicsMapMode,
646
647    /// Gravity for this simulation (overrides global if >0).
648    pub gravity: f32,
649
650    /// Length of the "bone" in pixels.
651    pub length: f32,
652
653    /// Oscillation frequency (Hz).
654    pub frequency: f32,
655
656    /// Angular damping (reduces angle oscillation).
657    pub angle_damping: f32,
658
659    /// Length damping (reduces extension oscillation).
660    pub length_damping: f32,
661
662    /// Parameter output scale (sx, sy).
663    pub output_scale: [f32; 2],
664
665    /// If true, physics is relative to local node (not global).
666    pub local_only: Option<bool>,
667}
668
669impl SimplePhysicsData {
670    fn from_json(v: &json::JsonValue) -> Self {
671        Self {
672            param: v.get_u32("param").unwrap_or(u32::MAX),
673            model_type: match v.get_str("model_type") {
674                Some(s) if s.eq_ignore_ascii_case("springpendulum") => {
675                    PhysicsModelType::SpringPendulum
676                }
677                _ => PhysicsModelType::Pendulum,
678            },
679            map_mode: match v.get_str("map_mode") {
680                Some(s) if s.eq_ignore_ascii_case("xy") => PhysicsMapMode::XY,
681                Some(s) if s.eq_ignore_ascii_case("lengthangle") => PhysicsMapMode::LengthAngle,
682                Some(s) if s.eq_ignore_ascii_case("yx") => PhysicsMapMode::YX,
683                _ => PhysicsMapMode::AngleLength,
684            },
685            gravity: v.get_f32("gravity", 1.0),
686            length: v.get_f32("length", 100.0),
687            frequency: v.get_f32("frequency", 1.0),
688            angle_damping: v.get_f32("angle_damping", 0.5),
689            length_damping: v.get_f32("length_damping", 0.5),
690            output_scale: v.get_vec2("output_scale").unwrap_or([1.0, 1.0]),
691            local_only: v.get_as_bool("local_only"),
692        }
693    }
694}
695
696/// Supported physics model types.
697#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
698pub enum PhysicsModelType {
699    /// Simple pendulum (oscillates under gravity).
700    #[default]
701    Pendulum,
702
703    /// Spring pendulum (oscillates and extends).
704    SpringPendulum,
705}
706
707/// Physics → parameter mapping modes.
708#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
709pub enum PhysicsMapMode {
710    /// Angle and length (2D polar).
711    #[default]
712    AngleLength,
713
714    /// Cartesian X and Y.
715    XY,
716
717    /// Length and angle (reverse order).
718    LengthAngle,
719
720    /// Cartesian Y and X (reverse order).
721    YX,
722}
723
724/// Visual container data (groups nodes with shared properties).
725#[derive(Clone, Debug, Default)]
726pub struct CompositeData {
727    /// Blend mode for the entire group.
728    pub blend_mode: BlendMode,
729
730    /// Additive tint applied to the entire group.
731    pub tint: [f32; 3],
732
733    /// Group screen tint.
734    pub screen_tint: [f32; 3],
735
736    /// Group global opacity.
737    pub opacity: f32,
738
739    /// List of masks.
740    pub mask: Vec<Mask>,
741
742    /// Alpha clipping threshold.
743    pub mask_threshold: f32,
744
745    /// If true, propagates meshgroup properties to children.
746    pub propagate_meshgroup: Option<bool>,
747}
748
749impl CompositeData {
750    fn from_json(v: &json::JsonValue) -> Self {
751        Self {
752            blend_mode: BlendMode::from_str(v.get_str("blend_mode").unwrap_or("normal"))
753                .unwrap_or_default(),
754            tint: v.get_vec3("tint").unwrap_or([1.0, 1.0, 1.0]),
755            screen_tint: v.get_vec3("screenTint").unwrap_or([0.0, 0.0, 0.0]),
756            opacity: v.get_f32("opacity", 1.0),
757            mask: parse_masks(v),
758            mask_threshold: v.get_f32("mask_threshold", 0.5),
759            propagate_meshgroup: v.get_as_bool("propagate_meshgroup"),
760        }
761    }
762}
763
764/// Mesh group data (enables dynamic deformation).
765#[derive(Clone, Debug, Default)]
766pub struct MeshGroupData {
767    /// Group geometry (can be deformed by parameters).
768    pub mesh: Option<Mesh>,
769
770    /// If true, the mesh can be dynamically deformed.
771    pub dynamic_deformation: bool,
772
773    /// If true, transforms child nodes along with the mesh.
774    pub translate_children: bool,
775}
776
777impl MeshGroupData {
778    fn from_json(v: &json::JsonValue) -> Self {
779        Self {
780            mesh: parse_mesh(v),
781            dynamic_deformation: v.get_bool("dynamic_deformation", false),
782            translate_children: v.get_bool("translate_children", true),
783        }
784    }
785}
786
787/// Blending modes for visual composition.
788/// Defines how colors of overlapping nodes are merged.
789#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
790pub enum BlendMode {
791    /// Normal blend (standard alpha compositing).
792    #[default]
793    Normal,
794
795    /// Multiply colors (darken).
796    Multiply,
797
798    /// Screen blend (lighten, light effect).
799    Screen,
800
801    /// Overlay (combines Multiply and Screen).
802    Overlay,
803
804    /// Darken (only darker pixels).
805    Darken,
806
807    /// Lighten (only lighter pixels).
808    Lighten,
809
810    /// Color dodge (lightens selectively).
811    ColorDodge,
812
813    /// Linear dodge (lightens linearly).
814    LinearDodge,
815
816    /// Add (sums colors, glow effect).
817    Add,
818
819    /// Color burn (darkens selectively).
820    ColorBurn,
821
822    /// Hard light (strong contrast).
823    HardLight,
824
825    /// Soft light (soft contrast).
826    SoftLight,
827
828    /// Subtract (subtracts colors).
829    Subtract,
830
831    /// Difference (absolute color difference).
832    Difference,
833
834    /// Exclusion (soft difference).
835    Exclusion,
836
837    /// Inverse (inverts based on overlapping color factor).
838    Inverse,
839
840    /// DestinationIn (keeps only pixels where destination exists).
841    DestinationIn,
842
843    /// ClipToLower (clipping respecting transparency, against lower content).
844    ClipToLower,
845
846    /// SliceFromLower (inverse of ClipToLower, cuts by lower content).
847    SliceFromLower,
848}
849
850impl BlendMode {
851    #[allow(clippy::should_implement_trait)]
852    pub fn from_str(s: &str) -> Option<Self> {
853        match s {
854            s if s.eq_ignore_ascii_case("normal") => Some(Self::Normal),
855            s if s.eq_ignore_ascii_case("multiply") => Some(Self::Multiply),
856            s if s.eq_ignore_ascii_case("screen") => Some(Self::Screen),
857            s if s.eq_ignore_ascii_case("overlay") => Some(Self::Overlay),
858            s if s.eq_ignore_ascii_case("darken") => Some(Self::Darken),
859            s if s.eq_ignore_ascii_case("lighten") => Some(Self::Lighten),
860            s if s.eq_ignore_ascii_case("colordodge") => Some(Self::ColorDodge),
861            s if s.eq_ignore_ascii_case("colorburn") => Some(Self::ColorBurn),
862            s if s.eq_ignore_ascii_case("hardlight") => Some(Self::HardLight),
863            s if s.eq_ignore_ascii_case("softlight") => Some(Self::SoftLight),
864            s if s.eq_ignore_ascii_case("lineardodge") => Some(Self::LinearDodge),
865            s if s.eq_ignore_ascii_case("difference") => Some(Self::Difference),
866            s if s.eq_ignore_ascii_case("exclusion") => Some(Self::Exclusion),
867            s if s.eq_ignore_ascii_case("add") => Some(Self::Add),
868            s if s.eq_ignore_ascii_case("subtract") => Some(Self::Subtract),
869            s if s.eq_ignore_ascii_case("cliptolower") => Some(Self::ClipToLower),
870            s if s.eq_ignore_ascii_case("slicefromlower") => Some(Self::SliceFromLower),
871            s if s.eq_ignore_ascii_case("inverse") => Some(Self::Inverse),
872            s if s.eq_ignore_ascii_case("destinationin") => Some(Self::DestinationIn),
873            _ => None,
874        }
875    }
876}
877
878#[derive(Clone, Debug)]
879pub struct Param {
880    /// Parent node UUID (hierarchical parameter organization).
881    pub parent_uuid: Option<u32>,
882
883    /// Global unique identifier of the parameter.
884    pub uuid: u32,
885
886    /// Readable name (visible in UI as slider).
887    pub name: String,
888
889    /// If true, is 2D vector (X, Y); if false, is scalar.
890    pub is_vec2: bool,
891
892    /// Minimum allowed value (x, y if vec2).
893    pub min: [f32; 2],
894
895    /// Maximum allowed value (x, y if vec2).
896    pub max: [f32; 2],
897
898    /// Default value at load (x, y if vec2).
899    pub defaults: [f32; 2],
900
901    /// Points on X and Y axes for discrete interpolation.
902    /// Allows snap to specific values.
903    pub axis_points: [Vec<f32>; 2],
904
905    /// How multiple bindings affecting the same target are combined.
906    pub merge_mode: MergeMode,
907
908    /// List of nodes/properties this parameter affects.
909    pub bindings: Vec<ParamBinding>,
910}
911
912fn parse_params(root: &json::JsonValue) -> HashMap<u32, Param> {
913    let params = root.get_array("param").unwrap_or(&[]);
914    let mut map = HashMap::default();
915    for p in params {
916        let uuid = p.get_u32("uuid").unwrap_or(u32::MAX);
917        map.insert(uuid, Param::from_json(p));
918    }
919    map
920}
921
922impl Param {
923    fn from_json(v: &json::JsonValue) -> Self {
924        Self {
925            parent_uuid: v.get_u32("parentUUID"),
926            uuid: v.get_u32("uuid").unwrap_or(u32::MAX),
927            name: v.get_str("name").unwrap_or("").to_owned(),
928            is_vec2: v.get_bool("is_vec2", false),
929            min: v.get_vec2("min").unwrap_or([0.0, 0.0]),
930            max: v.get_vec2("max").unwrap_or([1.0, 1.0]),
931            defaults: v.get_vec2("defaults").unwrap_or([0.0, 1.0]),
932            axis_points: parse_axis_points(v),
933            merge_mode: parse_merge_mode(v.get_str("merge_mode")),
934            bindings: parse_bindings(v),
935        }
936    }
937}
938
939fn parse_axis_points(v: &json::JsonValue) -> [Vec<f32>; 2] {
940    let axis = v.get_array("axis_points").unwrap_or(&[]);
941    if axis.len() != 2 {
942        return [Vec::new(), Vec::new()];
943    }
944    let parse_axis = |a: &json::JsonValue| -> Vec<f32> {
945        a.as_array()
946            .unwrap_or(&[])
947            .iter()
948            .map(|v| v.as_f32().unwrap_or(0.0))
949            .collect()
950    };
951    [parse_axis(&axis[0]), parse_axis(&axis[1])]
952}
953
954/// Binding between a parameter and a node property.
955/// Defines what property is animated and with what values.
956#[derive(Debug, Clone)]
957pub struct ParamBinding {
958    /// UUID of the target node to be animated.
959    pub node: u32,
960
961    /// Specific node property (TransformTX, Deform, Opacity, etc).
962    pub param_name: ParamName,
963
964    /// Values sampled over the parameter's axis-point grid.
965    /// Interpolated between grid points according to `interpolate_mode`.
966    pub values: BindingValues,
967
968    /// Grid of explicitly set points: [x_axis_point][y_axis_point] = true
969    /// if the value was authored (false = interpolated by the runtime).
970    pub is_set: Vec<Vec<bool>>,
971
972    /// Interpolation type between grid points.
973    pub interpolate_mode: Interpolation,
974}
975
976fn parse_bindings(v: &json::JsonValue) -> Vec<ParamBinding> {
977    let bindings = v.get_array("bindings").unwrap_or(&[]);
978    bindings.iter().map(ParamBinding::from_json).collect()
979}
980
981impl ParamBinding {
982    fn from_json(v: &json::JsonValue) -> Self {
983        let param_name = parse_param_name(v.get_str("param_name"));
984        let values_v = v.get("values");
985        Self {
986            node: v.get_u32("node").unwrap_or(u32::MAX),
987            values: match values_v {
988                Some(vals) => match &param_name {
989                    ParamName::Deform => BindingValues::Deform(FlatDeformValues::new(vals)),
990                    ParamName::Other(_) => BindingValues::Other(vals.clone()),
991                    _ => BindingValues::Transform(FlatTransformValues::new(vals)),
992                },
993                None => BindingValues::Transform(FlatTransformValues {
994                    data: Vec::new(),
995                    frames: 0,
996                    values_per_frame: 0,
997                }),
998            },
999            param_name,
1000            is_set: parse_is_set(v),
1001            interpolate_mode: parse_interpolation(v.get_str("interpolate_mode")),
1002        }
1003    }
1004}
1005
1006fn parse_is_set(v: &json::JsonValue) -> Vec<Vec<bool>> {
1007    let is_set = v.get_array("isSet").unwrap_or(&[]);
1008    is_set
1009        .iter()
1010        .map(|row| {
1011            row.as_array()
1012                .unwrap_or(&[])
1013                .iter()
1014                .map(|b| b.as_bool().unwrap_or(false))
1015                .collect()
1016        })
1017        .collect()
1018}
1019
1020/// Node properties that can be animated by parameters.
1021#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
1022pub enum ParamName {
1023    /// Translation X (transform.translation.x).
1024    TransformTX,
1025
1026    /// Translation Y (transform.translation.y).
1027    TransformTY,
1028
1029    /// Translation Z (transform.translation.z).
1030    TransformTZ,
1031
1032    /// Scale X.
1033    TransformSX,
1034
1035    /// Scale Y.
1036    TransformSY,
1037
1038    /// Rotation X (radians).
1039    TransformRX,
1040
1041    /// Rotation Y (radians).
1042    TransformRY,
1043
1044    /// Rotation Z (radians, typically the one used).
1045    TransformRZ,
1046
1047    /// Mesh deformation (mesh warping).
1048    Deform,
1049
1050    #[default]
1051    /// Node opacity.
1052    Opacity,
1053
1054    /// Other unknown parameter.
1055    Other(String),
1056}
1057
1058fn parse_param_name(s: Option<&str>) -> ParamName {
1059    match s {
1060        Some("transform.t.x") => ParamName::TransformTX,
1061        Some("transform.t.y") => ParamName::TransformTY,
1062        Some("transform.t.z") => ParamName::TransformTZ,
1063        Some("transform.s.x") => ParamName::TransformSX,
1064        Some("transform.s.y") => ParamName::TransformSY,
1065        Some("transform.r.x") => ParamName::TransformRX,
1066        Some("transform.r.y") => ParamName::TransformRY,
1067        Some("transform.r.z") => ParamName::TransformRZ,
1068        Some("deform") => ParamName::Deform,
1069        Some("opacity") => ParamName::Opacity,
1070        Some(other) => ParamName::Other(other.to_owned()),
1071        None => ParamName::Other(String::new()),
1072    }
1073}
1074
1075/// Keyframe values for a binding.
1076/// Can be transform (scalar) or deformation (vertices).
1077#[derive(Debug, Clone)]
1078pub enum BindingValues {
1079    /// Values for transform/opacity properties (1 value per frame).
1080    Transform(FlatTransformValues),
1081
1082    /// Values for deformation (2D offsets per vertex).
1083    Deform(FlatDeformValues),
1084
1085    /// Fallback for unknown types.
1086    Other(json::JsonValue),
1087}
1088
1089/// Efficient storage of scalar binding values.
1090/// Deserialized from `Vec<Vec<f32>>` (parameter axis-point grid [x][y])
1091/// but stored flat. `frames` = points on the X axis,
1092/// `values_per_frame` = points on the Y axis (1 for non-vec2 params).
1093#[derive(Debug, Clone)]
1094pub struct FlatTransformValues {
1095    /// Flat data buffer: [x0_y0, x0_y1, ..., x1_y0, ...]
1096    pub data: Vec<f32>,
1097
1098    /// Number of points on the X axis (outer array length).
1099    pub frames: usize,
1100
1101    /// Number of points on the Y axis (inner array length).
1102    pub values_per_frame: usize,
1103}
1104
1105impl FlatTransformValues {
1106    pub fn new(values: &json::JsonValue) -> Self {
1107        let parsed: Vec<Vec<f32>> = values
1108            .as_array()
1109            .unwrap_or(&[])
1110            .iter()
1111            .filter_map(|frame| {
1112                frame
1113                    .as_array()
1114                    .map(|f| f.iter().filter_map(|v| v.as_f32()).collect())
1115            })
1116            .collect();
1117
1118        if parsed.is_empty() {
1119            return Self {
1120                data: Vec::new(),
1121                frames: 0,
1122                values_per_frame: 0,
1123            };
1124        }
1125
1126        let values_per_frame = parsed[0].len();
1127
1128        debug_assert!(
1129            parsed.iter().all(|v| v.len() == values_per_frame),
1130            "Inconsistent values per frame"
1131        );
1132
1133        let frames = parsed.len();
1134        let data = parsed.into_iter().flatten().collect();
1135        Self {
1136            data,
1137            frames,
1138            values_per_frame,
1139        }
1140    }
1141    /// Get a specific value from a frame and index.
1142    /// O(1) access with linear indexing.
1143    pub fn get(&self, frame: usize, index: usize) -> Option<f32> {
1144        if frame >= self.frames || index >= self.values_per_frame {
1145            return None;
1146        }
1147        self.data
1148            .get(frame * self.values_per_frame + index)
1149            .copied()
1150    }
1151
1152    /// Return the number of frames.
1153    pub fn frames(&self) -> usize {
1154        self.frames
1155    }
1156
1157    /// Return values per frame.
1158    pub fn values_per_frame(&self) -> usize {
1159        self.values_per_frame
1160    }
1161}
1162
1163/// Efficient storage of deformation binding values.
1164/// Deserialized from `values[x][y][vertex] = [dx, dy]` but stored flat.
1165/// `frames` = points on the X axis; `vertices_per_frame` flattens
1166/// the Y axis together with the per-vertex offsets.
1167#[derive(Debug, Clone)]
1168pub struct FlatDeformValues {
1169    /// Flat offset buffer: [x0_v0, x0_v1, ..., x1_v0, ...]
1170    pub data: Vec<[f32; 2]>,
1171
1172    /// Number of points on the X axis (outer array length).
1173    pub frames: usize,
1174
1175    /// Number of [dx, dy] entries per X-axis point
1176    /// (Y-axis points × vertex count).
1177    pub vertices_per_frame: usize,
1178}
1179
1180impl FlatDeformValues {
1181    pub fn new(values: &json::JsonValue) -> Self {
1182        let frames_data: Vec<Vec<[f32; 2]>> = values
1183            .as_array()
1184            .unwrap_or(&[])
1185            .iter()
1186            .map(|frame| {
1187                frame
1188                    .as_array()
1189                    .unwrap_or(&[])
1190                    .iter()
1191                    .flat_map(|vertex| {
1192                        vertex
1193                            .as_array()
1194                            .unwrap_or(&[])
1195                            .iter()
1196                            .filter_map(|coords| {
1197                                let pair = coords.as_array()?;
1198                                Some([pair.first()?.as_f32()?, pair.get(1)?.as_f32()?])
1199                            })
1200                    })
1201                    .collect()
1202            })
1203            .collect();
1204
1205        if frames_data.is_empty() {
1206            return Self {
1207                data: Vec::new(),
1208                frames: 0,
1209                vertices_per_frame: 0,
1210            };
1211        }
1212
1213        let frames = frames_data.len();
1214        let vertices_per_frame = frames_data[0].len();
1215
1216        debug_assert!(
1217            frames_data.iter().all(|f| f.len() == vertices_per_frame),
1218            "Inconsistent vertices per frame"
1219        );
1220
1221        let data = frames_data.into_iter().flatten().collect();
1222        Self {
1223            data,
1224            frames,
1225            vertices_per_frame,
1226        }
1227    }
1228    /// Get the [x, y] offset of a vertex at a specific frame.
1229    /// O(1) access with direct index computation.
1230    pub fn get(&self, frame: usize, vertex: usize) -> Option<[f32; 2]> {
1231        if frame >= self.frames || vertex >= self.vertices_per_frame {
1232            return None;
1233        }
1234        let idx = frame * self.vertices_per_frame + vertex;
1235        self.data.get(idx).copied()
1236    }
1237
1238    /// Return the total number of frames.
1239    pub fn frames(&self) -> usize {
1240        self.frames
1241    }
1242
1243    /// Return vertices per frame.
1244    pub fn vertices_per_frame(&self) -> usize {
1245        self.vertices_per_frame
1246    }
1247
1248    /// Get all offsets of a frame (extracted slice).
1249    /// Useful for applying full deformation to a mesh.
1250    pub fn get_frame(&self, frame: usize) -> Option<&[[f32; 2]]> {
1251        if frame >= self.frames {
1252            return None;
1253        }
1254        let start = frame * self.vertices_per_frame;
1255        self.data.get(start..start + self.vertices_per_frame)
1256    }
1257}
1258
1259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1260pub enum MergeMode {
1261    /// Sums effects (default for rotation, deform).
1262    Additive,
1263
1264    /// Multiplies effects (default for scale).
1265    Multiplicative,
1266
1267    /// Overwrites (last parameter wins).
1268    Override,
1269
1270    /// Ignores existing values.
1271    Forced,
1272}
1273
1274fn parse_merge_mode(s: Option<&str>) -> MergeMode {
1275    match s {
1276        Some(s) if s.eq_ignore_ascii_case("multiply") => MergeMode::Multiplicative,
1277        Some(s) if s.eq_ignore_ascii_case("override") => MergeMode::Override,
1278        Some(s) if s.eq_ignore_ascii_case("forced") => MergeMode::Forced,
1279        _ => MergeMode::Additive,
1280    }
1281}
1282
1283/// Interpolation types between keyframes.
1284#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1285pub enum Interpolation {
1286    /// Linearly interpolates between frames (smoothed).
1287    #[default]
1288    Linear,
1289
1290    /// Jumps to previous keyframe value (no smoothing).
1291    Stepped,
1292
1293    /// Rounds to the nearest keyframe value (no smoothing).
1294    Nearest,
1295
1296    /// Smooth cubic interpolation (uses tension).
1297    Cubic,
1298}
1299
1300fn parse_interpolation(s: Option<&str>) -> Interpolation {
1301    match s {
1302        Some(s) if s.eq_ignore_ascii_case("stepped") => Interpolation::Stepped,
1303        Some(s) if s.eq_ignore_ascii_case("nearest") => Interpolation::Nearest,
1304        Some(s) if s.eq_ignore_ascii_case("cubic") => Interpolation::Cubic,
1305        Some(s) if s.eq_ignore_ascii_case("linear") => Interpolation::Linear,
1306        _ => Interpolation::Linear,
1307    }
1308}
1309
1310/// Automation track (placeholder struct).
1311/// Not implemented in this model, pending definition.
1312#[derive(Clone, Debug)]
1313pub struct Automation {}
1314
1315/// Pre-recorded animation clip.
1316/// Controls puppet parameters over time.
1317#[derive(Debug, Clone)]
1318pub struct Animation {
1319    /// Identifier name.
1320    pub name: String,
1321
1322    /// Duration of each frame in seconds (0.01666... ≈ 60fps).
1323    pub timestep: f32,
1324
1325    /// If true, values are added to current state instead of replacing.
1326    pub additive: bool,
1327
1328    /// Total number of frames in the animation.
1329    pub length: u32,
1330
1331    /// Lead-in frames (fade in).
1332    pub lead_in: u32,
1333
1334    /// Lead-out frames (fade out).
1335    pub lead_out: u32,
1336
1337    /// Animation weight for blending (0.0-1.0).
1338    pub weight: f32,
1339
1340    /// Tracks controlling individual parameters.
1341    pub lanes: Vec<AnimationLane>,
1342}
1343
1344impl Animation {
1345    /// Total duration in seconds.
1346    #[inline]
1347    pub fn duration(&self) -> f32 {
1348        self.length as f32 * self.timestep
1349    }
1350
1351    /// Convert time (seconds) to frame (can be fractional).
1352    #[inline]
1353    pub fn time_to_frame(&self, time: f32) -> f32 {
1354        time / self.timestep
1355    }
1356
1357    /// Convert frame to time in seconds.
1358    #[inline]
1359    pub fn frame_to_time(&self, frame: f32) -> f32 {
1360        frame * self.timestep
1361    }
1362}
1363
1364fn parse_animations(root: &json::JsonValue) -> HashMap<String, Animation> {
1365    let Some(anims) = root.get("animations") else {
1366        return HashMap::default();
1367    };
1368    let Some(obj) = anims.as_object() else {
1369        return HashMap::default();
1370    };
1371
1372    let mut map = HashMap::default();
1373    for (name, data) in obj.iter() {
1374        map.insert(
1375            name.to_owned(),
1376            Animation {
1377                name: name.to_owned(),
1378                timestep: data.get_f32("timestep", 0.016666668),
1379                additive: data.get_bool("additive", false),
1380                length: data.get_u32("length").unwrap_or(0),
1381                lead_in: data.get_u32("leadIn").unwrap_or(0),
1382                lead_out: data.get_u32("leadOut").unwrap_or(0),
1383                weight: data.get_f32("animationWeight", 1.0),
1384                lanes: parse_lanes(data),
1385            },
1386        );
1387    }
1388    map
1389}
1390
1391/// Animation lane that controls a specific parameter.
1392#[derive(Debug, Clone)]
1393pub struct AnimationLane {
1394    /// Interpolation type between keyframes.
1395    pub interpolation: Interpolation,
1396
1397    /// Target parameter UUID.
1398    pub param_uuid: u32,
1399
1400    /// Parameter component (0=X, 1=Y for vec2).
1401    pub target: u8,
1402
1403    /// How to combine with other animations/base values.
1404    pub merge_mode: MergeMode,
1405
1406    /// Keyframes ordered by frame.
1407    pub keyframes: Vec<Keyframe>,
1408}
1409
1410impl AnimationLane {
1411    /// Evaluate the value at a given frame (can be fractional).
1412    pub fn evaluate(&self, frame: f32) -> f32 {
1413        if self.keyframes.is_empty() {
1414            return 0.0;
1415        }
1416
1417        // Before the first keyframe
1418        if frame <= self.keyframes[0].frame as f32 {
1419            return self.keyframes[0].value;
1420        }
1421
1422        // After the last keyframe
1423        let last = &self.keyframes[self.keyframes.len() - 1];
1424        if frame >= last.frame as f32 {
1425            return last.value;
1426        }
1427
1428        // Find adjacent keyframes
1429        let mut prev_idx = 0;
1430        for (i, kf) in self.keyframes.iter().enumerate() {
1431            if kf.frame as f32 > frame {
1432                break;
1433            }
1434            prev_idx = i;
1435        }
1436
1437        let prev = &self.keyframes[prev_idx];
1438        let next = &self.keyframes[prev_idx + 1];
1439
1440        let t = (frame - prev.frame as f32) / (next.frame as f32 - prev.frame as f32);
1441
1442        match self.interpolation {
1443            Interpolation::Stepped => prev.value,
1444            Interpolation::Nearest => {
1445                if t < 0.5 {
1446                    prev.value
1447                } else {
1448                    next.value
1449                }
1450            }
1451            Interpolation::Linear => lerp(prev.value, next.value, t),
1452            Interpolation::Cubic => {
1453                // Catmull-Rom with tension
1454                let tension = (prev.tension + next.tension) * 0.5;
1455                cubic_interpolate(prev.value, next.value, t, tension)
1456            }
1457        }
1458    }
1459}
1460
1461fn parse_lanes(v: &json::JsonValue) -> Vec<AnimationLane> {
1462    let lanes = v.get_array("lanes").unwrap_or(&[]);
1463    lanes
1464        .iter()
1465        .map(|lane| AnimationLane {
1466            interpolation: parse_interpolation(lane.get_str("interpolation")),
1467            param_uuid: lane.get_u32("uuid").unwrap_or(u32::MAX),
1468            target: lane.get_u32("target").unwrap_or(0) as u8,
1469            merge_mode: parse_merge_mode(lane.get_str("merge_mode")),
1470            keyframes: parse_keyframes(lane),
1471        })
1472        .collect()
1473}
1474
1475/// Single keyframe.
1476#[derive(Debug, Clone, Copy)]
1477pub struct Keyframe {
1478    /// Frame index (integer).
1479    pub frame: u32,
1480
1481    /// Value at this frame.
1482    pub value: f32,
1483
1484    /// Tension for cubic interpolation (0.0-1.0).
1485    pub tension: f32,
1486}
1487
1488fn parse_keyframes(v: &json::JsonValue) -> Vec<Keyframe> {
1489    let kfs = v.get_array("keyframes").unwrap_or(&[]);
1490    kfs.iter()
1491        .map(|kf| Keyframe {
1492            frame: kf.get_u32("frame").unwrap_or(0),
1493            value: kf.get_f32("value", 0.0),
1494            tension: kf.get_f32("tension", 0.5),
1495        })
1496        .collect()
1497}
1498
1499/// Node group for visual organization in editor.
1500/// The "folders" you see in the UI, for easier navigation.
1501#[derive(Clone, Debug)]
1502pub struct Group {
1503    /// Unique group UUID.
1504    pub group_uuid: u32,
1505
1506    /// Readable group name (e.g. "Head", "Eyes", "Hair").
1507    pub name: String,
1508
1509    /// Normalized RGB color [0.0-1.0] for editor visualization.
1510    pub color: [f32; 3],
1511}
1512
1513fn parse_groups(root: &json::JsonValue) -> Vec<Group> {
1514    let groups = root.get_array("groups").unwrap_or(&[]);
1515    groups
1516        .iter()
1517        .map(|g| Group {
1518            group_uuid: g.get_u32("groupUUID").unwrap_or(u32::MAX),
1519            name: g.get_str("name").unwrap_or("").to_owned(),
1520            color: g.get_vec3("color").unwrap_or([0.0, 0.0, 0.0]),
1521        })
1522        .collect()
1523}
1524
1525/// Extended vendor data section.
1526#[derive(Debug, Clone)]
1527pub struct VendorData {
1528    /// Name/identifier of the vendor data.
1529    pub name: String,
1530    /// JSON payload.
1531    pub data: json::JsonValue,
1532}
1533
1534/// Supported texture formats.
1535#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1536#[repr(u8)]
1537pub enum TextureFormat {
1538    /// PNG format (lossless, with alpha channel).
1539    #[default]
1540    Png = 0,
1541    /// TGA format (lossless).
1542    Tga = 1,
1543    /// BC7/BPTC format (compressed, lossy).
1544    Bc7 = 2,
1545}
1546
1547impl TextureFormat {
1548    /// Try to create a `TextureFormat` from a byte.
1549    pub fn from_byte(b: u8) -> Option<Self> {
1550        match b {
1551            0 => Some(Self::Png),
1552            1 => Some(Self::Tga),
1553            2 => Some(Self::Bc7),
1554            _ => None,
1555        }
1556    }
1557
1558    /// Returns the file extension associated with this format.
1559    pub fn extension(&self) -> &'static str {
1560        match self {
1561            Self::Png => "png",
1562            Self::Tga => "tga",
1563            Self::Bc7 => "bc7",
1564        }
1565    }
1566
1567    /// Gets width and height of an image from its bytes,
1568    /// according to the texture format.
1569    pub fn get_img_dim(&self, data: &[u8]) -> std::io::Result<(u32, u32)> {
1570        match self {
1571            TextureFormat::Png => {
1572                // PNG header:
1573                // Width and height are in bytes 16–23 (big endian)
1574                if data.len() < 24 {
1575                    return Err(std::io::Error::new(
1576                        std::io::ErrorKind::InvalidData,
1577                        "Invalid PNG data (too short)",
1578                    ));
1579                }
1580
1581                let width = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
1582                let height = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
1583
1584                Ok((width, height))
1585            }
1586
1587            TextureFormat::Tga => {
1588                // TGA header:
1589                // Width in bytes 12–13 and height in 14–15 (little endian)
1590                if data.len() < 18 {
1591                    return Err(std::io::Error::new(
1592                        std::io::ErrorKind::InvalidData,
1593                        "Invalid TGA data (too short)",
1594                    ));
1595                }
1596
1597                let width = u16::from_le_bytes([data[12], data[13]]) as u32;
1598                let height = u16::from_le_bytes([data[14], data[15]]) as u32;
1599
1600                Ok((width, height))
1601            }
1602
1603            TextureFormat::Bc7 => {
1604                // BC7 has no standard header of its own.
1605                // Typically found inside DDS or KTX containers.
1606                // For now we return placeholder values.
1607                Ok((0, 0))
1608            }
1609        }
1610    }
1611}
1612
1613/// Internal storage of texture data.
1614#[derive(Debug, Clone)]
1615pub enum TextureData {
1616    /// Encoded data (PNG, TGA, BC7).
1617    Encoded(Vec<u8>),
1618    /// Decoded data in RGBA8 format.
1619    Rgba(Vec<u8>),
1620}
1621
1622/// A texture used by the puppet.
1623#[derive(Debug, Clone)]
1624pub struct Texture {
1625    /// Unique ID (index inside the puppet texture array).
1626    pub id: u32,
1627    /// Width in pixels.
1628    pub width: u32,
1629    /// Height in pixels.
1630    pub height: u32,
1631    /// Texture data format.
1632    pub format: TextureFormat,
1633    /// Texture data.
1634    pub data: TextureData,
1635}
1636
1637impl Texture {
1638    /// Creates a new texture with the given parameters.
1639    pub fn new(id: u32, width: u32, height: u32, format: TextureFormat, data: Vec<u8>) -> Self {
1640        Self {
1641            id,
1642            width,
1643            height,
1644            format,
1645            data: TextureData::Encoded(data),
1646        }
1647    }
1648
1649    /// Try to compute the texture dimensions from
1650    /// the encoded data and the format.
1651    pub fn dimensions_from_data(&self) -> std::io::Result<(u32, u32)> {
1652        match &self.data {
1653            TextureData::Encoded(bytes) => self.format.get_img_dim(bytes),
1654            TextureData::Rgba(_) => {
1655                // If data is already decoded, use
1656                // the stored dimensions
1657                Ok((self.width, self.height))
1658            }
1659        }
1660    }
1661}
1662
1663#[inline]
1664fn lerp(a: f32, b: f32, t: f32) -> f32 {
1665    a + (b - a) * t
1666}
1667
1668#[inline]
1669fn cubic_interpolate(a: f32, b: f32, t: f32, tension: f32) -> f32 {
1670    // Hermite with adjustable tension
1671    let t2 = t * t;
1672    let t3 = t2 * t;
1673
1674    // Tension 0.5 = standard Catmull-Rom
1675    let m = (1.0 - tension) * (b - a);
1676
1677    let h1 = 2.0 * t3 - 3.0 * t2 + 1.0;
1678    let h2 = t3 - 2.0 * t2 + t;
1679    let h3 = -2.0 * t3 + 3.0 * t2;
1680    let h4 = t3 - t2;
1681
1682    h1 * a + h2 * m + h3 * b + h4 * m
1683}