distark-render 1.1.4 → 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.
Files changed (120) hide show
  1. package/README.md +0 -0
  2. package/assets/tank.json +0 -0
  3. package/dist/cli/animate.d.ts +0 -0
  4. package/dist/cli/animate.d.ts.map +0 -0
  5. package/dist/cli/animate.js +0 -0
  6. package/dist/cli/animate.js.map +0 -0
  7. package/dist/cli/diff.d.ts +0 -0
  8. package/dist/cli/diff.d.ts.map +0 -0
  9. package/dist/cli/diff.js +0 -0
  10. package/dist/cli/diff.js.map +0 -0
  11. package/dist/cli/distark-check.d.ts +0 -0
  12. package/dist/cli/distark-check.d.ts.map +0 -0
  13. package/dist/cli/distark-check.js +14 -14
  14. package/dist/cli/distark-check.js.map +0 -0
  15. package/dist/cli/query.d.ts +0 -0
  16. package/dist/cli/query.d.ts.map +0 -0
  17. package/dist/cli/query.js +0 -0
  18. package/dist/cli/query.js.map +0 -0
  19. package/dist/cli/record.d.ts +0 -0
  20. package/dist/cli/record.d.ts.map +0 -0
  21. package/dist/cli/record.js +0 -0
  22. package/dist/cli/record.js.map +0 -0
  23. package/dist/cli/render.d.ts +0 -0
  24. package/dist/cli/render.d.ts.map +0 -0
  25. package/dist/cli/render.js +0 -0
  26. package/dist/cli/render.js.map +0 -0
  27. package/dist/cli/shared.d.ts +0 -0
  28. package/dist/cli/shared.d.ts.map +0 -0
  29. package/dist/cli/shared.js +0 -0
  30. package/dist/cli/shared.js.map +0 -0
  31. package/dist/cli/test.d.ts +0 -0
  32. package/dist/cli/test.d.ts.map +0 -0
  33. package/dist/cli/test.js +0 -0
  34. package/dist/cli/test.js.map +0 -0
  35. package/dist/cli/verify.d.ts +0 -0
  36. package/dist/cli/verify.d.ts.map +0 -0
  37. package/dist/cli/verify.js +0 -0
  38. package/dist/cli/verify.js.map +0 -0
  39. package/dist/modules/adapters/p5Renderer.d.ts +0 -0
  40. package/dist/modules/adapters/p5Renderer.d.ts.map +0 -0
  41. package/dist/modules/adapters/p5Renderer.js +0 -0
  42. package/dist/modules/adapters/p5Renderer.js.map +0 -0
  43. package/dist/modules/adapters/skiaRenderer.d.ts +0 -0
  44. package/dist/modules/adapters/skiaRenderer.d.ts.map +0 -0
  45. package/dist/modules/adapters/skiaRenderer.js +0 -0
  46. package/dist/modules/adapters/skiaRenderer.js.map +0 -0
  47. package/dist/modules/animationDiff.d.ts +0 -0
  48. package/dist/modules/animationDiff.d.ts.map +0 -0
  49. package/dist/modules/animationDiff.js +0 -0
  50. package/dist/modules/animationDiff.js.map +0 -0
  51. package/dist/modules/eyeSystem.d.ts +0 -0
  52. package/dist/modules/eyeSystem.d.ts.map +0 -0
  53. package/dist/modules/eyeSystem.js +0 -0
  54. package/dist/modules/eyeSystem.js.map +0 -0
  55. package/dist/modules/imageLoad.d.ts +0 -0
  56. package/dist/modules/imageLoad.d.ts.map +0 -0
  57. package/dist/modules/imageLoad.js +0 -0
  58. package/dist/modules/imageLoad.js.map +0 -0
  59. package/dist/modules/mouthSystem.d.ts +0 -0
  60. package/dist/modules/mouthSystem.d.ts.map +0 -0
  61. package/dist/modules/mouthSystem.js +0 -0
  62. package/dist/modules/mouthSystem.js.map +0 -0
  63. package/dist/modules/renderRig.d.ts +0 -0
  64. package/dist/modules/renderRig.d.ts.map +1 -1
  65. package/dist/modules/renderRig.js +110 -124
  66. package/dist/modules/renderRig.js.map +1 -1
  67. package/dist/tests/helpers.d.ts +0 -0
  68. package/dist/tests/helpers.d.ts.map +0 -0
  69. package/dist/tests/helpers.js +0 -0
  70. package/dist/tests/helpers.js.map +0 -0
  71. package/dist/tests/test-animation-diff-rendering.d.ts +0 -0
  72. package/dist/tests/test-animation-diff-rendering.d.ts.map +0 -0
  73. package/dist/tests/test-animation-diff-rendering.js +0 -0
  74. package/dist/tests/test-animation-diff-rendering.js.map +0 -0
  75. package/dist/tests/test-animation-diff.d.ts +0 -0
  76. package/dist/tests/test-animation-diff.d.ts.map +0 -0
  77. package/dist/tests/test-animation-diff.js +0 -0
  78. package/dist/tests/test-animation-diff.js.map +0 -0
  79. package/dist/tests/test-autofit.d.ts +0 -0
  80. package/dist/tests/test-autofit.d.ts.map +0 -0
  81. package/dist/tests/test-autofit.js +0 -0
  82. package/dist/tests/test-autofit.js.map +0 -0
  83. package/dist/tests/test-base-autofit.d.ts +0 -0
  84. package/dist/tests/test-base-autofit.d.ts.map +0 -0
  85. package/dist/tests/test-base-autofit.js +0 -0
  86. package/dist/tests/test-base-autofit.js.map +0 -0
  87. package/dist/tests/test-cli.d.ts +0 -0
  88. package/dist/tests/test-cli.d.ts.map +0 -0
  89. package/dist/tests/test-cli.js +0 -0
  90. package/dist/tests/test-cli.js.map +0 -0
  91. package/dist/tests/test-image-loading.d.ts +0 -0
  92. package/dist/tests/test-image-loading.d.ts.map +0 -0
  93. package/dist/tests/test-image-loading.js +0 -0
  94. package/dist/tests/test-image-loading.js.map +0 -0
  95. package/dist/tests/test-joint-movement.d.ts +0 -0
  96. package/dist/tests/test-joint-movement.d.ts.map +0 -0
  97. package/dist/tests/test-joint-movement.js +0 -0
  98. package/dist/tests/test-joint-movement.js.map +0 -0
  99. package/dist/tests/test-p5-image-types.d.ts +0 -0
  100. package/dist/tests/test-p5-image-types.d.ts.map +0 -0
  101. package/dist/tests/test-p5-image-types.js +0 -0
  102. package/dist/tests/test-p5-image-types.js.map +0 -0
  103. package/dist/tests/test-skia.d.ts +0 -0
  104. package/dist/tests/test-skia.d.ts.map +0 -0
  105. package/dist/tests/test-skia.js +0 -0
  106. package/dist/tests/test-skia.js.map +0 -0
  107. package/dist/tests/test-torso-pivot.d.ts +11 -0
  108. package/dist/tests/test-torso-pivot.d.ts.map +1 -0
  109. package/dist/tests/test-torso-pivot.js +265 -0
  110. package/dist/tests/test-torso-pivot.js.map +1 -0
  111. package/dist/tests/test-visual-verification.d.ts +0 -0
  112. package/dist/tests/test-visual-verification.d.ts.map +0 -0
  113. package/dist/tests/test-visual-verification.js +0 -0
  114. package/dist/tests/test-visual-verification.js.map +0 -0
  115. package/dist/types.d.ts +0 -0
  116. package/dist/types.d.ts.map +0 -0
  117. package/dist/types.js +0 -0
  118. package/dist/types.js.map +0 -0
  119. package/package.json +2 -1
  120. package/assets/presets/jumping-jack.json +0 -194
