aada 0.1.1

纯 Rust 的 2D 骨骼动画引擎,兼容 Spine 4.x 导出格式(JSON + atlas)。求解到分层 Mesh 与各 Mesh 渲染设置为止,之后交给渲染器。
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
//! 约束:IK 与 transform 约束的定义、每帧状态与应用。
//!
//! [`ConstraintSet`] 为骨架级静态定义(含按 `order` 排序的应用顺序),每帧由
//! [`ConstraintSet::new_state`] 以骨架默认值构建 [`ConstraintState`],动画时间轴覆盖后,
//! [`apply_constraints`] 按 order 全局应用到 `Pose`。算法移植自 spine 4.2 官方运行时。

use glam::Vec2;

use crate::{
    cacl::affine::decompose_affine,
    data::runtime::skeleton::{BoneIdx, Pose, Skeleton},
};

/// IK 约束(静态数据)
#[derive(Debug, Clone)]
pub struct IkConstraintData {
    pub bones: Vec<BoneIdx>,
    pub target: BoneIdx,
    /// 骨架默认混合强度(0-1),动画时间轴可覆盖
    pub mix: f32,
    pub bend_positive: bool,
    pub compress: bool,
    pub stretch: bool,
    pub order: u32,
}

/// transform 约束(静态数据)
#[derive(Debug, Clone)]
pub struct TransformConstraintData {
    pub bones: Vec<BoneIdx>,
    pub target: BoneIdx,
    pub mix_rotate: f32,
    pub mix_x: f32,
    pub mix_y: f32,
    pub mix_scale_x: f32,
    pub mix_scale_y: f32,
    pub mix_shear_y: f32,
    pub offset_rotation: f32,
    pub offset_x: f32,
    pub offset_y: f32,
    pub offset_scale_x: f32,
    pub offset_scale_y: f32,
    pub offset_shear_y: f32,
    pub order: u32,
}

/// 约束集合中的条目
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintIdx {
    Ik(u32),
    Transform(u32),
}

/// 约束集合(静态不变):按 order 全局排序应用
#[derive(Debug, Clone, Default)]
pub struct ConstraintSet {
    pub ik: Vec<IkConstraintData>,
    pub transform: Vec<TransformConstraintData>,
    pub order: Vec<ConstraintIdx>,
}

impl ConstraintSet {
    /// 以骨架默认值构建每帧约束状态
    pub fn new_state(&self) -> ConstraintState {
        ConstraintState {
            ik: self
                .ik
                .iter()
                .map(|c| IkState {
                    mix: c.mix,
                    bend_positive: c.bend_positive,
                    compress: c.compress,
                    stretch: c.stretch,
                })
                .collect(),
            transform: self
                .transform
                .iter()
                .map(|c| TransformState {
                    mix_rotate: c.mix_rotate,
                    mix_x: c.mix_x,
                    mix_y: c.mix_y,
                    mix_scale_x: c.mix_scale_x,
                    mix_scale_y: c.mix_scale_y,
                    mix_shear_y: c.mix_shear_y,
                })
                .collect(),
        }
    }
}

/// IK 约束每帧状态(动画时间轴覆盖后的值)
#[derive(Debug, Clone, Copy, Default)]
pub struct IkState {
    pub mix: f32,
    pub bend_positive: bool,
    pub compress: bool,
    pub stretch: bool,
}

/// transform 约束每帧状态
#[derive(Debug, Clone, Copy, Default)]
pub struct TransformState {
    pub mix_rotate: f32,
    pub mix_x: f32,
    pub mix_y: f32,
    pub mix_scale_x: f32,
    pub mix_scale_y: f32,
    pub mix_shear_y: f32,
}

/// 每帧约束状态
#[derive(Debug, Clone, Default)]
pub struct ConstraintState {
    pub ik: Vec<IkState>,
    pub transform: Vec<TransformState>,
}

/// 按 order 全局顺序应用所有约束(在动画应用之后、网格变形之前调用)
pub fn apply_constraints(
    pose: &mut Pose,
    skeleton: &Skeleton,
    set: &ConstraintSet,
    state: &ConstraintState,
) {
    for c in &set.order {
        match c {
            ConstraintIdx::Ik(i) => {
                apply_ik(pose, skeleton, &set.ik[*i as usize], &state.ik[*i as usize])
            }
            ConstraintIdx::Transform(i) => apply_transform(
                pose,
                skeleton,
                &set.transform[*i as usize],
                &state.transform[*i as usize],
            ),
        }
    }
}

