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