@energy8platform/golem 0.5.0 → 0.7.0

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 (72) hide show
  1. package/README.md +18 -0
  2. package/dist/editor.css +3 -4
  3. package/dist/editor.js +177 -75
  4. package/dist/lib/cli.js +302 -126
  5. package/dist/lib/cli.js.map +4 -4
  6. package/dist/lib/e8/agent.d.ts +35 -0
  7. package/dist/lib/e8/host.d.ts +22 -0
  8. package/dist/lib/e8/node.d.ts +66 -0
  9. package/dist/lib/e8/runtime.d.ts +10 -0
  10. package/dist/lib/e8/schema.d.ts +3 -0
  11. package/dist/lib/e8/types.d.ts +80 -0
  12. package/dist/lib/e8-agent.js +6781 -0
  13. package/dist/lib/e8-agent.js.map +7 -0
  14. package/dist/lib/e8-client.js +116 -0
  15. package/dist/lib/e8-host.js +6929 -0
  16. package/dist/lib/e8-host.js.map +7 -0
  17. package/dist/lib/e8-runtime.js +2024 -0
  18. package/dist/lib/e8-runtime.js.map +7 -0
  19. package/dist/lib/e8-schema.js +7 -0
  20. package/dist/lib/e8-schema.js.map +7 -0
  21. package/dist/lib/editor/api.d.ts +1 -0
  22. package/dist/lib/editor/embed.d.ts +9 -0
  23. package/dist/lib/editor/server.d.ts +23 -1
  24. package/dist/lib/editor/store.d.ts +107 -0
  25. package/dist/lib/editor/timeline.d.ts +15 -0
  26. package/dist/lib/editor-entry.d.ts +1 -1
  27. package/dist/lib/editor-entry.js +303 -126
  28. package/dist/lib/editor-entry.js.map +4 -4
  29. package/dist/lib/harness.js +69 -14
  30. package/dist/lib/interactive-editor.js.map +1 -1
  31. package/dist/lib/rig-anim.d.ts +2 -0
  32. package/dist/lib/rig-control-qa.d.ts +27 -0
  33. package/dist/lib/rig-format.d.ts +989 -0
  34. package/dist/lib/rig-import-layers.d.ts +2 -0
  35. package/dist/lib/rig-version.d.ts +12 -0
  36. package/dist/lib/runtime.js +69 -14
  37. package/dist/lib/runtime.js.map +3 -3
  38. package/dist/lib/spine-import.d.ts +4 -0
  39. package/dist/lib/tool-schema.d.ts +3 -0
  40. package/dist/lib/tools.d.ts +2 -0
  41. package/dist/lib/tools.js +257 -73
  42. package/dist/lib/tools.js.map +4 -4
  43. package/editor.html +2 -2
  44. package/package.json +38 -3
  45. package/skills/e8-golem/SKILL.md +71 -0
  46. package/skills/golem-symbol-animation/SKILL.md +87 -0
  47. package/skills/golem-symbol-animation/agents/openai.yaml +4 -0
  48. package/skills/golem-symbol-animation/references/facial-controls.md +48 -0
  49. package/skills/golem-symbol-animation/references/game-engine-integration.md +133 -0
  50. package/skills/golem-symbol-animation/references/golem-authoring.md +138 -0
  51. package/skills/golem-symbol-animation/references/interactive-editor.md +17 -0
  52. package/skills/golem-symbol-animation/references/motion-craft.md +113 -0
  53. package/skills/golem-symbol-animation/references/packed-delivery.md +88 -0
  54. package/skills/golem-symbol-animation/references/quantitative-qa.md +106 -0
  55. package/skills/golem-symbol-animation/references/spine-rive-study.md +87 -0
  56. package/skills/golem-symbol-animation/scripts/atlas_parts.py +87 -0
  57. package/skills/golem-symbol-animation/scripts/check_package.py +71 -0
  58. package/skills/golem-symbol-animation/scripts/image_gates.py +143 -0
  59. package/skills/golem-symbol-cutting/SKILL.md +70 -0
  60. package/skills/golem-symbol-cutting/agents/openai.yaml +4 -0
  61. package/skills/golem-symbol-cutting/assets/h1/h1.atlas.png +0 -0
  62. package/skills/golem-symbol-cutting/assets/h1/h1.png +0 -0
  63. package/skills/golem-symbol-cutting/references/cutting-workflow.md +92 -0
  64. package/skills/golem-symbol-cutting/references/facial-layers.md +44 -0
  65. package/skills/golem-symbol-cutting/references/generation-workflow.md +109 -0
  66. package/skills/golem-symbol-cutting/references/golem-handoff.md +62 -0
  67. package/skills/golem-symbol-cutting/references/h1-example.md +46 -0
  68. package/skills/golem-symbol-cutting/references/hybrid-workflow.md +119 -0
  69. package/skills/golem-symbol-cutting/references/registration-and-motion.md +89 -0
  70. package/skills/golem-symbol-cutting/references/spine-rive-construction.md +73 -0
  71. package/skills/golem-symbol-cutting/scripts/atlas_parts.py +87 -0
  72. package/skills/golem-symbol-cutting/scripts/hybrid_parts.py +279 -0
package/dist/lib/tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/rig-api.ts
2
2
  import { z as z3 } from "zod";
3
3
  import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
4
- import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
4
+ import { dirname as dirname8, join as join9, relative as relative3, isAbsolute as isAbsolute2 } from "node:path";
5
5
 
6
6
  // src/rig-io.ts
7
7
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
@@ -24,8 +24,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
24
24
  // src/rig-queue.ts
25
25
  import { AsyncLocalStorage } from "node:async_hooks";
26
26
  import { resolve } from "node:path";