/// 角度差归一化到 [-180, 180]
fn wrap_deg(a: f32) -> f32 {
    let mut a = a % 360.0;
    if a > 180.0 {
        a -= 360.0;
    } else if a < -180.0 {
        a += 360.0;
    }
    a
}

fn apply_ik(pose: &mut Pose, skeleton: &Skeleton, ik: &IkConstraintData, state: &IkState) {
    let alpha = state.mix;
    if alpha <= 0.0 {
        return;
    }
    let target_pos = pose.get_world_affine(skeleton, ik.target).translation;
    let bend_dir = if state.bend_positive { 1.0f32 } else { -1.0f32 };
    match ik.bones.len() {
        1 => apply_ik_1(
            pose,
            skeleton,
            ik.bones[0],
            target_pos,
            state.compress,
            state.stretch,
            alpha,
        ),
        // spine 4.2 只支持 1 或 2 根骨骼,更多时忽略
        2 => apply_ik_2(
            pose,
            skeleton,
            ik.bones[0],
            ik.bones[1],
            target_pos,
            bend_dir,
            state.stretch,
            alpha,
        ),
        _ => {}
    }
}

/// 单骨骼 IK(移植 spine 4.2 IkConstraint.apply(bone, ...)):
/// target 转到骨骼父空间,骨骼局部旋转指向它(混合 alpha)
fn apply_ik_1(
    pose: &mut Pose,
    skeleton: &Skeleton,
    bone: BoneIdx,
    target_pos: Vec2,
    compress: bool,
    stretch: bool,
    alpha: f32,
) {
    let b = pose.bone_data[bone].clone();
    let parent_world = match skeleton.data_static[bone].parent_index {
        Some(pi) => pose.get_world_affine(skeleton, pi),
        None => glam::Affine2::IDENTITY,
    };
    let pa = parent_world.matrix2.x_axis;
    let pb = parent_world.matrix2.y_axis;
    let p = pa.x * pb.y - pa.y * pb.x; // det
    let x = target_pos.x - parent_world.translation.x;
    let y = target_pos.y - parent_world.translation.y;
    // target 在父空间的坐标减去骨骼局部位置 = 骨骼局部 x 轴应指向的方向
    let (tx, ty) = if p.abs() <= 1e-4 {
        (0.0, 0.0)
    } else {
        (
            (x * pb.y - y * pb.x) / p - b.translate_x,
            (y * pa.x - x * pa.y) / p - b.translate_y,
        )
    };
    // rotationIK = -ashearX - arotation + atan2(ty, tx)
    let mut rotation_ik = -b.shear_x_deg - b.rotation_deg + ty.atan2(tx).to_degrees();
    if b.scale_x < 0.0 {
        rotation_ik += 180.0;
    }
    rotation_ik = wrap_deg(rotation_ik);
    let mut sx = b.scale_x;
    let sy = b.scale_y;
    if compress || stretch {
        // 目标距骨骼末端的距离相对骨骼长度
        let b_len = skeleton.data_static[bone].length * sx;
        if b_len > 1e-4 {
            let tx_w = target_pos.x - pose.get_world_affine(skeleton, bone).translation.x;
            let ty_w = target_pos.y - pose.get_world_affine(skeleton, bone).translation.y;
            let dd = tx_w * tx_w + ty_w * ty_w;
            if (compress && dd < b_len * b_len) || (stretch && dd > b_len * b_len) {
                let s = ((dd.sqrt() / b_len - 1.0) * alpha + 1.0).max(1e-4);
                sx *= s; // 官方 uniform 标志默认 false:只拉伸 x
            }
        }
    }
    let d = &mut pose.bone_data[bone];
    d.rotation_deg += rotation_ik * alpha;
    d.scale_x = sx;
    d.scale_y = sy;
    pose.invalidate_world(skeleton, bone);
}