package/README.md CHANGED
File without changes
package/assets/tank.json CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/cli/diff.js CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -16,20 +16,20 @@ import { runDiff } from './diff.js';
16
16
  import { runQuery } from './query.js';
17
17
  import { runRecord } from './record.js';
18
18
  import { runTest } from './test.js';
19
- const USAGE = `distark-check - CLI tool for LLM-driven rig testing
20
-
21
- Commands:
22
- render <rig.json> [-o out.png] [--width N] [--height N] [--report]
23
- verify <rig.json> [--checks all|bounds,visibility,proportions,zorder]
24
- animate <rig.json> <animation.json> [-o frames/] [--width N] [--height N]
25
- diff <before.png> <after.png> [-o diff.png] [--threshold N]
26
- query <image.png|rig.json> --prompt "..." [-o report.json]
27
- test <rig.json|image|video> --prompt "..." [--tries N] [-o report.json]
28
- record <world.json> [--orc URL] [-o out.mp4] [--timeout 300]
29
-
30
- Environment:
31
- GEMINI_API_KEY Required for 'query' and 'test' commands
32
- ORC_URL Orchestrator URL for 'record' command (default: https://orchestrator.distark.com)
19
+ const USAGE = `distark-check - CLI tool for LLM-driven rig testing
20
+
21
+ Commands:
22
+ render <rig.json> [-o out.png] [--width N] [--height N] [--report]
23
+ verify <rig.json> [--checks all|bounds,visibility,proportions,zorder]
24
+ animate <rig.json> <animation.json> [-o frames/] [--width N] [--height N]
25
+ diff <before.png> <after.png> [-o diff.png] [--threshold N]
26
+ query <image.png|rig.json> --prompt "..." [-o report.json]
27
+ test <rig.json|image|video> --prompt "..." [--tries N] [-o report.json]
28
+ record <world.json> [--orc URL] [-o out.mp4] [--timeout 300]
29
+
30
+ Environment:
31
+ GEMINI_API_KEY Required for 'query' and 'test' commands
32
+ ORC_URL Orchestrator URL for 'record' command (default: https://orchestrator.distark.com)
33
33
  `;
34
34
  async function main() {
35
35
  const args = process.argv.slice(2);
File without changes
File without changes
File without changes
package/dist/cli/query.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/cli/test.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"renderRig.d.ts","sourceRoot":"","sources":["../../modules/renderRig.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,KAAK,EACR,OAAO,EACP,aAAa,EACb,aAAa,EAGb,QAAQ,EACR,aAAa,EAChB,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,qBAAa,oBAAoB;IAC7B,SAAS,CAAC,WAAW,EAAE,WAAW,CAAC;IAEnC;;;OAGG;gBACS,WAAW,CAAC,EAAE,WAAW;IAIrC;;;OAGG;IACH,cAAc,IAAI,WAAW;IAI7B;;OAEG;IACH,OAAO,CAAC,cAAc;IAYtB;;OAEG;IACH,uBAAuB,CACnB,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,aAAkB,GAC5B,aAAa;IAwyChB;;;;OAIG;IACH,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,EAAE,OAAO,SAAK,GAAG,aAAa;IA6DnG;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CACR,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAAC,EACnE,YAAY,GAAE,QAAyB,EACvC,eAAe,GAAE,OAAc,EAC/B,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;CA8FnB;AAGD,eAAO,MAAM,eAAe,sBAA6B,CAAC;AAG1D,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAAC,EACnE,YAAY,GAAE,QAAyB,EACvC,eAAe,GAAE,OAAe,EAChC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAChC,OAAO,CAAC,IAAI,CAAC,CAEf;AAGD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,aAAkB,GAAG,aAAa,CAEpG"}
1
+ {"version":3,"file":"renderRig.d.ts","sourceRoot":"","sources":["../../modules/renderRig.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,KAAK,EACR,OAAO,EACP,aAAa,EACb,aAAa,EAGb,QAAQ,EACR,aAAa,EAChB,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,qBAAa,oBAAoB;IAC7B,SAAS,CAAC,WAAW,EAAE,WAAW,CAAC;IAEnC;;;OAGG;gBACS,WAAW,CAAC,EAAE,WAAW;IAIrC;;;OAGG;IACH,cAAc,IAAI,WAAW;IAI7B;;OAEG;IACH,OAAO,CAAC,cAAc;IAYtB;;OAEG;IACH,uBAAuB,CACnB,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,aAAkB,GAC5B,aAAa;IA+xChB;;;;OAIG;IACH,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,EAAE,OAAO,SAAK,GAAG,aAAa;IA6DnG;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CACR,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAAC,EACnE,YAAY,GAAE,QAAyB,EACvC,eAAe,GAAE,OAAc,EAC/B,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;CA8FnB;AAGD,eAAO,MAAM,eAAe,sBAA6B,CAAC;AAG1D,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAAC,EACnE,YAAY,GAAE,QAAyB,EACvC,eAAe,GAAE,OAAe,EAChC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAChC,OAAO,CAAC,IAAI,CAAC,CAEf;AAGD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,aAAkB,GAAG,aAAa,CAEpG"}
@@ -72,6 +72,19 @@ export class CharacterRigRenderer {
72
72
  scaleX: flipX ? -1 : 1,
73
73
  scaleY: 1
74
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
+ });
75
88
  // Torso
76
89
  if (visibility.torso !== false) {
77
90
  const torsoWidth = dimensions.torso?.width || 60;
@@ -80,20 +93,30 @@ export class CharacterRigRenderer {
80
93
  // CRITICAL FIX: Look up image by ACTUAL PATH from rigData, not semantic key!
81
94
  const torsoImagePath = rigData.imagePaths?.torso;
82
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
+ });
83
107
  allObjects.push({
84
108
  name: 'torso',
85
109
  type: 'limb',
86
110
  zIndex: zIndexValues['torso'] || 1,
87
111
  width: torsoWidth * imageScale,
88
112
  height: torsoHeight * imageScale,
89
- x: rootTransform.x,
90
- y: rootTransform.y,
91
- rotation: rootTransform.rotation + selfRot,
92
- scaleX: rootTransform.scaleX,
93
- scaleY: rootTransform.scaleY,
94
- // 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,
95
118
  anchorX: 0.5,
96
- anchorY: 1,
119
+ anchorY: 1, // bottom-center sits at root_torso world position
97
120
  imageKey: 'imagePaths.torso',
98
121
  imageData: torsoImage,
99
122
  selfRotation: selfRot
@@ -108,10 +131,10 @@ export class CharacterRigRenderer {
108
131
  const headHeight = dimensions.head?.height || 80;
109
132
  const headRot = rotations["head"] || 0;
110
133
  const selfRot = selfRotations["head"] || 0;
111
- // Calculate head transform
112
- const headParentTransform = this.applyTransform(rootTransform, {
113
- x: pivotPoint.x || 0,
114
- 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),
115
138
  rotation: headRot,
116
139
  scaleX: flipHead ? -1 : 1,
117
140
  scaleY: 1
@@ -182,9 +205,9 @@ export class CharacterRigRenderer {
182
205
  // Calculate final dimensions with size scaling
183
206
  const finalMouthWidth = baseMouthWidth * mouthSizeScale;
184
207
  const finalMouthHeight = baseMouthHeight * mouthSizeScale;
185
- const headBaseTransform = this.applyTransform(rootTransform, {
186
- x: headPivot.x || 0,
187
- 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),
188
211
  rotation: headRot,
189
212
  scaleX: flipHead ? -1 : 1,
190
213
  scaleY: 1
@@ -239,9 +262,9 @@ export class CharacterRigRenderer {
239
262
  if (visibility.leftEye !== false && leftEyeImage && eyes) {
240
263
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
241
264
  const headRot = rotations["head"] || 0;
242
- const headBaseTransform = this.applyTransform(rootTransform, {
243
- x: headPivot.x || 0,
244
- 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),
245
268
  rotation: headRot,
246
269
  scaleX: flipHead ? -1 : 1,
247
270
  scaleY: 1
@@ -282,9 +305,9 @@ export class CharacterRigRenderer {
282
305
  if (visibility.rightEye !== false && rightEyeImage && eyes) {
283
306
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
284
307
  const headRot = rotations["head"] || 0;
285
- const headBaseTransform = this.applyTransform(rootTransform, {
286
- x: headPivot.x || 0,
287
- 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),
288
311
  rotation: headRot,
289
312
  scaleX: flipHead ? -1 : 1,
290
313
  scaleY: 1
@@ -325,9 +348,9 @@ export class CharacterRigRenderer {
325
348
  if (visibility.leftIris !== false && leftIrisImage && eyes) {
326
349
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
327
350
  const headRot = rotations["head"] || 0;
328
- const headBaseTransform = this.applyTransform(rootTransform, {
329
- x: headPivot.x || 0,
330
- 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),
331
354
  rotation: headRot,
332
355
  scaleX: flipHead ? -1 : 1,
333
356
  scaleY: 1
@@ -370,9 +393,9 @@ export class CharacterRigRenderer {
370
393
  if (visibility.rightIris !== false && rightIrisImage && eyes) {
371
394
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
372
395
  const headRot = rotations["head"] || 0;
373
- const headBaseTransform = this.applyTransform(rootTransform, {
374
- x: headPivot.x || 0,
375
- 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),
376
399
  rotation: headRot,
377
400
  scaleX: flipHead ? -1 : 1,
378
401
  scaleY: 1
@@ -431,9 +454,9 @@ export class CharacterRigRenderer {
431
454
  if (visibility.leftEyeLid !== false && leftEyeLidImage && eyes) {
432
455
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
433
456
  const headRot = rotations["head"] || 0;
434
- const headBaseTransform = this.applyTransform(rootTransform, {
435
- x: headPivot.x || 0,
436
- 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),
437
460
  rotation: headRot,
438
461
  scaleX: flipHead ? -1 : 1,
439
462
  scaleY: 1
@@ -489,9 +512,9 @@ export class CharacterRigRenderer {
489
512
  if (visibility.rightEyeLid !== false && rightEyeLidImage && eyes) {
490
513
  const headPivot = pivotPoints['torso_head'] || { x: 0, y: 0 };
491
514
  const headRot = rotations["head"] || 0;
492
- const headBaseTransform = this.applyTransform(rootTransform, {
493
- x: headPivot.x || 0,
494
- 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),
495
518
  rotation: headRot,
496
519
  scaleX: flipHead ? -1 : 1,
497
520
  scaleY: 1
@@ -536,9 +559,9 @@ export class CharacterRigRenderer {
536
559
  const height = dimensions.leftUpperArm?.height || 50;
537
560
  const limbRot = rotations['leftUpperArm'] || 0;
538
561
  const selfRot = selfRotations['leftUpperArm'] || 0;
539
- const upperArmParentTransform = this.applyTransform(rootTransform, {
540
- x: pivotPoint.x || 0,
541
- 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),
542
565
  rotation: limbRot,
543
566
  scaleX: 1,
544
567
  scaleY: 1
@@ -582,9 +605,9 @@ export class CharacterRigRenderer {
582
605
  const height = dimensions.leftForearm?.height || 45;
583
606
  const limbRot = rotations["leftForearm"] || 0;
584
607
  const selfRot = selfRotations["leftForearm"] || 0;
585
- const upperArmBase = this.applyTransform(rootTransform, {
586
- x: upperPivot.x || 0,
587
- 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),
588
611
  rotation: upperRot,
589
612
  scaleX: 1,
590
613
  scaleY: 1
@@ -639,9 +662,9 @@ export class CharacterRigRenderer {
639
662
  const height = dimensions.leftHand?.height || 30;
640
663
  const handRot = rotations["leftHand"] || 0;
641
664
  const selfRot = selfRotations["leftHand"] || 0;
642
- const upperArmBase = this.applyTransform(rootTransform, {
643
- x: upperPivot.x || 0,
644
- 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),
645
668
  rotation: upperRot,
646
669
  scaleX: 1,
647
670
  scaleY: 1
@@ -695,9 +718,9 @@ export class CharacterRigRenderer {
695
718
  const height = dimensions.rightUpperArm?.height || 50;
696
719
  const limbRot = rotations['rightUpperArm'] || 0;
697
720
  const selfRot = selfRotations['rightUpperArm'] || 0;
698
- const upperArmParentTransform = this.applyTransform(rootTransform, {
699
- x: pivotPoint.x || 0,
700
- 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),
701
724
  rotation: limbRot,
702
725
  scaleX: 1,
703
726
  scaleY: 1
@@ -741,9 +764,9 @@ export class CharacterRigRenderer {
741
764
  const height = dimensions.rightForearm?.height || 45;
742
765
  const limbRot = rotations["rightForearm"] || 0;
743
766
  const selfRot = selfRotations["rightForearm"] || 0;
744
- const upperArmBase = this.applyTransform(rootTransform, {
745
- x: upperPivot.x || 0,
746
- 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),
747
770
  rotation: upperRot,
748
771
  scaleX: 1,
749
772
  scaleY: 1
@@ -798,9 +821,9 @@ export class CharacterRigRenderer {
798
821
  const height = dimensions.rightHand?.height || 30;
799
822
  const handRot = rotations["rightHand"] || 0;
800
823
  const selfRot = selfRotations["rightHand"] || 0;
801
- const upperArmBase = this.applyTransform(rootTransform, {
802
- x: upperPivot.x || 0,
803
- 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),
804
827
  rotation: upperRot,
805
828
  scaleX: 1,
806
829
  scaleY: 1
@@ -1036,58 +1059,49 @@ export class CharacterRigRenderer {
1036
1059
  }
1037
1060
  // Sort all objects by zIndex
1038
1061
  allObjects.sort((a, b) => a.zIndex - b.zIndex);
1039
- // 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.
1040
1065
  const computedPivotPoints = [];
1041
- // Head/torso pivot
1042
- const torsoHead = pivotPoints['torso_head'] || { x: 0, y: 0 };
1043
- const headPivotWorld = this.applyTransform(rootTransform, {
1044
- x: torsoHead.x || 0,
1045
- y: torsoHead.y || 0,
1046
- 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,
1047
1071
  scaleX: 1,
1048
1072
  scaleY: 1
1049
1073
  });
1050
- computedPivotPoints.push({
1051
- name: 'torso_head',
1052
- x: headPivotWorld.x,
1053
- y: headPivotWorld.y
1054
- });
1055
- // 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
1056
1081
  const headAngle = rotations['head'] || 0;
1057
- const headAngleEff = flipHead ? -headAngle : headAngle;
1058
- const headOffX = 0; // head offset defaults to 0
1059
- const headOffY = 0; // head offset defaults to 0
1060
- const headOffEffX = flipHead ? -headOffX : headOffX;
1061
- const cosH = Math.cos(headAngleEff);
1062
- const sinH = Math.sin(headAngleEff);
1063
1082
  const mouthOffset = pivotPoints['head_mouth'] || { x: 0, y: 0 };
1064
- const mouthOffX = flipHead ? -(mouthOffset.x || 0) : (mouthOffset.x || 0);
1065
- const mouthOffY = mouthOffset.y || 0;
1066
- computedPivotPoints.push({
1067
- name: 'head_mouth',
1068
- x: centerX + (torsoHead.x || 0) + headOffEffX + (mouthOffX * cosH - mouthOffY * sinH),
1069
- y: centerY + (torsoHead.y || 0) + headOffY + (mouthOffX * sinH + mouthOffY * cosH)
1070
- });
1071
- // Left arm chain pivots
1072
- const lShoulder = pivotPoints['torso_leftUpperArm'] || { x: 0, y: 0 };
1073
- const lShoulderWorld = this.applyTransform(rootTransform, {
1074
- x: lShoulder.x || 0,
1075
- 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,
1076
1092
  rotation: 0,
1077
1093
  scaleX: 1,
1078
1094
  scaleY: 1
1079
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);
1080
1100
  computedPivotPoints.push({ name: 'torso_leftUpperArm', x: lShoulderWorld.x, y: lShoulderWorld.y });
1081
1101
  const rL1 = rotations['leftUpperArm'] || 0;
1082
1102
  const leftUpperArmHeight = dimensions.leftUpperArm?.height || 50;
1083
1103
  const lElbowOff = pivotPoints['leftUpperArm_leftForearm'] || { x: 0, y: 0 };
1084
- const lElbowBase = this.applyTransform(rootTransform, {
1085
- x: lShoulder.x || 0,
1086
- y: lShoulder.y || 0,
1087
- rotation: rL1,
1088
- scaleX: 1,
1089
- scaleY: 1
1090
- });
1104
+ const lElbowBase = torsoLocalPivot(lShoulder.x || 0, lShoulder.y || 0, rL1);
1091
1105
  const lElbowWorld = this.applyTransform(lElbowBase, {
1092
1106
  x: lElbowOff.x || 0,
1093
1107
  y: (lElbowOff.y || 0) - leftUpperArmHeight,
@@ -1115,24 +1129,12 @@ export class CharacterRigRenderer {
1115
1129
  computedPivotPoints.push({ name: 'leftForearm_leftHand', x: lWristWorld.x, y: lWristWorld.y });
1116
1130
  // Right arm chain pivots
1117
1131
  const rShoulder = pivotPoints['torso_rightUpperArm'] || { x: 0, y: 0 };
1118
- const rShoulderWorld = this.applyTransform(rootTransform, {
1119
- x: rShoulder.x || 0,
1120
- y: rShoulder.y || 0,
1121
- rotation: 0,
1122
- scaleX: 1,
1123
- scaleY: 1
1124
- });
1132
+ const rShoulderWorld = torsoLocalPivot(rShoulder.x || 0, rShoulder.y || 0, 0);
1125
1133
  computedPivotPoints.push({ name: 'torso_rightUpperArm', x: rShoulderWorld.x, y: rShoulderWorld.y });
1126
1134
  const rR1 = rotations['rightUpperArm'] || 0;
1127
1135
  const rightUpperArmHeight = dimensions.rightUpperArm?.height || 50;
1128
1136
  const rElbowOff = pivotPoints['rightUpperArm_rightForearm'] || { x: 0, y: 0 };
1129
- const rElbowBase = this.applyTransform(rootTransform, {
1130
- x: rShoulder.x || 0,
1131
- y: rShoulder.y || 0,
1132
- rotation: rR1,
1133
- scaleX: 1,
1134
- scaleY: 1
1135
- });
1137
+ const rElbowBase = torsoLocalPivot(rShoulder.x || 0, rShoulder.y || 0, rR1);
1136
1138
  const rElbowWorld = this.applyTransform(rElbowBase, {
1137
1139
  x: rElbowOff.x || 0,
1138
1140
  y: (rElbowOff.y || 0) - rightUpperArmHeight,
@@ -1158,25 +1160,17 @@ export class CharacterRigRenderer {
1158
1160
  scaleY: 1
1159
1161
  });
1160
1162
  computedPivotPoints.push({ name: 'rightForearm_rightHand', x: rWristWorld.x, y: rWristWorld.y });
1161
- // Left leg chain pivots
1163
+ // Left leg chain pivots — legs are rooted to rootTransform, not torsoTransform
1162
1164
  const lHip = pivotPoints['torso_leftThigh'] || { x: 0, y: 0 };
1163
1165
  const lHipWorld = this.applyTransform(rootTransform, {
1164
- x: lHip.x || 0,
1165
- y: lHip.y || 0,
1166
- rotation: 0,
1167
- scaleX: 1,
1168
- scaleY: 1
1166
+ x: lHip.x || 0, y: lHip.y || 0, rotation: 0, scaleX: 1, scaleY: 1
1169
1167
  });
1170
1168
  computedPivotPoints.push({ name: 'torso_leftThigh', x: lHipWorld.x, y: lHipWorld.y });
1171
1169
  const rLT1 = rotations['leftThigh'] || 0;
1172
1170
  const leftThighHeight = dimensions.leftThigh?.height || 60;
1173
1171
  const lKneeOff = pivotPoints['leftThigh_leftLeg'] || { x: 0, y: 0 };
1174
1172
  const lThighBase = this.applyTransform(rootTransform, {
1175
- x: lHip.x || 0,
1176
- y: lHip.y || 0,
1177
- rotation: rLT1,
1178
- scaleX: 1,
1179
- scaleY: 1
1173
+ x: lHip.x || 0, y: lHip.y || 0, rotation: rLT1, scaleX: 1, scaleY: 1
1180
1174
  });
1181
1175
  const lKneeWorld = this.applyTransform(lThighBase, {
1182
1176
  x: lKneeOff.x || 0,
@@ -1186,25 +1180,17 @@ export class CharacterRigRenderer {
1186
1180
  scaleY: 1
1187
1181
  });
1188
1182
  computedPivotPoints.push({ name: 'leftThigh_leftLeg', x: lKneeWorld.x, y: lKneeWorld.y });
1189
- // Right leg chain pivots
1183
+ // Right leg chain pivots — legs are rooted to rootTransform, not torsoTransform
1190
1184
  const rHip = pivotPoints['torso_rightThigh'] || { x: 0, y: 0 };
1191
1185
  const rHipWorld = this.applyTransform(rootTransform, {
1192
- x: rHip.x || 0,
1193
- y: rHip.y || 0,
1194
- rotation: 0,
1195
- scaleX: 1,
1196
- scaleY: 1
1186
+ x: rHip.x || 0, y: rHip.y || 0, rotation: 0, scaleX: 1, scaleY: 1
1197
1187
  });
1198
1188
  computedPivotPoints.push({ name: 'torso_rightThigh', x: rHipWorld.x, y: rHipWorld.y });
1199
1189
  const rRT1 = rotations['rightThigh'] || 0;
1200
1190
  const rightThighHeight = dimensions.rightThigh?.height || 60;
1201
1191
  const rKneeOff = pivotPoints['rightThigh_rightLeg'] || { x: 0, y: 0 };
1202
1192
  const rThighBase = this.applyTransform(rootTransform, {
1203
- x: rHip.x || 0,
1204
- y: rHip.y || 0,
1205
- rotation: rRT1,
1206
- scaleX: 1,
1207
- scaleY: 1
1193
+ x: rHip.x || 0, y: rHip.y || 0, rotation: rRT1, scaleX: 1, scaleY: 1
1208
1194
  });
1209
1195
  const rKneeWorld = this.applyTransform(rThighBase, {
1210
1196
  x: rKneeOff.x || 0,