27
- var pending = /* @__PURE__ */ new Map();
28
- var active = new AsyncLocalStorage();
27
+ var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
28
+ pending: /* @__PURE__ */ new Map(),
29
+ active: new AsyncLocalStorage()
30
+ };
29
31
  async function withRigQueue(path, op) {
30
32
  const key = resolve(path);
31
33
  const held = active.getStore();
@@ -216,7 +218,9 @@ var PathAttachment = z.object({
216
218
  /** curve lengths for constant-speed positioning */
217
219
  lengths: z.array(z.number()).default([]),
218
220
  closed: z.boolean().default(false),
219
- constantSpeed: z.boolean().default(true)
221
+ constantSpeed: z.boolean().default(true),
222
+ /** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
223
+ clipping: z.object({ end: Id.optional() }).optional()
220
224
  });
221
225
  var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
222
226
  var IkConstraint = z.object({
@@ -285,7 +289,9 @@ var Key = z.object({
285
289
  t: z.number().nonnegative(),
286
290
  v: KeyValue,
287
291
  /** easing INTO the next key */
288
- ease: Ease.default("ease_in_out")
292
+ ease: Ease.default("ease_in_out"),
293
+ /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
294
+ componentEase: z.array(Ease).min(1).optional()
289
295
  });
290
296
  var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
291
297
  var Track = z.discriminatedUnion("target", [
@@ -472,6 +478,10 @@ function validate(doc) {
472
478
  else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
473
479
  }
474
480
  }
481
+ for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
482
+ if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
483
+ if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
484
+ }
475
485
  const vertexCount2 = (a) => {
476
486
  if (a.type === "region") return 4;
477
487
  if (a.type === "mesh") return a.uvs.length / 2;
@@ -555,6 +565,7 @@ function validate(doc) {
555
565
  const att = t.target === "deform" ? attachments.get(t.id) : void 0;
556
566
  let prev = -1;
557
567
  for (const k of t.keys) {
568
+ if (k.componentEase && (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number") || k.componentEase.length !== k.v.length)) errs.push(`animation "${a.id}" ${tid}: componentEase must match the numeric-array value length`);
558
569
  if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
559
570
  if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
560
571
  if (t.target === "control") {
@@ -732,8 +743,8 @@ function sample(track, t, loop = false) {
732
743
  }
733
744
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
734
745
  if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
735
- const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
736
- return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
746
+ const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
747
+ return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
737
748
  }
738
749
  return a.v;
739
750
  }
@@ -825,6 +836,13 @@ function boneWorldOf(bone, s, parent) {
825
836
  if (!parent) return local;
826
837
  if (bone.inherit === "normal") return mul(parent, local);
827
838
  const [tx, ty] = apply(parent, s.x, s.y);
839
+ if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
840
+ const r = s.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
841
+ const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
842
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
843
+ const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
844
+ return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
845
+ }
828
846
  return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
829
847
  }
830
848
  var orderCache = /* @__PURE__ */ new WeakMap();
@@ -857,11 +875,11 @@ var localOf = (doc, pose, b) => {
857
875
  shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
858
876
  };
859
877
  };
860
- function updateSubtree(skel, root2) {
878
+ function updateSubtree(skel, root2, includeRoot = true) {
861
879
  const doc = skel.doc;
862
880
  const kids = /* @__PURE__ */ new Map();
863
881
  for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
864
- const stack = [root2];
882
+ const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
865
883
  while (stack.length) {
866
884
  const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
867
885
  st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
@@ -1104,7 +1122,8 @@ function applyTransformConstraint(skel, c) {
1104
1122
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
1105
1123
  c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
1106
1124
  const target = skel.bones.get(c.target), tw = target.world;
1107
- const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
1125
+ const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
1126
+ const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
1108
1127
  for (const id of c.bones) {
1109
1128
  const b = boneOf(skel.doc, id), st = skel.bones.get(id);
1110
1129
  if (c.local) {
@@ -1266,6 +1285,7 @@ function applyPathConstraint(skel, c) {
1266
1285
  const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
1267
1286
  const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
1268
1287
  const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
1288
+ const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
1269
1289
  const spacesCount = tangents ? bones.length : bones.length + 1;
1270
1290
  const spaces = new Array(spacesCount).fill(0), worldLengths = [];
1271
1291
  let sum = 0;
@@ -1300,9 +1320,10 @@ function applyPathConstraint(skel, c) {
1300
1320
  if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
1301
1321
  }
1302
1322
  let boneX = samples[0].x, boneY = samples[0].y;
1323
+ const solvedWorlds = /* @__PURE__ */ new Map();
1303
1324
  for (let i = 0; i < bones.length; i++) {
1304
1325
  const { b, st } = bones[i];
1305
- let w = { ...st.world };
1326
+ let w = { ...inputWorlds[i] };
1306
1327
  w.tx += (boneX - w.tx) * mixX;
1307
1328
  w.ty += (boneY - w.ty) * mixY;
1308
1329
  const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
@@ -1332,7 +1353,11 @@ function applyPathConstraint(skel, c) {
1332
1353
  const cos = Math.cos(r), sin = Math.sin(r);
1333
1354
  w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
1334
1355
  }
1335
- setEffectiveWorld(skel, b, st, w);
1356
+ solvedWorlds.set(b.id, w);
1357
+ }
1358
+ for (const b of boneOrder(skel.doc)) {
1359
+ const world = solvedWorlds.get(b.id);
1360
+ if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
1336
1361
  }
1337
1362
  }
1338
1363
  function measuredPathLength(points, closed, lengths, constantSpeed) {
@@ -1361,9 +1386,14 @@ function inheritedParentLinear(bone, parent) {
1361
1386
  case "onlyTranslation":
1362
1387
  return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
1363
1388
  case "noRotationOrReflection": {
1364
- const lenX = Math.hypot(parent.a, parent.b) || 1;
1389
+ const lenX = Math.hypot(parent.a, parent.b);
1365
1390
  const det = parent.a * parent.d - parent.b * parent.c;
1366
- return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };
1391
+ if (lenX * lenX <= 1e-4) {
1392
+ const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
1393
+ return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
1394
+ }
1395
+ const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
1396
+ return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };
1367
1397
  }
1368
1398
  case "noScale":
1369
1399
  case "noScaleOrReflection": {
@@ -1389,7 +1419,16 @@ function setEffectiveWorld(skel, bone, st, world) {
1389
1419
  st.scaleY = Math.hypot(lin.c, lin.d);
1390
1420
  st.shearX = 0;
1391
1421
  st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
1392
- updateSubtree(skel, bone.id);
1422
+ if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
1423
+ const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
1424
+ st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
1425
+ st.scaleX = Math.hypot(world.a, world.b);
1426
+ st.scaleY = Math.hypot(world.c, world.d);
1427
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
1428
+ st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
1429
+ }
1430
+ st.world = world;
1431
+ updateSubtree(skel, bone.id, false);
1393
1432
  }
1394
1433
  function attachmentWorldVertices(skel, slotId, att) {
1395
1434
  const deform = skel.pose.get(pk("deform", att.id, "vertices"));
@@ -1964,8 +2003,9 @@ function createDoc(name, width, height, notes) {
1964
2003
  function createSymbol(name, asset, margin = 0.15, notes) {
1965
2004
  const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
1966
2005
  const width = asset.width + 2 * mx, height = asset.height + 2 * my;
1967
- const fromFile = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/^[^a-z]+/, "");
1968
- const id = asset.id ?? (fromFile || name);
2006
+ const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
2007
+ const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
2008
+ const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
1969
2009
  let doc = createDoc(name, width, height, notes);
1970
2010
  doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
1971
2011
  doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
@@ -2148,7 +2188,7 @@ function removeSlot(doc, id) {
2148
2188
  return patchDrawOrder({
2149
2189
  ...doc,
2150
2190
  slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
2151
- attachments: doc.attachments.filter((a) => a.slot !== id),
2191
+ attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
2152
2192
  animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
2153
2193
  }, (o) => o.filter((s) => s !== id));
2154
2194
  }
@@ -2544,7 +2584,7 @@ function rename2(doc, kind, id, to) {
2544
2584
  return {
2545
2585
  ...doc,
2546
2586
  slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
2547
- attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
2587
+ attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
2548
2588
  path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
2549
2589
  };
2550
2590
  }
@@ -2721,7 +2761,7 @@ function setGoal(doc, bone, x, y, opts) {
2721
2761
 
2722
2762
  // src/rig-import-layers.ts
2723
2763
  import { readdir, readFile } from "node:fs/promises";
2724
- import { join as join2, basename as basename2 } from "node:path";
2764
+ import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
2725
2765
  var DEFAULT_PARENT = {
2726
2766
  head: "neck",
2727
2767
  neck: "torso",
@@ -2747,6 +2787,10 @@ function pngSize(buf) {
2747
2787
  if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
2748
2788
  return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
2749
2789
  }
2790
+ function outsideDir(dir, file) {
2791
+ const r = relative(dir, join2(dir, file));
2792
+ return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
2793
+ }
2750
2794
  async function importLayers(doc, dir, relPrefix = "layers") {
2751
2795
  let specs;
2752
2796
  try {
@@ -2759,6 +2803,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
2759
2803
  let next = doc;
2760
2804
  const parsed = [];
2761
2805
  for (const s of specs) {
2806
+ if (outsideDir(dir, s.file)) throw new Error(`layers.json file "${s.file}" is outside the layers folder (${dir})`);
2762
2807
  const buf = await readFile(join2(dir, s.file));
2763
2808
  const [w, h] = pngSize(buf);
2764
2809
  const m = basename2(s.file, ".png").match(/^(?:(\d+)_)?(.+)$/);
@@ -4015,7 +4060,7 @@ async function buildAtlas(doc, assetsDir, outDir, opts = {}) {
4015
4060
 
4016
4061
  // src/spine-import.ts
4017
4062
  import { readFile as readFile7, stat, writeFile as writeFile5 } from "node:fs/promises";
4018
- import { basename as basename4, dirname as dirname6, extname, join as join8, relative } from "node:path";
4063
+ import { basename as basename4, dirname as dirname6, extname, join as join8, relative as relative2 } from "node:path";
4019
4064
 
4020
4065
  // src/ktx2.ts
4021
4066
  import { readFile as readFile6 } from "node:fs/promises";
@@ -4157,6 +4202,9 @@ var Ids = class {
4157
4202
  has(name) {
4158
4203
  return this.map.has(name);
4159
4204
  }
4205
+ reserve(id) {
4206
+ this.used.add(id);
4207
+ }
4160
4208
  };
4161
4209
  var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
4162
4210
  var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
@@ -4165,6 +4213,7 @@ function importSpine(json, atlasText, opts = {}) {
4165
4213
  const warnings = [];
4166
4214
  const atlas = parseAtlas(atlasText);
4167
4215
  const version = String(json.skeleton?.spine ?? "4.0");
4216
+ if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
4168
4217
  const major = parseFloat(version);
4169
4218
  const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
4170
4219
  const margin = opts.margin ?? 0;
@@ -4172,6 +4221,7 @@ function importSpine(json, atlasText, opts = {}) {
4172
4221
  const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
4173
4222
  const boneIds = new Ids("b_"), slotIds = new Ids("s_"), assetIds = new Ids("a_"), attIds = new Ids("att_"), ikIds = new Ids("ik_"), tcIds = new Ids("tc_"), pcIds = new Ids("pc_"), animIds = new Ids("anim_");
4174
4223
  const ROOT = "origin";
4224
+ boneIds.reserve(ROOT);
4175
4225
  const attachmentIdMap = /* @__PURE__ */ new Map();
4176
4226
  const attachmentId = (slot, name) => {
4177
4227
  const key = `${slot} ${name}`;
@@ -4196,13 +4246,17 @@ function importSpine(json, atlasText, opts = {}) {
4196
4246
  });
4197
4247
  }
4198
4248
  const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
4249
+ const selectedSkin = opts.skin ? skins.find((s) => s.name === opts.skin) : skins.find((s) => s.name === "default") ?? skins[0];
4250
+ if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s) => s.name).join(", ")}`);
4251
+ const lookupSkins = [selectedSkin, skins.find((s) => s.name === "default")].filter((s, i, list) => s && list.indexOf(s) === i);
4252
+ if (skins.length > 1) warnings.push(`Spine skin "${selectedSkin?.name}" selected; setup and attachment timelines are flattened to this skin with default fallback (no runtime skin switching)`);
4199
4253
  const frameSuffix = (seq, index) => {
4200
- const n = String((seq.start ?? 0) + index);
4254
+ const n = String((seq.start ?? 1) + index);
4201
4255
  return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
4202
4256
  };
4203
4257
  const sequences = /* @__PURE__ */ new Map();
4204
4258
  const skinnedName = /* @__PURE__ */ new Map();
4205
- for (const skin2 of skins) {
4259
+ for (const skin2 of lookupSkins) {
4206
4260
  const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
4207
4261
  for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
4208
4262
  for (const [name, a] of Object.entries(atts)) {
@@ -4211,7 +4265,10 @@ function importSpine(json, atlasText, opts = {}) {
4211
4265
  if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
4212
4266
  }
4213
4267
  }
4214
- const attachmentRef = (slot, name) => attachmentId(slot, skinnedName.get(`${slot} ${name}`) ?? name);
4268
+ const attachmentRef = (slot, name) => {
4269
+ const resolved = skinnedName.get(`${slot} ${name}`);
4270
+ return resolved === void 0 ? null : attachmentId(slot, resolved);
4271
+ };
4215
4272
  const spineSlots = json.slots ?? [];
4216
4273
  const slotIndexByName = new Map(spineSlots.map((s, i) => [s.name, i]));
4217
4274
  const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
@@ -4219,11 +4276,12 @@ function importSpine(json, atlasText, opts = {}) {
4219
4276
  id: slotIds.get(s.name),
4220
4277
  bone: boneIds.get(s.bone),
4221
4278
  z: i,
4222
- ...s.attachment ? { attachment: attachmentRef(s.name, s.attachment) } : {},
4279
+ ...s.attachment && attachmentRef(s.name, s.attachment) ? { attachment: attachmentRef(s.name, s.attachment) } : {},
4223
4280
  blend: blendOf(s.blend),
4224
4281
  color: hex8(s.color),
4225
4282
  alpha: 1
4226
4283
  }));
4284
+ if (spineSlots.some((s) => s.dark)) warnings.push("Spine setup dark two-color tint is not supported");
4227
4285
  const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
4228
4286
  const assets = /* @__PURE__ */ new Map();
4229
4287
  const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
@@ -4249,6 +4307,7 @@ function importSpine(json, atlasText, opts = {}) {
4249
4307
  return id;
4250
4308
  };
4251
4309
  const attachments = [];
4310
+ const clipDeforms = /* @__PURE__ */ new Map();
4252
4311
  const readVertices = (a, vertexCount2) => {
4253
4312
  const v = a.vertices ?? [];
4254
4313
  if (v.length === vertexCount2 * 2) {
@@ -4269,6 +4328,22 @@ function importSpine(json, atlasText, opts = {}) {
4269
4328
  }
4270
4329
  return { weights };
4271
4330
  };
4331
+ const meshGeometry = (skinName, slot, name, visiting = /* @__PURE__ */ new Set()) => {
4332
+ const key = JSON.stringify([skinName, slot, name]);
4333
+ if (visiting.has(key)) {
4334
+ warnings.push(`linked mesh cycle at "${skinName}/${slot}/${name}" skipped`);
4335
+ return void 0;
4336
+ }
4337
+ const raw = skins.find((s) => s.name === skinName)?.attachments?.[slot]?.[name];
4338
+ if (!raw) {
4339
+ warnings.push(`linked mesh parent "${skinName}/${slot}/${name}" not found`);
4340
+ return void 0;
4341
+ }
4342
+ if (!raw.parent) return raw;
4343
+ visiting.add(key);
4344
+ return meshGeometry(raw.skin ?? "default", slot, raw.parent, visiting);
4345
+ };
4346
+ const inheritedDeform = /* @__PURE__ */ new Map();
4272
4347
  for (const skin2 of skins) {
4273
4348
  const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
4274
4349
  for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
@@ -4280,7 +4355,7 @@ function importSpine(json, atlasText, opts = {}) {
4280
4355
  for (const [name, a] of Object.entries(atts)) {
4281
4356
  if (a.sequence) {
4282
4357
  sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
4283
- const base = a.path ?? name;
4358
+ const base = a.path ?? a.name ?? name;
4284
4359
  for (let i = 0; i < (a.sequence.count ?? 0); i++) {
4285
4360
  const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
4286
4361
  if (!asset) continue;
@@ -4303,7 +4378,7 @@ function importSpine(json, atlasText, opts = {}) {
4303
4378
  }
4304
4379
  const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
4305
4380
  if (type === "region") {
4306
- const asset = assetFor(a.path ?? name);
4381
+ const asset = assetFor(a.path ?? a.name ?? name);
4307
4382
  if (!asset) continue;
4308
4383
  attachments.push({
4309
4384
  id,
@@ -4319,21 +4394,48 @@ function importSpine(json, atlasText, opts = {}) {
4319
4394
  ...a.height ? { height: a.height } : {}
4320
4395
  });
4321
4396
  } else if (type === "mesh" || type === "linkedmesh") {
4322
- if (type === "linkedmesh") {
4323
- warnings.push(`linked mesh "${slotName}/${name}" skipped`);
4324
- continue;
4325
- }
4326
- const asset = assetFor(a.path ?? name);
4397
+ const geometry = a.parent ? meshGeometry(skin2.name, slotName, name) : a;
4398
+ if (!geometry) continue;
4399
+ const asset = assetFor(a.path ?? a.name ?? name);
4327
4400
  if (!asset) continue;
4328
- const n = (a.uvs?.length ?? 0) / 2;
4329
- attachments.push({ id, slot, type: "mesh", asset, uvs: a.uvs, triangles: a.triangles, ...readVertices(a, n), hull: a.hull ?? 0, ...a.width ? { width: a.width } : {}, ...a.height ? { height: a.height } : {} });
4401
+ const n = (geometry.uvs?.length ?? 0) / 2;
4402
+ attachments.push({ id, slot, type: "mesh", asset, uvs: geometry.uvs, triangles: geometry.triangles, ...readVertices(geometry, n), hull: geometry.hull ?? 0, ...geometry.width ? { width: geometry.width } : {}, ...geometry.height ? { height: geometry.height } : {} });
4403
+ if (a.parent && (a.timelines ?? a.deform ?? true)) {
4404
+ const parentSkin = a.skin ?? "default";
4405
+ inheritedDeform.set(id, attachmentId(slotName, (parentSkin === "default" ? "" : `${parentSkin}_`) + a.parent));
4406
+ }
4330
4407
  } else if (type === "path") {
4331
4408
  attachments.push({ id, slot, type: "path", ...readVertices(a, a.vertexCount ?? 0), lengths: a.lengths ?? [], closed: !!a.closed, constantSpeed: a.constantSpeed ?? true });
4409
+ } else if (type === "clipping") {
4410
+ const data = readVertices(a, a.vertexCount), n = a.vertexCount;
4411
+ const recipes = Array.from({ length: n }, (_, i) => [
4412
+ [[(i + n - 1) % n, 1 / 3], [i, 2 / 3]],
4413
+ [[i, 1]],
4414
+ [[i, 2 / 3], [(i + 1) % n, 1 / 3]]
4415
+ ]).flat();
4416
+ let geometry;
4417
+ if (data.weights) {
4418
+ const starts = [];
4419
+ let offset = 0;
4420
+ for (const list of data.weights) {
4421
+ starts.push(offset);
4422
+ offset += list.length * 2;
4423
+ }
4424
+ const mapping = recipes.flatMap((recipe) => recipe.flatMap(([index]) => data.weights[index].map((_, k) => starts[index] + k * 2)));
4425
+ geometry = { weights: recipes.map((recipe) => recipe.flatMap(([index, factor]) => data.weights[index].map((inf) => ({ ...inf, w: inf.w * factor })))) };
4426
+ clipDeforms.set(id, (offsets) => mapping.flatMap((index) => [offsets[index] ?? 0, offsets[index + 1] ?? 0]));
4427
+ } else {
4428
+ const expand = (values2) => recipes.flatMap((recipe) => [0, 1].map((axis) => recipe.reduce((sum, [index, factor]) => sum + (values2[index * 2 + axis] ?? 0) * factor, 0)));
4429
+ geometry = { vertices: expand(data.vertices) };
4430
+ clipDeforms.set(id, expand);
4431
+ }
4432
+ attachments.push({ id, slot, type: "path", ...geometry, closed: true, clipping: { ...a.end ? { end: slotIds.get(a.end) } : {} } });
4332
4433
  } else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
4333
4434
  }
4334
4435
  }
4335
4436
  }
4336
- const ik = (json.ik ?? []).map((c) => ({
4437
+ const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s) => s[kind]?.includes(c.name));
4438
+ const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
4337
4439
  id: ikIds.get(c.name),
4338
4440
  order: c.order ?? 0,
4339
4441
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4346,7 +4448,7 @@ function importSpine(json, atlasText, opts = {}) {
4346
4448
  stretch: !!c.stretch,
4347
4449
  uniform: !!c.uniform
4348
4450
  }));
4349
- const transform = (json.transform ?? []).map((c) => ({
4451
+ const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4350
4452
  id: tcIds.get(c.name),
4351
4453
  order: c.order ?? 0,
4352
4454
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4366,7 +4468,7 @@ function importSpine(json, atlasText, opts = {}) {
4366
4468
  mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
4367
4469
  mixShearY: c.mixShearY ?? c.shearMix ?? 1
4368
4470
  }));
4369
- const path = (json.path ?? []).map((c) => ({
4471
+ const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4370
4472
  id: pcIds.get(c.name),
4371
4473
  order: c.order ?? 0,
4372
4474
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4401,7 +4503,21 @@ function importSpine(json, atlasText, opts = {}) {
4401
4503
  for (let i = 0; i < frames.length; i++) {
4402
4504
  const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
4403
4505
  const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
4404
- keys.push({ t: r4(t), v: r4(abs(f[field] ?? def)), ease: ease2 });
4506
+ const curve = major >= 4 && Array.isArray(f.curve) ? f.curve.slice(comp * 4, comp * 4 + 4) : [];
4507
+ const value = f[field] ?? def;
4508
+ if (next && value === (next[field] ?? def) && curve.length === 4 && (curve[1] !== value || curve[3] !== value)) {
4509
+ const cubic = (p0, p1, p2, p3, u) => (1 - u) ** 3 * p0 + 3 * (1 - u) ** 2 * u * p1 + 3 * (1 - u) * u * u * p2 + u ** 3 * p3;
4510
+ const inner = Array.from({ length: 9 }, (_, k) => {
4511
+ const u = (k + 1) / 10;
4512
+ return { t: r4(cubic(t, curve[0], curve[2], next.time ?? 0, u)), v: r4(abs(cubic(value, curve[1], curve[3], value, u))), ease: "linear" };
4513
+ });
4514
+ if (inner.every((k, i2) => k.t > (i2 ? inner[i2 - 1].t : t) && k.t < (next.time ?? 0))) {
4515
+ keys.push({ t: r4(t), v: r4(abs(value)), ease: "linear" }, ...inner);
4516
+ continue;
4517
+ }
4518
+ warnings.push(`equal-endpoint ${field} curve at ${t}s has non-monotonic control times and could not be lowered`);
4519
+ }
4520
+ keys.push({ t: r4(t), v: r4(abs(value)), ease: ease2 });
4405
4521
  }
4406
4522
  return keys;
4407
4523
  };
@@ -4412,6 +4528,10 @@ function importSpine(json, atlasText, opts = {}) {
4412
4528
  const seen = (frames) => {
4413
4529
  for (const f of frames) duration = Math.max(duration, f.time ?? 0);
4414
4530
  };
4531
+ if (a.physics) {
4532
+ warnings.push(`${animName}: Spine physics timelines are not imported`);
4533
+ for (const timelines of Object.values(a.physics)) for (const frames of Object.values(timelines)) seen(frames);
4534
+ }
4415
4535
  for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
4416
4536
  const sb = spineBones[boneIndexByName.get(boneName)];
4417
4537
  if (!sb) {
@@ -4452,19 +4572,22 @@ function importSpine(json, atlasText, opts = {}) {
4452
4572
  if (kind === "attachment") {
4453
4573
  attachmentFrames.set(slotName, frames);
4454
4574
  tracks.push(track("slot", id, "attachment", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.name ? attachmentRef(slotName, f.name) : null, ease: "step" }))));
4455
- } else if (kind === "rgba" || kind === "color") {
4575
+ } else if (["rgba", "color", "rgb", "rgba2", "rgb2"].includes(kind)) {
4576
+ const setupAlpha = parseInt(hex8(spineSlots[slotIndexByName.get(slotName)].color).slice(7), 16) / 255;
4577
+ const rgbOnly = kind === "rgb" || kind === "rgb2";
4456
4578
  const rgbaOf = (f) => {
4457
- const c = hex8(f.color);
4458
- return [0, 2, 4, 6].map((k) => parseInt(c.slice(1 + k, 3 + k), 16) / 255);
4579
+ const c = hex8(f.color ?? f.light);
4580
+ return [0, 2, 4, 6].map((k, i) => i === 3 && rgbOnly ? setupAlpha : parseInt(c.slice(1 + k, 3 + k), 16) / 255);
4459
4581
  };
4582
+ if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
4460
4583
  tracks.push(track("slot", id, "color", frames.map((f, i) => {
4461
4584
  const rgba = rgbaOf(f), next = frames[i + 1];
4462
- return { t: r4(f.time ?? 0), v: rgba.map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, rgba[0], rgbaOf(next)[0]) : "linear" };
4463
- })));
4464
- } else if (kind === "rgb") {
4465
- tracks.push(track("slot", id, "color", frames.map((f) => {
4466
- const c = hex8((f.color ?? "ffffff") + "ff");
4467
- return { t: r4(f.time ?? 0), v: [0, 2, 4, 6].map((k) => r4(parseInt(c.slice(1 + k, 3 + k), 16) / 255)), ease: "linear" };
4585
+ return {
4586
+ t: r4(f.time ?? 0),
4587
+ v: rgba.map(r4),
4588
+ ease: "linear",
4589
+ ...next ? { componentEase: rgba.map((v, k) => rgbOnly && k === 3 ? "linear" : easeOf(f.curve, k, f.time ?? 0, next.time ?? 0, v, rgbaOf(next)[k])) } : {}
4590
+ };
4468
4591
  })));
4469
4592
  } else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
4470
4593
  else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
@@ -4503,15 +4626,30 @@ function importSpine(json, atlasText, opts = {}) {
4503
4626
  warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
4504
4627
  continue;
4505
4628
  }
4506
- const length = att.weights ? 2 * att.weights.reduce((s, l) => s + l.length, 0) : att.vertices?.length ?? 0;
4629
+ const rawClip = skins.find((s) => s.name === skinName)?.attachments?.[slotName]?.[attName];
4630
+ const clipData = clipDeforms.has(id) ? readVertices(rawClip, rawClip.vertexCount) : void 0;
4631
+ const data = clipData ?? att;
4632
+ const length = data.weights ? 2 * data.weights.reduce((s, l) => s + l.length, 0) : data.vertices?.length ?? 0;
4507
4633
  tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
4508
4634
  const full = new Array(length).fill(0);
4509
4635
  const off = f.offset ?? 0, vs = f.vertices ?? [];
4510
4636
  for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
4511
4637
  const next = frames[i + 1];
4512
- return { t: r4(f.time ?? 0), v: full.map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, 0, 1) : "linear" };
4638
+ return { t: r4(f.time ?? 0), v: (clipDeforms.get(id)?.(full) ?? full).map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, 0, 1) : "linear" };
4513
4639
  })));
4514
4640
  }
4641
+ for (let pass = 0; pass < inheritedDeform.size; pass++) {
4642
+ let changed = false;
4643
+ for (const [child, parent] of inheritedDeform) {
4644
+ if (tracks.some((t) => t.target === "deform" && t.id === child)) continue;
4645
+ const source = tracks.find((t) => t.target === "deform" && t.id === parent);
4646
+ if (source) {
4647
+ tracks.push({ ...source, id: child, keys: structuredClone(source.keys) });
4648
+ changed = true;
4649
+ }
4650
+ }
4651
+ if (!changed) break;
4652
+ }
4515
4653
  const drawOrderFrames = a.drawOrder ?? a.draworder;
4516
4654
  if (Array.isArray(drawOrderFrames)) {
4517
4655
  seen(drawOrderFrames);
@@ -4534,20 +4672,25 @@ function importSpine(json, atlasText, opts = {}) {
4534
4672
  for (const [name, frames] of Object.entries(a.ik ?? {})) {
4535
4673
  seen(frames);
4536
4674
  const id = ikIds.get(name);
4675
+ if (!ik.some((c) => c.id === id)) continue;
4537
4676
  tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
4538
4677
  tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
4539
4678
  tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
4540
4679
  tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
4541
4680
  tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
4542
4681
  }
4543
- const mixTrack = (target, id, frames, prop, old, comp) => track(target, id, prop, frames.map((f, i) => ({
4544
- t: r4(f.time ?? 0),
4545
- v: r4(f[prop] ?? f[old] ?? 1),
4546
- ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, f[prop] ?? f[old] ?? 1, frames[i + 1][prop] ?? frames[i + 1][old] ?? 1) : "linear"
4547
- })));
4682
+ const mixTrack = (target, id, frames, prop, old, comp) => {
4683
+ const value = (f) => f[prop] ?? (prop === "mixY" ? f.mixX : prop === "mixScaleY" ? f.mixScaleX : void 0) ?? f[old] ?? 1;
4684
+ return track(target, id, prop, frames.map((f, i) => ({
4685
+ t: r4(f.time ?? 0),
4686
+ v: r4(value(f)),
4687
+ ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, value(f), value(frames[i + 1])) : "linear"
4688
+ })));
4689
+ };
4548
4690
  for (const [name, frames] of Object.entries(a.transform ?? {})) {
4549
4691
  seen(frames);
4550
4692
  const id = tcIds.get(name);
4693
+ if (!transform.some((c) => c.id === id)) continue;
4551
4694
  const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
4552
4695
  for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
4553
4696
  }
@@ -4555,6 +4698,7 @@ function importSpine(json, atlasText, opts = {}) {
4555
4698
  const id = pcIds.get(name);
4556
4699
  for (const [kind, frames] of Object.entries(tl)) {
4557
4700
  seen(frames);
4701
+ if (!path.some((c) => c.id === id)) continue;
4558
4702
  if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
4559
4703
  else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
4560
4704
  tracks.push(mixTrack("path", id, frames, prop, old, comp));
@@ -4717,21 +4861,28 @@ async function importSpineProject(opts) {
4717
4861
  const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
4718
4862
  const atlasText = await readFile7(atlasPath, "utf8");
4719
4863
  const { pages } = parseAtlas(atlasText);
4720
- if (pages.length !== 1) throw new Error(`expected one atlas page, got ${pages.length}`);
4721
- const pageStem = join8(dirname6(atlasPath), pages[0].name.replace(/\.[a-z0-9]+$/i, ""));
4722
- const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), pages[0].name), pageStem + ".png", pageStem + ".ktx2", stem + ".png", stem + ".ktx2"], "atlas page image");
4864
+ if (!pages.length) throw new Error("atlas has no pages");
4865
+ if (opts.image && pages.length !== 1) throw new Error("image override requires a single-page atlas; multi-page atlases resolve each named page beside the atlas");
4723
4866
  const rigDir = dirname6(opts.rigPath);
4724
- let pngPath = imagePath;
4725
- if (extname(imagePath).toLowerCase() === ".ktx2") {
4726
- pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
4727
- if (!await exists(pngPath)) {
4728
- const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
4729
- await writeFile5(pngPath, png);
4867
+ const pagePaths = /* @__PURE__ */ new Map();
4868
+ for (const page2 of pages) {
4869
+ if (!opts.image && outsideDir(dirname6(atlasPath), page2.name)) throw new Error(`atlas page "${page2.name}" is outside the atlas folder (${dirname6(atlasPath)})`);
4870
+ const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
4871
+ const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), page2.name), pageStem + ".png", pageStem + ".ktx2", ...pages.length === 1 ? [stem + ".png", stem + ".ktx2"] : []], `atlas page image ${page2.name}`);
4872
+ let pngPath = imagePath;
4873
+ if (extname(imagePath).toLowerCase() === ".ktx2") {
4874
+ pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
4875
+ if (!await exists(pngPath)) {
4876
+ const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
4877
+ await writeFile5(pngPath, png);
4878
+ }
4730
4879
  }
4880
+ pagePaths.set(page2.name, pngPath);
4731
4881
  }
4732
- const pageImage = relative(rigDir, pngPath).split("\\").join("/");
4882
+ const pageImage = (page2) => relative2(rigDir, pagePaths.get(page2)).split("\\").join("/");
4733
4883
  const name = opts.name ?? basename4(stem);
4734
- return { ...importSpine(json, atlasText, { name, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: pngPath };
4884
+ const imagePaths = [...pagePaths.values()];
4885
+ return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
4735
4886
  }
4736
4887
 
4737
4888
  // src/rig-templates.ts
@@ -5264,13 +5415,14 @@ function attachToBone(doc, animation, bone, target, t) {
5264
5415
  if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
5265
5416
  const [x, y] = apply(invert(to), from.tx, from.ty);
5266
5417
  const id = `attach_${bone}_${target}`;
5418
+ const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
5267
5419
  const offset = {
5268
5420
  x,
5269
5421
  y,
5270
- rotation: wrap2(rotationOf(from) - rotationOf(to)),
5422
+ rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
5271
5423
  scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
5272
5424
  scaleY: Math.hypot(from.c, from.d) - Math.hypot(to.c, to.d),
5273
- shearY: wrap2((Math.atan2(from.d, from.c) - Math.atan2(from.b, from.a) - Math.atan2(to.d, to.c) + Math.atan2(to.b, to.a)) * 180 / Math.PI)
5425
+ shearY: wrap2((Math.atan2(from.d, from.c) - Math.atan2(from.b, from.a) - Math.atan2(to.d, to.c) + Math.atan2(to.b, to.a)) * 180 / Math.PI) * handedness
5274
5426
  };
5275
5427
  const order = Math.max(0, ...doc.ik.map((c) => c.order), ...doc.transform.map((c) => c.order), ...doc.path.map((c) => c.order)) + 1;
5276
5428
  let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
@@ -5420,8 +5572,8 @@ async function load(path) {
5420
5572
  }
5421
5573
  var save = (path, doc) => saveJson(path, doc);
5422
5574
  var exists2 = (p) => stat2(p).then(() => true, () => false);
5423
- var resolveFrom = (rigPath, p) => isAbsolute(p) ? p : join9(dirname8(rigPath), p);
5424
- var srcFor = (rigPath, file) => relative2(dirname8(rigPath), file).split("\\").join("/");
5575
+ var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
5576
+ var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
5425
5577
  var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
5426
5578
  async function pngInfo(rigPath, file) {
5427
5579
  const abs = resolveFrom(rigPath, file);
@@ -5533,10 +5685,11 @@ tool(
5533
5685
  var KeyIn = z3.object({
5534
5686
  t: z3.number().describe("seconds"),
5535
5687
  v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
5688
+ componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
5536
5689
  ease: Ease.optional().describe("into the next key: linear|step|ease_in|ease_out|ease_in_out|back_out|elastic_out|{bezier:[x1,y1,x2,y2]}; default ease_in_out; minimum_jerk for a rest-to-rest segment")
5537
5690
  });
5538
5691
  var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
5539
- var KEY_DOC = "Keys are {t, v, ease?}. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
5692
+ var KEY_DOC = "Keys are {t, v, ease?, componentEase?}; componentEase selects independent easing for numeric-array components. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
5540
5693
  tool(
5541
5694
  "rig_create",
5542
5695
  "Create a new empty rig document with a root bone at bottom-center.",
@@ -5660,19 +5813,20 @@ tool(
5660
5813
  );
5661
5814
  tool(
5662
5815
  "rig_import_spine",
5663
- "Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text; a KTX2 page is transcoded to PNG next to rig.json. Then use rig_get / rig_render_preview / rig_check as with any rig.",
5816
+ "Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text; named pages resolve beside the atlas, and KTX2 pages transcode to PNG next to rig.json. Read warnings for unsupported physics and dark tint. Then use rig_get / rig_render_preview / rig_check as with any rig.",
5664
5817
  {
5665
5818
  path: Path,
5666
5819
  json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
5667
5820
  atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
5668
5821
  image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
5822
+ skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
5669
5823
  name: z3.string().optional().describe("default: the json file name"),
5670
5824
  margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
5671
5825
  notes: z3.string().optional()
5672
5826
  },
5673
- async ({ path, json, atlas, image, name, margin, notes }) => {
5827
+ async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
5674
5828
  try {
5675
- const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
5829
+ const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
5676
5830
  const res = await create(path, parse(r.doc));
5677
5831
  if (r.warnings.length) res.text += `
5678
5832
  warnings:
@@ -6443,9 +6597,39 @@ tool(
6443
6597
  }
6444
6598
  }
6445
6599
  );
6600
+
6601
+ // src/rig-version.ts
6602
+ import { stat as stat3 } from "node:fs/promises";
6603
+ async function rigVersion(abs) {
6604
+ try {
6605
+ const s = await stat3(abs);
6606
+ return `${s.mtimeMs}:${s.size}`;
6607
+ } catch {
6608
+ return "missing";
6609
+ }
6610
+ }
6611
+ async function callToolVersioned(name, args) {
6612
+ const path = typeof args.path === "string" ? args.path : void 0;
6613
+ if (!path) return { result: await callTool(name, args), before: "missing", after: "missing" };
6614
+ return withRigQueue(path, async () => {
6615
+ const before = await rigVersion(path);
6616
+ const result = await callTool(name, args);
6617
+ return { result, before, after: await rigVersion(path) };
6618
+ });
6619
+ }
6620
+
6621
+ // src/tool-schema.ts
6622
+ import { zodToJsonSchema } from "zod-to-json-schema";
6623
+ function jsonSchemaOf(tool2) {
6624
+ const { $schema: _drop, ...schema } = zodToJsonSchema(tool2.input, { $refStrategy: "none" });
6625
+ return schema;
6626
+ }
6446
6627
  export {
6447
6628
  callTool,
6629
+ callToolVersioned,
6448
6630
  closeTools,
6631
+ jsonSchemaOf,
6632
+ rigVersion,
6449
6633
  tools
6450
6634
  };
6451
6635
  //# sourceMappingURL=tools.js.map