/// 两骨骼 IK(移植 spine 4.2 IkConstraint.apply(parent, child, ...)):
/// 在 parent 的父骨骼(pp)坐标系中解三角形,末端指向 target。
/// 与官方一致:弯曲方向由 bend_dir 控制,混合只作用于旋转增量。
/// 参数与官方 apply 签名对应
#[allow(clippy::too_many_arguments)]
fn apply_ik_2(
    pose: &mut Pose,
    skeleton: &Skeleton,
    parent: BoneIdx,
    child: BoneIdx,
    target_pos: Vec2,
    bend_dir: f32,
    stretch: bool,
    alpha: f32,
) {
    use crate::data::runtime::skeleton::InheritMode;
    let parent_static = &skeleton.data_static[parent];
    let child_static = &skeleton.data_static[child];
    // 官方:inherit 非 normal 时直接返回
    if parent_static.inherit_mode != InheritMode::Normal
        || child_static.inherit_mode != InheritMode::Normal
    {
        return;
    }
    let p = pose.bone_data[parent].clone();
    let c = pose.bone_data[child].clone();
    let px = p.translate_x;
    let py = p.translate_y;
    let mut psx = p.scale_x;
    let mut psy = p.scale_y;
    let mut sx = psx;
    let sy = psy;
    let csx = c.scale_x;
    // 反射符号(scale 为负)
    let (os1, mut s2) = if psx < 0.0 {
        psx = -psx;
        (180.0f32, -1.0f32)
    } else {
        (0.0, 1.0)
    };
    if psy < 0.0 {
        psy = -psy;
        s2 = -s2;
    }
    let os2 = if csx < 0.0 { 180.0 } else { 0.0 };
    let cx = c.translate_x;
    let parent_world = pose.get_world_affine(skeleton, parent);
    let a = parent_world.matrix2.x_axis.x;
    let b_ = parent_world.matrix2.y_axis.x;
    let c_ = parent_world.matrix2.x_axis.y;
    let d_ = parent_world.matrix2.y_axis.y;
    let uniform = (psx - psy).abs() <= 1e-4;
    let cy;
    let (cwx, cwy) = if !uniform || stretch {
        cy = 0.0;
        (
            a * cx + parent_world.translation.x,
            c_ * cx + parent_world.translation.y,
        )
    } else {
        cy = c.translate_y;
        (
            a * cx + b_ * c.translate_y + parent_world.translation.x,
            c_ * cx + d_ * c.translate_y + parent_world.translation.y,
        )
    };
    // pp(parent 的父)矩阵
    let pp = match parent_static.parent_index {
        Some(pi) => pose.get_world_affine(skeleton, pi),
        None => glam::Affine2::IDENTITY,
    };
    let pa = pp.matrix2.x_axis.x;
    let pb = pp.matrix2.y_axis.x;
    let pc = pp.matrix2.x_axis.y;
    let pd = pp.matrix2.y_axis.y;
    let det = pa * pd - pb * pc;
    let id = if det.abs() <= 1e-4 { 0.0 } else { 1.0 / det };
    // child 原点相对 parent 原点(pp 空间)→ l1
    let x = cwx - pp.translation.x;
    let y = cwy - pp.translation.y;
    let dx = (x * pd - y * pb) * id - px;
    let dy = (y * pa - x * pc) * id - py;
    let l1 = (dx * dx + dy * dy).sqrt();
    let mut l2 = child_static.length * csx;
    if l1 < 1e-4 {
        // 退化:parent 当单骨骼 IK,child 复位
        apply_ik_1(pose, skeleton, parent, target_pos, false, stretch, alpha);
        let cd = &mut pose.bone_data[child];
        cd.rotation_deg = 0.0;
        pose.invalidate_world(skeleton, child);
        return;
    }
    // target 相对 parent 原点(pp 空间)
    let x = target_pos.x - pp.translation.x;
    let y = target_pos.y - pp.translation.y;
    let tx = (x * pd - y * pb) * id - px;
    let ty = (y * pa - x * pc) * id - py;
    let dd = tx * tx + ty * ty;
    let rad = std::f32::consts::PI;
    let (a1, a2, stretch_scale);
    if uniform {
        l2 *= psx;
        let cos = (dd - l1 * l1 - l2 * l2) / (2.0 * l1 * l2);
        if cos < -1.0 {
            a2 = rad * bend_dir;
        } else if cos > 1.0 {
            a2 = 0.0;
        } else {
            a2 = cos.acos() * bend_dir;
        }
        let a = l1 + l2 * a2.cos();
        let b = l2 * a2.sin();
        a1 = (ty * a - tx * b).atan2(tx * a + ty * b);
        stretch_scale = if stretch && cos > 1.0 {
            (dd.sqrt() / (l1 + l2) - 1.0) * alpha + 1.0
        } else {
            1.0
        };
    } else {
        // parent 局部非均匀 scale:官方二次方程分支
        let a = psx * l2;
        let b = psy * l2;
        let aa = a * a;
        let bb = b * b;
        let ta = ty.atan2(tx);
        let c = bb * l1 * l1 + aa * dd - aa * bb;
        let c1 = -2.0 * bb * l1;
        let c2 = bb - aa;
        let d = c1 * c1 - 4.0 * c2 * c;
        let (mut a1v, mut a2v, mut solved) = (0.0f32, 0.0f32, false);
        if d >= 0.0 {
            let mut q = d.sqrt();
            if c1 < 0.0 {
                q = -q;
            }
            q = -(c1 + q) * 0.5;
            let r0 = q / c2;
            let r1 = c / q;
            let r = if r0.abs() < r1.abs() { r0 } else { r1 };
            let r0 = dd - r * r;
            if r0 >= 0.0 {
                let y = r0.sqrt() * bend_dir;
                a1v = ta - y.atan2(r);
                a2v = (y / psy).atan2((r - l1) / psx);
                solved = true;
            }
        }
        if !solved {
            // 退化:最接近 target 的边界解
            let mut min_angle = rad;
            let mut min_x = l1 - a;
            let mut min_dist = min_x * min_x;
            let mut min_y = 0.0;
            let mut max_angle = 0.0;
            let mut max_x = l1 + a;
            let mut max_dist = max_x * max_x;
            let mut max_y = 0.0;
            let cc = -a * l1 / (aa - bb);
            if (-1.0..=1.0).contains(&cc) {
                let cc = cc.acos();
                let xv = a * cc.cos() + l1;
                let yv = b * cc.sin();
                let d2 = xv * xv + yv * yv;
                if d2 < min_dist {
                    min_angle = cc;
                    min_dist = d2;
                    min_x = xv;
                    min_y = yv;
                }
                if d2 > max_dist {
                    max_angle = cc;
                    max_dist = d2;
                    max_x = xv;
                    max_y = yv;
                }
            }
            if dd <= (min_dist + max_dist) * 0.5 {
                a1v = ta - (min_y * bend_dir).atan2(min_x);
                a2v = min_angle * bend_dir;
            } else {
                a1v = ta - (max_y * bend_dir).atan2(max_x);
                a2v = max_angle * bend_dir;
            }
        }
        a1 = a1v;
        a2 = a2v;
        stretch_scale = 1.0;
    }
    // 写回(与官方一致:只改旋转,清 parent shear,scale 按需拉伸)
    let os = cy.atan2(cx) * s2;
    let mut a1_delta = (a1 - os).to_degrees() + os1 - p.rotation_deg;
    a1_delta = wrap_deg(a1_delta);
    if stretch_scale != 1.0 {
        sx *= stretch_scale; // 官方 uniform 标志默认 false:只拉伸 x
    }
    let pd_ = &mut pose.bone_data[parent];
    pd_.rotation_deg += a1_delta * alpha;
    pd_.scale_x = sx;
    pd_.scale_y = sy;
    pd_.shear_x_deg = 0.0;
    pd_.shear_y_deg = 0.0;
    let mut a2_delta = ((a2 + os).to_degrees() - c.shear_x_deg) * s2 + os2 - c.rotation_deg;
    a2_delta = wrap_deg(a2_delta);
    let cd = &mut pose.bone_data[child];
    cd.rotation_deg += a2_delta * alpha;
    pose.invalidate_world(skeleton, parent);
    pose.invalidate_world(skeleton, child);
}

