distark-render 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,13 +58,12 @@ export class CharacterRigRenderer {
58
58
  const jointOffset = rigData.jointOffset || {};
59
59
  const zIndexValues = rigData.zIndexValues || {};
60
60
  const eyes = rigData.eyes || {};
61
- const hiddenValues = rigData.hiddenValues || {}; // true = hidden
61
+ const visibility = rigData.visibility || {}; // Default: all visible if not specified
62
62
  const flipX = rigData.flipX || false; // No flip by default
63
63
  const flipHead = rigData.flipHead || false; // No head flip by default
64
64
  const imageScale = rigData.imageScale ?? 1.0; // Use imageScale from rigData, default to 1.0
65
65
  // Create array of all limbs/components with their computed transforms
66
66
  const allObjects = [];
67
- const isVisible = (partName) => hiddenValues[partName] !== true;
68
67
  // Root transform (character position in world space)
69
68
  const rootTransform = {
70
69
  x: centerX,
@@ -73,35 +72,58 @@ export class CharacterRigRenderer {
73
72
  scaleX: flipX ? -1 : 1,
74
73
  scaleY: 1
75
74
  };
75
+ // Torso pivot transform: allows the torso (and all children) to rotate/offset
76
+ // around a configurable pivot point (e.g. hip/waist area).
77
+ // root_torso defines where in root-local space the pivot lives.
78
+ // rotationValues["torso"] is the rotation angle applied around that pivot.
79
+ const root_torso = pivotPoints['root_torso'] || { x: 0, y: 0 };
80
+ const torsoPivotRot = rotations["torso"] || 0;
81
+ const torsoTransform = this.applyTransform(rootTransform, {
82
+ x: root_torso.x || 0,
83
+ y: root_torso.y || 0,
84
+ rotation: torsoPivotRot,
85
+ scaleX: 1,
86
+ scaleY: 1
87
+ });
76
88
  // Torso
77
- if (isVisible('torso')) {
89
+ if (visibility.torso !== false) {
78
90
  const torsoWidth = dimensions.torso?.width || 60;
79
91
  const torsoHeight = dimensions.torso?.height || 120;
80
92
  const selfRot = selfRotations["torso"] || 0;
81
93
  // CRITICAL FIX: Look up image by ACTUAL PATH from rigData, not semantic key!
82
94
  const torsoImagePath = rigData.imagePaths?.torso;
83
95
  const torsoImage = torsoImagePath ? loadedImages[torsoImagePath] : undefined;
96
+ // The torso image bottom-center is anchored at torsoTransform (= root_torso
97
+ // world position). Moving root_torso moves the torso image; rotating
98
+ // rotationValues["torso"] rotates the torso around that anchor point.
99
+ const torsoJointOffset = jointOffset['root_torso'] || { x: 0, y: 0 };
100
+ const torsoImageTransform = this.applyTransform(torsoTransform, {
101
+ x: torsoJointOffset.x || 0,
102
+ y: torsoJointOffset.y || 0,
103
+ rotation: selfRot,
104
+ scaleX: 1,
105
+ scaleY: 1
106
+ });
84
107
  allObjects.push({
85
108
  name: 'torso',
86
109
  type: 'limb',
87
110
  zIndex: zIndexValues['torso'] || 1,
88
111
  width: torsoWidth * imageScale,
89
112
  height: torsoHeight * imageScale,
90
- x: rootTransform.x,
91
- y: rootTransform.y,
92
- rotation: rootTransform.rotation + selfRot,
93
- scaleX: rootTransform.scaleX,
94
- scaleY: rootTransform.scaleY,
95
- // Anchor point (where the image is drawn from - top center for torso)
113
+ x: torsoImageTransform.x,
114
+ y: torsoImageTransform.y,
115
+ rotation: torsoImageTransform.rotation,
116
+ scaleX: torsoImageTransform.scaleX,
117
+ scaleY: torsoImageTransform.scaleY,
96
118
  anchorX: 0.5,
97
- anchorY: 1,
119
+ anchorY: 1, // bottom-center sits at root_torso world position
98
120
  imageKey: 'imagePaths.torso',
99
121
  imageData: torsoImage,
100
122
  selfRotation: selfRot
101
123
  });
102
124
  }
103
125
  // Head
104
- if (isVisible('head')) {
126
+ if (visibility.head !== false) {
105
127
  const connectionKey = `torso_head`;
106
128
  const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
107
129
  const headJointOffset = jointOffset[connectionKey] || { x: 0, y: 0 };
@@ -109,10 +131,10 @@ export class CharacterRigRenderer {
109
131
  const headHeight = dimensions.head?.height || 80;
110
132
  const headRot = rotations["head"] || 0;
111
133
  const selfRot = selfRotations["head"] || 0;
112
- // Calculate head transform
113
- const headParentTransform = this.applyTransform(rootTransform, {
114
- x: pivotPoint.x || 0,
115
- y: pivotPoint.y || 0,
134
+ // Calculate head transform — pivot is expressed in root space; convert to torso-local.
135
+ const headParentTransform = this.applyTransform(torsoTransform, {
136
+ x: (pivotPoint.x || 0) - (root_torso.x || 0),
137
+ y: (pivotPoint.y || 0) - (root_torso.y || 0),
116
138
  rotation: headRot,
117
139
  scaleX: flipHead ? -1 : 1,
118
140
  scaleY: 1
@@ -147,7 +169,7 @@ export class CharacterRigRenderer {
147
169
  });
148
170
  }
149
171
  // Mouth (child of head)
150
- if (isVisible('mouth')) {
172
+ if (visibility.mouth !== false) {
151
173
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
152
174
  const selfRot = selfRotations["mouth"] || 0;
153
175
  const headRot = rotations["head"] || 0;
@@ -183,9 +205,9 @@ export class CharacterRigRenderer {
183
205
  // Calculate final dimensions with size scaling
184
206
  const finalMouthWidth = baseMouthWidth * mouthSizeScale;
185
207
  const finalMouthHeight = baseMouthHeight * mouthSizeScale;
186
- const headBaseTransform = this.applyTransform(rootTransform, {
187
- x: headPivot.x || 0,
188
- y: headPivot.y || 0,
208
+ const headBaseTransform = this.applyTransform(torsoTransform, {
209
+ x: (headPivot.x || 0) - (root_torso.x || 0),
210
+ y: (headPivot.y || 0) - (root_torso.y || 0),
189
211
  rotation: headRot,
190
212
  scaleX: flipHead ? -1 : 1,
191
213
  scaleY: 1
@@ -237,12 +259,12 @@ export class CharacterRigRenderer {
237
259
  // CRITICAL FIX: Look up image by ACTUAL PATH from rigData.eyes
238
260
  const leftEyeImagePath = eyes?.leftEyeImage;
239
261
  const leftEyeImage = leftEyeImagePath ? loadedImages[leftEyeImagePath] : undefined;
240
- if (isVisible('leftEye') && leftEyeImage && eyes) {
262
+ if (visibility.leftEye !== false && leftEyeImage && eyes) {
241
263
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
242
264
  const headRot = rotations["head"] || 0;
243
- const headBaseTransform = this.applyTransform(rootTransform, {
244
- x: headPivot.x || 0,
245
- y: headPivot.y || 0,
265
+ const headBaseTransform = this.applyTransform(torsoTransform, {
266
+ x: (headPivot.x || 0) - (root_torso.x || 0),
267
+ y: (headPivot.y || 0) - (root_torso.y || 0),
246
268
  rotation: headRot,
247
269
  scaleX: flipHead ? -1 : 1,
248
270
  scaleY: 1
@@ -280,12 +302,12 @@ export class CharacterRigRenderer {
280
302
  // Right Eye
281
303
  const rightEyeImagePath = eyes?.rightEyeImage;
282
304
  const rightEyeImage = getImage(rightEyeImagePath);
283
- if (isVisible('rightEye') && rightEyeImage && eyes) {
305
+ if (visibility.rightEye !== false && rightEyeImage && eyes) {
284
306
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
285
307
  const headRot = rotations["head"] || 0;
286
- const headBaseTransform = this.applyTransform(rootTransform, {
287
- x: headPivot.x || 0,
288
- y: headPivot.y || 0,
308
+ const headBaseTransform = this.applyTransform(torsoTransform, {
309
+ x: (headPivot.x || 0) - (root_torso.x || 0),
310
+ y: (headPivot.y || 0) - (root_torso.y || 0),
289
311
  rotation: headRot,
290
312
  scaleX: flipHead ? -1 : 1,
291
313
  scaleY: 1
@@ -323,12 +345,12 @@ export class CharacterRigRenderer {
323
345
  // Left Iris
324
346
  const leftIrisImagePath = eyes?.leftIris;
325
347
  const leftIrisImage = getImage(leftIrisImagePath);
326
- if (isVisible('leftIris') && leftIrisImage && eyes) {
348
+ if (visibility.leftIris !== false && leftIrisImage && eyes) {
327
349
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
328
350
  const headRot = rotations["head"] || 0;
329
- const headBaseTransform = this.applyTransform(rootTransform, {
330
- x: headPivot.x || 0,
331
- y: headPivot.y || 0,
351
+ const headBaseTransform = this.applyTransform(torsoTransform, {
352
+ x: (headPivot.x || 0) - (root_torso.x || 0),
353
+ y: (headPivot.y || 0) - (root_torso.y || 0),
332
354
  rotation: headRot,
333
355
  scaleX: flipHead ? -1 : 1,
334
356
  scaleY: 1
@@ -368,12 +390,12 @@ export class CharacterRigRenderer {
368
390
  // Right Iris
369
391
  const rightIrisImagePath = eyes?.rightIris;
370
392
  const rightIrisImage = getImage(rightIrisImagePath);
371
- if (isVisible('rightIris') && rightIrisImage && eyes) {
393
+ if (visibility.rightIris !== false && rightIrisImage && eyes) {
372
394
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
373
395
  const headRot = rotations["head"] || 0;
374
- const headBaseTransform = this.applyTransform(rootTransform, {
375
- x: headPivot.x || 0,
376
- y: headPivot.y || 0,
396
+ const headBaseTransform = this.applyTransform(torsoTransform, {
397
+ x: (headPivot.x || 0) - (root_torso.x || 0),
398
+ y: (headPivot.y || 0) - (root_torso.y || 0),
377
399
  rotation: headRot,
378
400
  scaleX: flipHead ? -1 : 1,
379
401
  scaleY: 1
@@ -429,12 +451,12 @@ export class CharacterRigRenderer {
429
451
  getImage(eyes?.leftEyeLidHalfClosed) ||
430
452
  getImage(eyes?.leftEyeLidClosed);
431
453
  }
432
- if (isVisible('leftEyeLid') && leftEyeLidImage && eyes) {
454
+ if (visibility.leftEyeLid !== false && leftEyeLidImage && eyes) {
433
455
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
434
456
  const headRot = rotations["head"] || 0;
435
- const headBaseTransform = this.applyTransform(rootTransform, {
436
- x: headPivot.x || 0,
437
- y: headPivot.y || 0,
457
+ const headBaseTransform = this.applyTransform(torsoTransform, {
458
+ x: (headPivot.x || 0) - (root_torso.x || 0),
459
+ y: (headPivot.y || 0) - (root_torso.y || 0),
438
460
  rotation: headRot,
439
461
  scaleX: flipHead ? -1 : 1,
440
462
  scaleY: 1
@@ -487,12 +509,12 @@ export class CharacterRigRenderer {
487
509
  getImage(eyes?.rightEyeLidHalfClosed) ||
488
510
  getImage(eyes?.rightEyeLidClosed);
489
511
  }
490
- if (isVisible('rightEyeLid') && rightEyeLidImage && eyes) {
512
+ if (visibility.rightEyeLid !== false && rightEyeLidImage && eyes) {
491
513
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
492
514
  const headRot = rotations["head"] || 0;
493
- const headBaseTransform = this.applyTransform(rootTransform, {
494
- x: headPivot.x || 0,
495
- y: headPivot.y || 0,
515
+ const headBaseTransform = this.applyTransform(torsoTransform, {
516
+ x: (headPivot.x || 0) - (root_torso.x || 0),
517
+ y: (headPivot.y || 0) - (root_torso.y || 0),
496
518
  rotation: headRot,
497
519
  scaleX: flipHead ? -1 : 1,
498
520
  scaleY: 1
@@ -529,7 +551,7 @@ export class CharacterRigRenderer {
529
551
  }
530
552
  // Left Arm chain (leftUpperArm, leftForearm, leftHand)
531
553
  // Left Upper Arm
532
- if (isVisible('leftUpperArm')) {
554
+ if (visibility.leftUpperArm !== false) {
533
555
  const connectionKey = `torso_leftUpperArm`;
534
556
  const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
535
557
  const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
@@ -537,9 +559,9 @@ export class CharacterRigRenderer {
537
559
  const height = dimensions.leftUpperArm?.height || 50;
538
560
  const limbRot = rotations['leftUpperArm'] || 0;
539
561
  const selfRot = selfRotations['leftUpperArm'] || 0;
540
- const upperArmParentTransform = this.applyTransform(rootTransform, {
541
- x: pivotPoint.x || 0,
542
- y: pivotPoint.y || 0,
562
+ const upperArmParentTransform = this.applyTransform(torsoTransform, {
563
+ x: (pivotPoint.x || 0) - (root_torso.x || 0),
564
+ y: (pivotPoint.y || 0) - (root_torso.y || 0),
543
565
  rotation: limbRot,
544
566
  scaleX: 1,
545
567
  scaleY: 1
@@ -571,7 +593,7 @@ export class CharacterRigRenderer {
571
593
  });
572
594
  }
573
595
  // Left Forearm
574
- if (isVisible('leftForearm')) {
596
+ if (visibility.leftForearm !== false) {
575
597
  const upperConnectionKey = `torso_leftUpperArm`;
576
598
  const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
577
599
  const upperHeight = dimensions.leftUpperArm?.height || 50;
@@ -583,9 +605,9 @@ export class CharacterRigRenderer {
583
605
  const height = dimensions.leftForearm?.height || 45;
584
606
  const limbRot = rotations["leftForearm"] || 0;
585
607
  const selfRot = selfRotations["leftForearm"] || 0;
586
- const upperArmBase = this.applyTransform(rootTransform, {
587
- x: upperPivot.x || 0,
588
- y: upperPivot.y || 0,
608
+ const upperArmBase = this.applyTransform(torsoTransform, {
609
+ x: (upperPivot.x || 0) - (root_torso.x || 0),
610
+ y: (upperPivot.y || 0) - (root_torso.y || 0),
589
611
  rotation: upperRot,
590
612
  scaleX: 1,
591
613
  scaleY: 1
@@ -624,7 +646,7 @@ export class CharacterRigRenderer {
624
646
  });
625
647
  }
626
648
  // Left Hand
627
- if (isVisible('leftHand')) {
649
+ if (visibility.leftHand !== false) {
628
650
  const upperConnectionKey = `torso_leftUpperArm`;
629
651
  const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
630
652
  const upperHeight = dimensions.leftUpperArm?.height || 50;
@@ -640,9 +662,9 @@ export class CharacterRigRenderer {
640
662
  const height = dimensions.leftHand?.height || 30;
641
663
  const handRot = rotations["leftHand"] || 0;
642
664
  const selfRot = selfRotations["leftHand"] || 0;
643
- const upperArmBase = this.applyTransform(rootTransform, {
644
- x: upperPivot.x || 0,
645
- y: upperPivot.y || 0,
665
+ const upperArmBase = this.applyTransform(torsoTransform, {
666
+ x: (upperPivot.x || 0) - (root_torso.x || 0),
667
+ y: (upperPivot.y || 0) - (root_torso.y || 0),
646
668
  rotation: upperRot,
647
669
  scaleX: 1,
648
670
  scaleY: 1
@@ -688,7 +710,7 @@ export class CharacterRigRenderer {
688
710
  }
689
711
  // Right Arm chain (rightUpperArm, rightForearm, rightHand)
690
712
  // Right Upper Arm
691
- if (isVisible('rightUpperArm')) {
713
+ if (visibility.rightUpperArm !== false) {
692
714
  const connectionKey = `torso_rightUpperArm`;
693
715
  const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
694
716
  const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
@@ -696,9 +718,9 @@ export class CharacterRigRenderer {
696
718
  const height = dimensions.rightUpperArm?.height || 50;
697
719
  const limbRot = rotations['rightUpperArm'] || 0;
698
720
  const selfRot = selfRotations['rightUpperArm'] || 0;
699
- const upperArmParentTransform = this.applyTransform(rootTransform, {
700
- x: pivotPoint.x || 0,
701
- y: pivotPoint.y || 0,
721
+ const upperArmParentTransform = this.applyTransform(torsoTransform, {
722
+ x: (pivotPoint.x || 0) - (root_torso.x || 0),
723
+ y: (pivotPoint.y || 0) - (root_torso.y || 0),
702
724
  rotation: limbRot,
703
725
  scaleX: 1,
704
726
  scaleY: 1
@@ -730,7 +752,7 @@ export class CharacterRigRenderer {
730
752
  });
731
753
  }
732
754
  // Right Forearm
733
- if (isVisible('rightForearm')) {
755
+ if (visibility.rightForearm !== false) {
734
756
  const upperConnectionKey = `torso_rightUpperArm`;
735
757
  const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
736
758
  const upperHeight = dimensions.rightUpperArm?.height || 50;
@@ -742,9 +764,9 @@ export class CharacterRigRenderer {
742
764
  const height = dimensions.rightForearm?.height || 45;
743
765
  const limbRot = rotations["rightForearm"] || 0;
744
766
  const selfRot = selfRotations["rightForearm"] || 0;
745
- const upperArmBase = this.applyTransform(rootTransform, {
746
- x: upperPivot.x || 0,
747
- y: upperPivot.y || 0,
767
+ const upperArmBase = this.applyTransform(torsoTransform, {
768
+ x: (upperPivot.x || 0) - (root_torso.x || 0),
769
+ y: (upperPivot.y || 0) - (root_torso.y || 0),
748
770
  rotation: upperRot,
749
771
  scaleX: 1,
750
772
  scaleY: 1
@@ -783,7 +805,7 @@ export class CharacterRigRenderer {
783
805
  });
784
806
  }
785
807
  // Right Hand
786
- if (isVisible('rightHand')) {
808
+ if (visibility.rightHand !== false) {
787
809
  const upperConnectionKey = `torso_rightUpperArm`;
788
810
  const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
789
811
  const upperHeight = dimensions.rightUpperArm?.height || 50;
@@ -799,9 +821,9 @@ export class CharacterRigRenderer {
799
821
  const height = dimensions.rightHand?.height || 30;
800
822
  const handRot = rotations["rightHand"] || 0;
801
823
  const selfRot = selfRotations["rightHand"] || 0;
802
- const upperArmBase = this.applyTransform(rootTransform, {
803
- x: upperPivot.x || 0,
804
- y: upperPivot.y || 0,
824
+ const upperArmBase = this.applyTransform(torsoTransform, {
825
+ x: (upperPivot.x || 0) - (root_torso.x || 0),
826
+ y: (upperPivot.y || 0) - (root_torso.y || 0),
805
827
  rotation: upperRot,
806
828
  scaleX: 1,
807
829
  scaleY: 1
@@ -847,7 +869,7 @@ export class CharacterRigRenderer {
847
869
  }
848
870
  // Left Leg chain (leftThigh, leftLeg)
849
871
  // Left Thigh
850
- if (isVisible('leftThigh')) {
872
+ if (visibility.leftThigh !== false) {
851
873
  const connectionKey = `torso_leftThigh`;
852
874
  const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
853
875
  const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
@@ -889,7 +911,7 @@ export class CharacterRigRenderer {
889
911
  });
890
912
  }
891
913
  // Left Leg
892
- if (isVisible('leftLeg')) {
914
+ if (visibility.leftLeg !== false) {
893
915
  const upperConnectionKey = `torso_leftThigh`;
894
916
  const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
895
917
  const upperHeight = dimensions.leftThigh?.height || 60;
@@ -942,7 +964,7 @@ export class CharacterRigRenderer {
942
964
  }
943
965
  // Right Leg chain (rightThigh, rightLeg)
944
966
  // Right Thigh
945
- if (isVisible('rightThigh')) {
967
+ if (visibility.rightThigh !== false) {
946
968
  const connectionKey = `torso_rightThigh`;
947
969
  const pivotPoint = pivotPoints[connectionKey] || { x: 0, y: 0 };
948
970
  const jointOff = jointOffset[connectionKey] || { x: 0, y: 0 };
@@ -984,7 +1006,7 @@ export class CharacterRigRenderer {
984
1006
  });
985
1007
  }
986
1008
  // Right Leg
987
- if (isVisible('rightLeg')) {
1009
+ if (visibility.rightLeg !== false) {
988
1010
  const upperConnectionKey = `torso_rightThigh`;
989
1011
  const upperPivot = pivotPoints[upperConnectionKey] || { x: 0, y: 0 };
990
1012
  const upperHeight = dimensions.rightThigh?.height || 60;
@@ -1037,58 +1059,49 @@ export class CharacterRigRenderer {
1037
1059
  }
1038
1060
  // Sort all objects by zIndex
1039
1061
  allObjects.sort((a, b) => a.zIndex - b.zIndex);
1040
- // Compute pivot points in world space
1062
+ // Compute pivot points in world space.
1063
+ // All torso-attached joints are resolved via torsoTransform so they correctly
1064
+ // follow any torso rotation applied around root_torso.
1041
1065
  const computedPivotPoints = [];
1042
- // Head/torso pivot
1043
- const torsoHead = pivotPoints['torso_head'] || { x: 0, y: 0 };
1044
- const headPivotWorld = this.applyTransform(rootTransform, {
1045
- x: torsoHead.x || 0,
1046
- y: torsoHead.y || 0,
1047
- rotation: 0,
1066
+ // Helper: convert a root-space pivot offset to torso-local space, then to world.
1067
+ const torsoLocalPivot = (px, py, rot) => this.applyTransform(torsoTransform, {
1068
+ x: px - (root_torso.x || 0),
1069
+ y: py - (root_torso.y || 0),
1070
+ rotation: rot,
1048
1071
  scaleX: 1,
1049
1072
  scaleY: 1
1050
1073
  });
1051
- computedPivotPoints.push({
1052
- name: 'torso_head',
1053
- x: headPivotWorld.x,
1054
- y: headPivotWorld.y
1055
- });
1056
- // Mouth pivot
1074
+ // root_torso pivot (the torso rotation origin in world space)
1075
+ computedPivotPoints.push({ name: 'root_torso', x: torsoTransform.x, y: torsoTransform.y });
1076
+ // Head/torso pivot
1077
+ const torsoHead = pivotPoints['torso_head'] || { x: 0, y: 0 };
1078
+ const headPivotWorld = torsoLocalPivot(torsoHead.x || 0, torsoHead.y || 0, 0);
1079
+ computedPivotPoints.push({ name: 'torso_head', x: headPivotWorld.x, y: headPivotWorld.y });
1080
+ // Mouth pivot — child of head, uses applyTransform chain for correctness
1057
1081
  const headAngle = rotations['head'] || 0;
1058
- const headAngleEff = flipHead ? -headAngle : headAngle;
1059
- const headOffX = 0; // head offset defaults to 0
1060
- const headOffY = 0; // head offset defaults to 0
1061
- const headOffEffX = flipHead ? -headOffX : headOffX;
1062
- const cosH = Math.cos(headAngleEff);
1063
- const sinH = Math.sin(headAngleEff);
1064
1082
  const mouthOffset = pivotPoints['head_mouth'] || { x: 0, y: 0 };
1065
- const mouthOffX = flipHead ? -(mouthOffset.x || 0) : (mouthOffset.x || 0);
1066
- const mouthOffY = mouthOffset.y || 0;
1067
- computedPivotPoints.push({
1068
- name: 'head_mouth',
1069
- x: centerX + (torsoHead.x || 0) + headOffEffX + (mouthOffX * cosH - mouthOffY * sinH),
1070
- y: centerY + (torsoHead.y || 0) + headOffY + (mouthOffX * sinH + mouthOffY * cosH)
1071
- });
1072
- // Left arm chain pivots
1073
- const lShoulder = pivotPoints['torso_leftUpperArm'] || { x: 0, y: 0 };
1074
- const lShoulderWorld = this.applyTransform(rootTransform, {
1075
- x: lShoulder.x || 0,
1076
- y: lShoulder.y || 0,
1083
+ const mouthHeadBase = torsoLocalPivot(torsoHead.x || 0, torsoHead.y || 0, headAngle);
1084
+ // Apply flipHead mirror to the head base before computing mouth
1085
+ const mouthHeadBaseFlipped = {
1086
+ ...mouthHeadBase,
1087
+ scaleX: mouthHeadBase.scaleX * (flipHead ? -1 : 1)
1088
+ };
1089
+ const mouthPivotWorld = this.applyTransform(mouthHeadBaseFlipped, {
1090
+ x: mouthOffset.x || 0,
1091
+ y: mouthOffset.y || 0,
1077
1092
  rotation: 0,
1078
1093
  scaleX: 1,
1079
1094
  scaleY: 1
1080
1095
  });
1096
+ computedPivotPoints.push({ name: 'head_mouth', x: mouthPivotWorld.x, y: mouthPivotWorld.y });
1097
+ // Left arm chain pivots
1098
+ const lShoulder = pivotPoints['torso_leftUpperArm'] || { x: 0, y: 0 };
1099
+ const lShoulderWorld = torsoLocalPivot(lShoulder.x || 0, lShoulder.y || 0, 0);
1081
1100
  computedPivotPoints.push({ name: 'torso_leftUpperArm', x: lShoulderWorld.x, y: lShoulderWorld.y });
1082
1101
  const rL1 = rotations['leftUpperArm'] || 0;
1083
1102
  const leftUpperArmHeight = dimensions.leftUpperArm?.height || 50;
1084
1103
  const lElbowOff = pivotPoints['leftUpperArm_leftForearm'] || { x: 0, y: 0 };
1085
- const lElbowBase = this.applyTransform(rootTransform, {
1086
- x: lShoulder.x || 0,
1087
- y: lShoulder.y || 0,
1088
- rotation: rL1,
1089
- scaleX: 1,
1090
- scaleY: 1
1091
- });
1104
+ const lElbowBase = torsoLocalPivot(lShoulder.x || 0, lShoulder.y || 0, rL1);
1092
1105
  const lElbowWorld = this.applyTransform(lElbowBase, {
1093
1106
  x: lElbowOff.x || 0,
1094
1107
  y: (lElbowOff.y || 0) - leftUpperArmHeight,
@@ -1116,24 +1129,12 @@ export class CharacterRigRenderer {
1116
1129
  computedPivotPoints.push({ name: 'leftForearm_leftHand', x: lWristWorld.x, y: lWristWorld.y });
1117
1130
  // Right arm chain pivots
1118
1131
  const rShoulder = pivotPoints['torso_rightUpperArm'] || { x: 0, y: 0 };
1119
- const rShoulderWorld = this.applyTransform(rootTransform, {
1120
- x: rShoulder.x || 0,
1121
- y: rShoulder.y || 0,
1122
- rotation: 0,
1123
- scaleX: 1,
1124
- scaleY: 1
1125
- });
1132
+ const rShoulderWorld = torsoLocalPivot(rShoulder.x || 0, rShoulder.y || 0, 0);
1126
1133
  computedPivotPoints.push({ name: 'torso_rightUpperArm', x: rShoulderWorld.x, y: rShoulderWorld.y });
1127
1134
  const rR1 = rotations['rightUpperArm'] || 0;
1128
1135
  const rightUpperArmHeight = dimensions.rightUpperArm?.height || 50;
1129
1136
  const rElbowOff = pivotPoints['rightUpperArm_rightForearm'] || { x: 0, y: 0 };
1130
- const rElbowBase = this.applyTransform(rootTransform, {
1131
- x: rShoulder.x || 0,
1132
- y: rShoulder.y || 0,
1133
- rotation: rR1,
1134
- scaleX: 1,
1135
- scaleY: 1
1136
- });
1137
+ const rElbowBase = torsoLocalPivot(rShoulder.x || 0, rShoulder.y || 0, rR1);
1137
1138
  const rElbowWorld = this.applyTransform(rElbowBase, {
1138
1139
  x: rElbowOff.x || 0,
1139
1140
  y: (rElbowOff.y || 0) - rightUpperArmHeight,
@@ -1159,25 +1160,17 @@ export class CharacterRigRenderer {
1159
1160
  scaleY: 1
1160
1161
  });
1161
1162
  computedPivotPoints.push({ name: 'rightForearm_rightHand', x: rWristWorld.x, y: rWristWorld.y });
1162
- // Left leg chain pivots
1163
+ // Left leg chain pivots — legs are rooted to rootTransform, not torsoTransform
1163
1164
  const lHip = pivotPoints['torso_leftThigh'] || { x: 0, y: 0 };
1164
1165
  const lHipWorld = this.applyTransform(rootTransform, {
1165
- x: lHip.x || 0,
1166
- y: lHip.y || 0,
1167
- rotation: 0,
1168
- scaleX: 1,
1169
- scaleY: 1
1166
+ x: lHip.x || 0, y: lHip.y || 0, rotation: 0, scaleX: 1, scaleY: 1
1170
1167
  });
1171
1168
  computedPivotPoints.push({ name: 'torso_leftThigh', x: lHipWorld.x, y: lHipWorld.y });
1172
1169
  const rLT1 = rotations['leftThigh'] || 0;
1173
1170
  const leftThighHeight = dimensions.leftThigh?.height || 60;
1174
1171
  const lKneeOff = pivotPoints['leftThigh_leftLeg'] || { x: 0, y: 0 };
1175
1172
  const lThighBase = this.applyTransform(rootTransform, {
1176
- x: lHip.x || 0,
1177
- y: lHip.y || 0,
1178
- rotation: rLT1,
1179
- scaleX: 1,
1180
- scaleY: 1
1173
+ x: lHip.x || 0, y: lHip.y || 0, rotation: rLT1, scaleX: 1, scaleY: 1
1181
1174
  });
1182
1175
  const lKneeWorld = this.applyTransform(lThighBase, {
1183
1176
  x: lKneeOff.x || 0,
@@ -1187,25 +1180,17 @@ export class CharacterRigRenderer {
1187
1180
  scaleY: 1
1188
1181
  });
1189
1182
  computedPivotPoints.push({ name: 'leftThigh_leftLeg', x: lKneeWorld.x, y: lKneeWorld.y });
1190
- // Right leg chain pivots
1183
+ // Right leg chain pivots — legs are rooted to rootTransform, not torsoTransform
1191
1184
  const rHip = pivotPoints['torso_rightThigh'] || { x: 0, y: 0 };
1192
1185
  const rHipWorld = this.applyTransform(rootTransform, {
1193
- x: rHip.x || 0,
1194
- y: rHip.y || 0,
1195
- rotation: 0,
1196
- scaleX: 1,
1197
- scaleY: 1
1186
+ x: rHip.x || 0, y: rHip.y || 0, rotation: 0, scaleX: 1, scaleY: 1
1198
1187
  });
1199
1188
  computedPivotPoints.push({ name: 'torso_rightThigh', x: rHipWorld.x, y: rHipWorld.y });
1200
1189
  const rRT1 = rotations['rightThigh'] || 0;
1201
1190
  const rightThighHeight = dimensions.rightThigh?.height || 60;
1202
1191
  const rKneeOff = pivotPoints['rightThigh_rightLeg'] || { x: 0, y: 0 };
1203
1192
  const rThighBase = this.applyTransform(rootTransform, {
1204
- x: rHip.x || 0,
1205
- y: rHip.y || 0,
1206
- rotation: rRT1,
1207
- scaleX: 1,
1208
- scaleY: 1
1193
+ x: rHip.x || 0, y: rHip.y || 0, rotation: rRT1, scaleX: 1, scaleY: 1
1209
1194
  });
1210
1195
  const rKneeWorld = this.applyTransform(rThighBase, {
1211
1196
  x: rKneeOff.x || 0,