/// transform 约束(world 绝对模式),移植 spine 4.2 TransformConstraint.applyAbsoluteWorld:
/// - mixRotate:骨骼世界矩阵整体旋转角度差(含 offset)
/// - mixX/Y:target 局部坐标 (offsetX, offsetY) 经 target 局部→世界变换后,骨骼世界位置向它混合
/// - mixScaleX/Y:scale 加法混合(目标 = target 世界 scale + offsetScale)
/// - mixShearY:骨骼 y 轴角向 (target 的 y 轴角 + offsetShearY) 混合
/// - 最后按官方 updateAppliedTransform 从世界矩阵回写局部参数
fn apply_transform(
    pose: &mut Pose,
    skeleton: &Skeleton,
    tc: &TransformConstraintData,
    state: &TransformState,
) {
    use crate::data::runtime::skeleton::InheritMode;
    if state.mix_rotate == 0.0
        && state.mix_x == 0.0
        && state.mix_y == 0.0
        && state.mix_scale_x == 0.0
        && state.mix_scale_y == 0.0
        && state.mix_shear_y == 0.0
    {
        return;
    }
    let target_world = pose.get_world_affine(skeleton, tc.target);
    let ta = target_world.matrix2.x_axis.x;
    let tb = target_world.matrix2.y_axis.x;
    let tc_ = target_world.matrix2.x_axis.y;
    let td = target_world.matrix2.y_axis.y;
    // 官方:target 带反射时 offset 旋转/剪切符号反转;offsetRotation/offsetShearY 进入弧度域
    let reflect = if ta * td - tb * tc_ > 0.0 {
        1.0f32
    } else {
        -1.0f32
    };
    let off_r = (tc.offset_rotation * reflect).to_radians();
    let off_sy = (tc.offset_shear_y * reflect).to_radians();

    for &bone in &tc.bones {
        let b = pose.bone_data[bone].clone();
        let mut w = pose.get_world_affine(skeleton, bone);
        // mixRotate:世界矩阵整体旋转(增量)
        if state.mix_rotate != 0.0 {
            let a = w.matrix2.x_axis.x;
            let c = w.matrix2.x_axis.y;
            let mut r = off_r + tc_.atan2(ta) - c.atan2(a);
            wrap_rad(&mut r);
            r *= state.mix_rotate;
            let (cos, sin) = (r.cos(), r.sin());
            let xa = w.matrix2.x_axis;
            let ya = w.matrix2.y_axis;
            w.matrix2.x_axis = Vec2::new(cos * xa.x - sin * xa.y, sin * xa.x + cos * xa.y);
            w.matrix2.y_axis = Vec2::new(cos * ya.x - sin * ya.y, sin * ya.x + cos * ya.y);
        }
        // mixX/Y:target 局部 offset → 世界
        if state.mix_x != 0.0 || state.mix_y != 0.0 {
            let off_world = target_world.transform_point2(Vec2::new(tc.offset_x, tc.offset_y));
            w.translation.x += (off_world.x - w.translation.x) * state.mix_x;
            w.translation.y += (off_world.y - w.translation.y) * state.mix_y;
        }
        // mixScaleX/Y:加法混合
        if state.mix_scale_x != 0.0 {
            let s = w.matrix2.x_axis.length();
            if s != 0.0 {
                let ts = Vec2::new(ta, tc_).length();
                let ns = (s + (ts - s + tc.offset_scale_x) * state.mix_scale_x) / s;
                w.matrix2.x_axis *= ns;
            }
        }
        if state.mix_scale_y != 0.0 {
            let s = w.matrix2.y_axis.length();
            if s != 0.0 {
                let ts = Vec2::new(tb, td).length();
                let ns = (s + (ts - s + tc.offset_scale_y) * state.mix_scale_y) / s;
                w.matrix2.y_axis *= ns;
            }
        }
        // mixShearY:y 轴角混合(官方判定 mixShearY > 0 才应用)
        if state.mix_shear_y > 0.0 {
            let by = w.matrix2.y_axis.y.atan2(w.matrix2.y_axis.x);
            let mut r =
                td.atan2(tb) - tc_.atan2(ta) - (by - w.matrix2.x_axis.y.atan2(w.matrix2.x_axis.x));
            wrap_rad(&mut r);
            r = by + (r + off_sy) * state.mix_shear_y;
            let s = w.matrix2.y_axis.length();
            w.matrix2.y_axis = Vec2::new(r.cos() * s, r.sin() * s);
        }
        // 官方 updateAppliedTransform:世界矩阵 → 局部参数写回
        let parent_world = match skeleton.data_static[bone].parent_index {
            Some(pi) => pose.get_world_affine(skeleton, pi),
            None => glam::Affine2::IDENTITY,
        };
        let d = &mut pose.bone_data[bone];
        if b.inherit_mode != InheritMode::Normal {
            // 非 normal 继承:官方 updateAppliedTransform 对父矩阵做相同处理,这里退化为直接分解局部
            // (Aya 全部 normal;非 normal 骨骼不受 transform 约束)
            let local = parent_world.inverse() * w;
            let (lx, ly, lr, lsx, lsy, _, lshy) = decompose_affine(&local);
            d.translate_x = lx;
            d.translate_y = ly;
            d.rotation_deg = lr;
            d.scale_x = lsx;
            d.scale_y = lsy;
            d.shear_x_deg = 0.0;
            d.shear_y_deg = lshy;
            pose.invalidate_world(skeleton, bone);
            continue;
        }
        let pa = parent_world.matrix2.x_axis.x;
        let pb = parent_world.matrix2.y_axis.x;
        let pc = parent_world.matrix2.x_axis.y;
        let pd = parent_world.matrix2.y_axis.y;
        let det = pa * pd - pb * pc;
        if det.abs() <= 1e-4 {
            continue;
        }
        let pid = 1.0 / det;
        let dx = w.translation.x - parent_world.translation.x;
        let dy = w.translation.y - parent_world.translation.y;
        let lx = (dx * pd - dy * pb) * pid;
        let ly = (dy * pa - dx * pc) * pid;
        let a = w.matrix2.x_axis.x;
        let b_ = w.matrix2.y_axis.x;
        let c_ = w.matrix2.x_axis.y;
        let dd_ = w.matrix2.y_axis.y;
        let ia = pid * pd;
        let ib = pid * pb;
        let ic = pid * pc;
        let id = pid * pa;
        let ra = ia * a - ib * c_;
        let rb = ia * b_ - ib * dd_;
        let rc = id * c_ - ic * a;
        let rd = id * dd_ - ic * b_;
        d.translate_x = lx;
        d.translate_y = ly;
        d.rotation_deg = rc.atan2(ra).to_degrees();
        let lsx = (ra * ra + rc * rc).sqrt();
        if lsx > 1e-4 {
            let det_l = ra * rd - rb * rc;
            d.scale_x = lsx;
            d.scale_y = det_l / lsx;
            d.shear_x_deg = 0.0;
            d.shear_y_deg = -(ra * rb + rc * rd).atan2(det_l).to_degrees();
        } else {
            d.scale_x = 0.0;
            d.scale_y = (rb * rb + rd * rd).sqrt();
            d.shear_x_deg = 0.0;
            d.shear_y_deg = 0.0;
        }
        pose.invalidate_world(skeleton, bone);
    }
}

/// 弧度角归一化到 (-π, π]
fn wrap_rad(a: &mut f32) {
    let mut a_ = *a % (std::f32::consts::PI * 2.0);
    if a_ > std::f32::consts::PI {
        a_ -= std::f32::consts::PI * 2.0;
    } else if a_ < -std::f32::consts::PI {
        a_ += std::f32::consts::PI * 2.0;
    }
    *a = a_;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        data::runtime::skeleton::{BoneDataStatic, Skeleton},
        utils::default,
    };

    /// 骨架:root -> (p, c1, c2),p 和 c1 组成 IK 链
    fn build_ik_skeleton() -> Skeleton {
        let mut s = Skeleton::new();
        s.insert_bone("root", default()).unwrap();
        let p = BoneDataStatic {
            length: 100.0,
            parent_index: s.get_bone_index("root"),
            ..Default::default()
        };
        s.insert_bone("p", p).unwrap();
        let c = BoneDataStatic {
            length: 80.0,
            // spine 约定:child 骨骼原点在 parent 骨骼末端
            translate_x: 100.0,
            parent_index: s.get_bone_index("p"),
            ..Default::default()
        };
        s.insert_bone("c", c).unwrap();
        let t = BoneDataStatic {
            parent_index: s.get_bone_index("root"),
            ..Default::default()
        };
        s.insert_bone("t", t).unwrap();
        s.build_children_map();
        s.build_roots();
        s
    }

    fn pose(skeleton: &Skeleton) -> Pose {
        skeleton.default_pose()
    }

    #[test]
    fn ik_two_bone_reaches_target() {
        let skeleton = build_ik_skeleton();
        let mut pose = pose(&skeleton);
        // target 在 (150, 60)(可达:|t| = 161.5 < 180)
        pose.bone_data[skeleton.get_bone_index("t").unwrap()].translate_x = 150.0;
        pose.bone_data[skeleton.get_bone_index("t").unwrap()].translate_y = 60.0;

        let ik = IkConstraintData {
            bones: vec![
                skeleton.get_bone_index("p").unwrap(),
                skeleton.get_bone_index("c").unwrap(),
            ],
            target: skeleton.get_bone_index("t").unwrap(),
            mix: 1.0,
            bend_positive: true,
            compress: false,
            stretch: false,
            order: 0,
        };
        let set = ConstraintSet {
            ik: vec![ik],
            transform: vec![],
            order: vec![ConstraintIdx::Ik(0)],
        };
        let state = set.new_state();
        apply_constraints(&mut pose, &skeleton, &set, &state);

        // 链末端(c 的骨骼末端)应接近 target
        let c_world = pose.get_world_affine(&skeleton, skeleton.get_bone_index("c").unwrap());
        let end = c_world.transform_point2(Vec2::new(80.0, 0.0));
        let target = pose.get_world_affine(&skeleton, skeleton.get_bone_index("t").unwrap());
        let d = (end - target.translation).length();
        assert!(
            d < 1.0,
            "IK 末端 {end:?} 应到达 target {:?},偏差 {d}",
            target.translation
        );
    }

    #[test]
    fn ik_mix_zero_no_effect() {
        let skeleton = build_ik_skeleton();
        let mut pose = pose(&skeleton);
        pose.bone_data[skeleton.get_bone_index("t").unwrap()].translate_x = 150.0;
        let before = pose.bone_data[skeleton.get_bone_index("p").unwrap()].rotation_deg;
        let ik = IkConstraintData {
            bones: vec![
                skeleton.get_bone_index("p").unwrap(),
                skeleton.get_bone_index("c").unwrap(),
            ],
            target: skeleton.get_bone_index("t").unwrap(),
            mix: 0.0,
            bend_positive: true,
            compress: false,
            stretch: false,
            order: 0,
        };
        let set = ConstraintSet {
            ik: vec![ik],
            transform: vec![],
            order: vec![ConstraintIdx::Ik(0)],
        };
        let state = set.new_state();
        apply_constraints(&mut pose, &skeleton, &set, &state);
        let after = pose.bone_data[skeleton.get_bone_index("p").unwrap()].rotation_deg;
        assert_eq!(before, after);
    }

    /// 参数化 IK:多个目标点(各象限)、不可达(完全伸直)、bend 两侧弯曲
    #[test]
    fn ik_reaches_various_targets() {
        let skeleton = build_ik_skeleton();
        let (pi, ci, ti) = (
            skeleton.get_bone_index("p").unwrap(),
            skeleton.get_bone_index("c").unwrap(),
            skeleton.get_bone_index("t").unwrap(),
        );
        // (target, bend_positive, 可达?, 弯曲侧判定:spine 语义 bend=+1 → parent 末端在 P->T 顺时针侧)
        let cases: [(f32, f32, bool, bool, bool); 5] = [
            (150.0, 60.0, true, true, false), // 右上方,bend+:M 在 P->T 顺时针侧
            (150.0, 60.0, true, false, true), // 同点,bend-:弯向另一侧
            (-120.0, -70.0, true, true, false), // 左下方(链可及 240 半径内)
            (60.0, -170.0, false, true, false), // d=180.3 刚超 l1+l2=180:不可达直链
            (300.0, 200.0, false, true, false), // 不可达:完全伸直指向 target
        ];
        for (tx, ty, reachable, bend_pos, expect_ccw) in cases {
            let mut pose = pose(&skeleton);
            pose.bone_data[ti].translate_x = tx;
            pose.bone_data[ti].translate_y = ty;
            let ik = IkConstraintData {
                bones: vec![pi, ci],
                target: ti,
                mix: 1.0,
                bend_positive: bend_pos,
                compress: false,
                stretch: false,
                order: 0,
            };
            let set = ConstraintSet {
                ik: vec![ik],
                transform: vec![],
                order: vec![ConstraintIdx::Ik(0)],
            };
            let state = set.new_state();
            apply_constraints(&mut pose, &skeleton, &set, &state);
            let c_world = pose.get_world_affine(&skeleton, ci);
            let end = c_world.transform_point2(Vec2::new(80.0, 0.0));
            let target = pose.get_world_affine(&skeleton, ti).translation;
            let d = (end - target).length();
            if reachable {
                assert!(
                    d < 1.0,
                    "target ({tx},{ty}) bend={bend_pos}: 末端 {end:?} 应到达 {target:?},偏差 {d}"
                );
                // 弯曲侧:parent 末端应偏向 P->T 的 bend 侧(叉积符号)
                let p_world = pose.get_world_affine(&skeleton, pi).translation;
                let m = pose
                    .get_world_affine(&skeleton, pi)
                    .transform_point2(Vec2::new(100.0, 0.0));
                let to_t = target - p_world;
                let to_m = m - p_world;
                let cross = to_t.x * to_m.y - to_t.y * to_m.x;
                let ccw = cross > 0.0;
                assert_eq!(
                    ccw, expect_ccw,
                    "target ({tx},{ty}) bend={bend_pos}: 弯曲侧不符 cross={cross}"
                );
            } else {
                // 不可达:完全伸直,末端在 target 方向、距链头 = l1+l2
                let from_p = end - pose.get_world_affine(&skeleton, pi).translation;
                let dist = from_p.length();
                assert!(
                    (dist - 180.0).abs() < 1.0,
                    "target ({tx},{ty}): 不可达应完全伸直,末端距 {dist}(期望 180)"
                );
                let dir_t = (target - pose.get_world_affine(&skeleton, pi).translation).normalize();
                let dir_e = from_p.normalize();
                assert!(
                    (dir_t - dir_e).length() < 1e-2,
                    "target ({tx},{ty}): 不可达方向应指向 target"
                );
            }
        }
    }

    /// 与 spine 4.2 官方运行时对照(手算精确值):
    /// target=(150,60)、l1=100、l2=80(dd=26100,cos=0.60625,a2=52.68°):
    /// bendDir=+1 → parent delta = -1.388°, child delta = +52.68°
    /// bendDir=-1 → parent delta = +44.99°, child delta = -52.68°
    #[test]
    fn ik_matches_official_runtime_values() {
        let skeleton = build_ik_skeleton();
        let (pi, ci, ti) = (
            skeleton.get_bone_index("p").unwrap(),
            skeleton.get_bone_index("c").unwrap(),
            skeleton.get_bone_index("t").unwrap(),
        );
        for (bend_pos, exp_p, exp_c) in [(true, -1.388, 52.68), (false, 44.99, -52.68)] {
            let mut pose = pose(&skeleton);
            pose.bone_data[ti].translate_x = 150.0;
            pose.bone_data[ti].translate_y = 60.0;
            let ik = IkConstraintData {
                bones: vec![pi, ci],
                target: ti,
                mix: 1.0,
                bend_positive: bend_pos,
                compress: false,
                stretch: false,
                order: 0,
            };
            let set = ConstraintSet {
                ik: vec![ik],
                transform: vec![],
                order: vec![ConstraintIdx::Ik(0)],
            };
            let state = set.new_state();
            apply_constraints(&mut pose, &skeleton, &set, &state);
            let rp = pose.bone_data[pi].rotation_deg;
            let rc = pose.bone_data[ci].rotation_deg;
            assert!(
                (rp - exp_p).abs() < 0.02 && (rc - exp_c).abs() < 0.02,
                "bend_positive={bend_pos}: parent={rp} (期望 {exp_p}), child={rc} (期望 {exp_c})"
            );
        }
    }

    /// 骨架:root -> (target, bone)
    #[test]
    fn transform_follows_target_rotation() {
        let mut skeleton = Skeleton::new();
        skeleton.insert_bone("root", default()).unwrap();
        let target = BoneDataStatic {
            parent_index: skeleton.get_bone_index("root"),
            ..Default::default()
        };
        skeleton.insert_bone("target", target).unwrap();
        let bone = BoneDataStatic {
            parent_index: skeleton.get_bone_index("root"),
            rotation_deg: 10.0,
            ..Default::default()
        };
        skeleton.insert_bone("bone", bone).unwrap();
        skeleton.build_children_map();
        skeleton.build_roots();

        let mut pose = skeleton.default_pose();
        // target 旋转 30°
        pose.bone_data[skeleton.get_bone_index("target").unwrap()].rotation_deg = 30.0;

        let tc = TransformConstraintData {
            bones: vec![skeleton.get_bone_index("bone").unwrap()],
            target: skeleton.get_bone_index("target").unwrap(),
            mix_rotate: 1.0,
            mix_x: 0.0,
            mix_y: 0.0,
            mix_scale_x: 0.0,
            mix_scale_y: 0.0,
            mix_shear_y: 0.0,
            offset_rotation: 0.0,
            offset_x: 0.0,
            offset_y: 0.0,
            offset_scale_x: 1.0,
            offset_scale_y: 1.0,
            offset_shear_y: 0.0,
            order: 0,
        };
        let set = ConstraintSet {
            ik: vec![],
            transform: vec![tc],
            order: vec![ConstraintIdx::Transform(0)],
        };
        let state = set.new_state();
        apply_constraints(&mut pose, &skeleton, &set, &state);
        let r = pose.bone_data[skeleton.get_bone_index("bone").unwrap()].rotation_deg;
        assert!((r - 30.0).abs() < 0.01, "bone 旋转应跟随 target: {r}");
    }
}