@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
@@ -1,8 +1,8 @@
1
1
  // src/editor/server.ts
2
2
  import { createServer } from "node:http";
3
- import { readFile as readFile10, stat as stat3, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
3
+ import { readFile as readFile10, stat as stat4, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
4
4
  import { watch as fsWatch } from "node:fs";
5
- import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as relative3 } from "node:path";
5
+ import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep as sep2, join as join10, relative as relative4 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7
7
 
8
8
  // src/rig-format.ts
@@ -173,7 +173,9 @@ var PathAttachment = z.object({
173
173
  /** curve lengths for constant-speed positioning */
174
174
  lengths: z.array(z.number()).default([]),
175
175
  closed: z.boolean().default(false),
176
- constantSpeed: z.boolean().default(true)
176
+ constantSpeed: z.boolean().default(true),
177
+ /** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
178
+ clipping: z.object({ end: Id.optional() }).optional()
177
179
  });
178
180
  var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
179
181
  var IkConstraint = z.object({
@@ -242,7 +244,9 @@ var Key = z.object({
242
244
  t: z.number().nonnegative(),
243
245
  v: KeyValue,
244
246
  /** easing INTO the next key */
245
- ease: Ease.default("ease_in_out")
247
+ ease: Ease.default("ease_in_out"),
248
+ /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
249
+ componentEase: z.array(Ease).min(1).optional()
246
250
  });
247
251
  var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
248
252
  var Track = z.discriminatedUnion("target", [
@@ -429,6 +433,10 @@ function validate(doc) {
429
433
  else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
430
434
  }
431
435
  }
436
+ for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
437
+ if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
438
+ if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
439
+ }
432
440
  const vertexCount2 = (a) => {
433
441
  if (a.type === "region") return 4;
434
442
  if (a.type === "mesh") return a.uvs.length / 2;
@@ -512,6 +520,7 @@ function validate(doc) {
512
520
  const att = t.target === "deform" ? attachments.get(t.id) : void 0;
513
521
  let prev = -1;
514
522
  for (const k of t.keys) {
523
+ 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`);
515
524
  if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
516
525
  if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
517
526
  if (t.target === "control") {
@@ -559,7 +568,7 @@ function parse(input) {
559
568
  // src/rig-api.ts
560
569
  import { z as z3 } from "zod";
561
570
  import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
562
- import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
571
+ import { dirname as dirname8, join as join9, relative as relative3, isAbsolute as isAbsolute2 } from "node:path";
563
572
 
564
573
  // src/rig-io.ts
565
574
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
@@ -582,8 +591,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
582
591
  // src/rig-queue.ts
583
592
  import { AsyncLocalStorage } from "node:async_hooks";
584
593
  import { resolve } from "node:path";
585
- var pending = /* @__PURE__ */ new Map();
586
- var active = new AsyncLocalStorage();
594
+ var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
595
+ pending: /* @__PURE__ */ new Map(),
596
+ active: new AsyncLocalStorage()
597
+ };
587
598
  async function withRigQueue(path, op) {
588
599
  const key = resolve(path);
589
600
  const held = active.getStore();
@@ -739,8 +750,8 @@ function sample(track, t, loop = false) {
739
750
  }
740
751
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
741
752
  if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
742
- const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
743
- return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
753
+ const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
754
+ return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
744
755
  }
745
756
  return a.v;
746
757
  }
@@ -832,6 +843,13 @@ function boneWorldOf(bone, s, parent) {
832
843
  if (!parent) return local;
833
844
  if (bone.inherit === "normal") return mul(parent, local);
834
845
  const [tx, ty] = apply(parent, s.x, s.y);
846
+ if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
847
+ 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);
848
+ const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
849
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
850
+ const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
851
+ return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
852
+ }
835
853
  return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
836
854
  }
837
855
  var orderCache = /* @__PURE__ */ new WeakMap();
@@ -864,11 +882,11 @@ var localOf = (doc, pose, b) => {
864
882
  shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
865
883
  };
866
884
  };
867
- function updateSubtree(skel, root2) {
885
+ function updateSubtree(skel, root2, includeRoot = true) {
868
886
  const doc = skel.doc;
869
887
  const kids = /* @__PURE__ */ new Map();
870
888
  for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
871
- const stack = [root2];
889
+ const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
872
890
  while (stack.length) {
873
891
  const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
874
892
  st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
@@ -1111,7 +1129,8 @@ function applyTransformConstraint(skel, c) {
1111
1129
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
1112
1130
  c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
1113
1131
  const target = skel.bones.get(c.target), tw = target.world;
1114
- const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
1132
+ const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
1133
+ const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
1115
1134
  for (const id of c.bones) {
1116
1135
  const b = boneOf(skel.doc, id), st = skel.bones.get(id);
1117
1136
  if (c.local) {
@@ -1273,6 +1292,7 @@ function applyPathConstraint(skel, c) {
1273
1292
  const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
1274
1293
  const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
1275
1294
  const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
1295
+ const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
1276
1296
  const spacesCount = tangents ? bones.length : bones.length + 1;
1277
1297
  const spaces = new Array(spacesCount).fill(0), worldLengths = [];
1278
1298
  let sum = 0;
@@ -1307,9 +1327,10 @@ function applyPathConstraint(skel, c) {
1307
1327
  if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
1308
1328
  }
1309
1329
  let boneX = samples[0].x, boneY = samples[0].y;
1330
+ const solvedWorlds = /* @__PURE__ */ new Map();
1310
1331
  for (let i = 0; i < bones.length; i++) {
1311
1332
  const { b, st } = bones[i];
1312
- let w = { ...st.world };
1333
+ let w = { ...inputWorlds[i] };
1313
1334
  w.tx += (boneX - w.tx) * mixX;
1314
1335
  w.ty += (boneY - w.ty) * mixY;
1315
1336
  const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
@@ -1339,7 +1360,11 @@ function applyPathConstraint(skel, c) {
1339
1360
  const cos = Math.cos(r), sin = Math.sin(r);
1340
1361
  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 };
1341
1362
  }
1342
- setEffectiveWorld(skel, b, st, w);
1363
+ solvedWorlds.set(b.id, w);
1364
+ }
1365
+ for (const b of boneOrder(skel.doc)) {
1366
+ const world = solvedWorlds.get(b.id);
1367
+ if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
1343
1368
  }
1344
1369
  }
1345
1370
  function measuredPathLength(points, closed, lengths, constantSpeed) {
@@ -1368,9 +1393,14 @@ function inheritedParentLinear(bone, parent) {
1368
1393
  case "onlyTranslation":
1369
1394
  return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
1370
1395
  case "noRotationOrReflection": {
1371
- const lenX = Math.hypot(parent.a, parent.b) || 1;
1396
+ const lenX = Math.hypot(parent.a, parent.b);
1372
1397
  const det = parent.a * parent.d - parent.b * parent.c;
1373
- return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };
1398
+ if (lenX * lenX <= 1e-4) {
1399
+ const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
1400
+ return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
1401
+ }
1402
+ const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
1403
+ 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 };
1374
1404
  }
1375
1405
  case "noScale":
1376
1406
  case "noScaleOrReflection": {
@@ -1396,7 +1426,16 @@ function setEffectiveWorld(skel, bone, st, world) {
1396
1426
  st.scaleY = Math.hypot(lin.c, lin.d);
1397
1427
  st.shearX = 0;
1398
1428
  st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
1399
- updateSubtree(skel, bone.id);
1429
+ if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
1430
+ const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
1431
+ st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
1432
+ st.scaleX = Math.hypot(world.a, world.b);
1433
+ st.scaleY = Math.hypot(world.c, world.d);
1434
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
1435
+ st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
1436
+ }
1437
+ st.world = world;
1438
+ updateSubtree(skel, bone.id, false);
1400
1439
  }
1401
1440
  function attachmentWorldVertices(skel, slotId, att) {
1402
1441
  const deform = skel.pose.get(pk("deform", att.id, "vertices"));
@@ -1971,8 +2010,9 @@ function createDoc(name, width, height, notes) {
1971
2010
  function createSymbol(name, asset, margin = 0.15, notes) {
1972
2011
  const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
1973
2012
  const width = asset.width + 2 * mx, height = asset.height + 2 * my;
1974
- const fromFile = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/^[^a-z]+/, "");
1975
- const id = asset.id ?? (fromFile || name);
2013
+ const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
2014
+ const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
2015
+ const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
1976
2016
  let doc = createDoc(name, width, height, notes);
1977
2017
  doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
1978
2018
  doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
@@ -2155,7 +2195,7 @@ function removeSlot(doc, id) {
2155
2195
  return patchDrawOrder({
2156
2196
  ...doc,
2157
2197
  slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
2158
- attachments: doc.attachments.filter((a) => a.slot !== id),
2198
+ attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
2159
2199
  animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
2160
2200
  }, (o) => o.filter((s) => s !== id));
2161
2201
  }
@@ -2551,7 +2591,7 @@ function rename2(doc, kind, id, to) {
2551
2591
  return {
2552
2592
  ...doc,
2553
2593
  slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
2554
- attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
2594
+ attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
2555
2595
  path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
2556
2596
  };
2557
2597
  }
@@ -2728,7 +2768,7 @@ function setGoal(doc, bone, x, y, opts) {
2728
2768
 
2729
2769
  // src/rig-import-layers.ts
2730
2770
  import { readdir, readFile } from "node:fs/promises";
2731
- import { join as join2, basename as basename2 } from "node:path";
2771
+ import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
2732
2772
  var DEFAULT_PARENT = {
2733
2773
  head: "neck",
2734
2774
  neck: "torso",
@@ -2754,6 +2794,10 @@ function pngSize(buf) {
2754
2794
  if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
2755
2795
  return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
2756
2796
  }
2797
+ function outsideDir(dir, file) {
2798
+ const r = relative(dir, join2(dir, file));
2799
+ return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
2800
+ }
2757
2801
  async function importLayers(doc, dir, relPrefix = "layers") {
2758
2802
  let specs;
2759
2803
  try {
@@ -2766,6 +2810,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
2766
2810
  let next = doc;
2767
2811
  const parsed = [];
2768
2812
  for (const s of specs) {
2813
+ if (outsideDir(dir, s.file)) throw new Error(`layers.json file "${s.file}" is outside the layers folder (${dir})`);
2769
2814
  const buf = await readFile(join2(dir, s.file));
2770
2815
  const [w, h] = pngSize(buf);
2771
2816
  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,21 +4202,26 @@ 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;
4163
4211
  var hex8 = (c) => `#${(c ?? "ffffffff").toLowerCase().padEnd(8, "f").slice(0, 8)}`;
4164
- function importSpine(json, atlasText, opts = {}) {
4212
+ function importSpine(json2, atlasText, opts = {}) {
4165
4213
  const warnings = [];
4166
4214
  const atlas = parseAtlas(atlasText);
4167
- const version = String(json.skeleton?.spine ?? "4.0");
4215
+ const version = String(json2.skeleton?.spine ?? "4.0");
4216
+ if (json2.physics?.length) warnings.push(`${json2.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
4168
4217
  const major = parseFloat(version);
4169
- const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
4218
+ const bounds = { x: json2.skeleton?.x ?? 0, y: json2.skeleton?.y ?? 0, w: json2.skeleton?.width ?? 0, h: json2.skeleton?.height ?? 0 };
4170
4219
  const margin = opts.margin ?? 0;
4171
4220
  let originX = -bounds.x + margin, originY = bounds.y + bounds.h + margin;
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}`;
@@ -4182,7 +4232,7 @@ function importSpine(json, atlasText, opts = {}) {
4182
4232
  }
4183
4233
  return id;
4184
4234
  };
4185
- const spineBones = json.bones ?? [];
4235
+ const spineBones = json2.bones ?? [];
4186
4236
  const boneIndexByName = new Map(spineBones.map((b, i) => [b.name, i]));
4187
4237
  const bones = [{ id: ROOT, transform: { x: r4(originX), y: r4(originY) }, role: "root" }];
4188
4238
  for (const b of spineBones) {
@@ -4195,14 +4245,18 @@ function importSpine(json, atlasText, opts = {}) {
4195
4245
  ...b.color ? { color: b.color } : {}
4196
4246
  });
4197
4247
  }
4198
- const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
4248
+ const skins = Array.isArray(json2.skins) ? json2.skins : Object.entries(json2.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,19 +4265,23 @@ 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);
4215
- const spineSlots = json.slots ?? [];
4268
+ const attachmentRef = (slot, name) => {
4269
+ const resolved = skinnedName.get(`${slot} ${name}`);
4270
+ return resolved === void 0 ? null : attachmentId(slot, resolved);
4271
+ };
4272
+ const spineSlots = json2.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";
4218
4275
  const slots = spineSlots.map((s, i) => ({
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 = (json2.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 = (json2.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 = (json2.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,17 +4503,35 @@ 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
  };
4408
4524
  const animations = [];
4409
- for (const [animName, a] of Object.entries(json.animations ?? {})) {
4525
+ for (const [animName, a] of Object.entries(json2.animations ?? {})) {
4410
4526
  const tracks = [], events = [];
4411
4527
  let duration = 0;
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));
@@ -4659,7 +4803,7 @@ function importSpine(json, atlasText, opts = {}) {
4659
4803
  const build = () => ({
4660
4804
  format: "energy8.rig",
4661
4805
  version: 2,
4662
- meta: { name: opts.name ?? json.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
4806
+ meta: { name: opts.name ?? json2.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
4663
4807
  assets: [...assets.values()],
4664
4808
  bones,
4665
4809
  slots,
@@ -4712,26 +4856,33 @@ async function firstExisting(candidates, what) {
4712
4856
  throw new Error(`${what} not found, tried: ${candidates.join(", ")}`);
4713
4857
  }
4714
4858
  async function importSpineProject(opts) {
4715
- const json = JSON.parse(await readFile7(opts.json, "utf8"));
4859
+ const json2 = JSON.parse(await readFile7(opts.json, "utf8"));
4716
4860
  const stem = opts.json.replace(/\.json$/i, "");
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(json2, 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 });
@@ -5419,8 +5571,8 @@ async function load(path) {
5419
5571
  }
5420
5572
  var save = (path, doc) => saveJson(path, doc);
5421
5573
  var exists2 = (p) => stat2(p).then(() => true, () => false);
5422
- var resolveFrom = (rigPath, p) => isAbsolute(p) ? p : join9(dirname8(rigPath), p);
5423
- var srcFor = (rigPath, file) => relative2(dirname8(rigPath), file).split("\\").join("/");
5574
+ var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
5575
+ var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
5424
5576
  var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
5425
5577
  async function pngInfo(rigPath, file) {
5426
5578
  const abs = resolveFrom(rigPath, file);
@@ -5532,10 +5684,11 @@ tool(
5532
5684
  var KeyIn = z3.object({
5533
5685
  t: z3.number().describe("seconds"),
5534
5686
  v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
5687
+ componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
5535
5688
  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")
5536
5689
  });
5537
5690
  var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
5538
- 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].";
5691
+ 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].";
5539
5692
  tool(
5540
5693
  "rig_create",
5541
5694
  "Create a new empty rig document with a root bone at bottom-center.",
@@ -5659,19 +5812,20 @@ tool(
5659
5812
  );
5660
5813
  tool(
5661
5814
  "rig_import_spine",
5662
- "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.",
5815
+ "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.",
5663
5816
  {
5664
5817
  path: Path,
5665
5818
  json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
5666
5819
  atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
5667
5820
  image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
5821
+ skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
5668
5822
  name: z3.string().optional().describe("default: the json file name"),
5669
5823
  margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
5670
5824
  notes: z3.string().optional()
5671
5825
  },
5672
- async ({ path, json, atlas, image, name, margin, notes }) => {
5826
+ async ({ path, json: json2, atlas, image, name, skin: skin2, margin, notes }) => {
5673
5827
  try {
5674
- const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
5828
+ const r = await importSpineProject({ json: resolveFrom(path, json2), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
5675
5829
  const res = await create(path, parse(r.doc));
5676
5830
  if (r.warnings.length) res.text += `
5677
5831
  warnings:
@@ -6443,40 +6597,41 @@ tool(
6443
6597
  }
6444
6598
  );
6445
6599
 
6600
+ // src/rig-version.ts
6601
+ import { stat as stat3 } from "node:fs/promises";
6602
+ async function rigVersion(abs) {
6603
+ try {
6604
+ const s = await stat3(abs);
6605
+ return `${s.mtimeMs}:${s.size}`;
6606
+ } catch {
6607
+ return "missing";
6608
+ }
6609
+ }
6610
+
6446
6611
  // src/editor/server.ts
6447
6612
  var TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".map": "application/json", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp" };
6448
- async function startEditorServer(opts) {
6613
+ var json = (res, status, body) => {
6614
+ res.writeHead(status, { "content-type": "application/json" });
6615
+ res.end(JSON.stringify(body));
6616
+ };
6617
+ function createEditorHandler(opts) {
6449
6618
  const root2 = resolve2(opts.root);
6450
6619
  const assets = resolve2(opts.assets ?? fileURLToPath3(new URL("../..", import.meta.url)));
6451
6620
  const inside = (rel) => {
6452
6621
  const abs = resolve2(root2, rel);
6453
- return abs === root2 || abs.startsWith(root2 + sep) ? abs : void 0;
6454
- };
6455
- const version = async (abs) => {
6456
- try {
6457
- const s = await stat3(abs);
6458
- return `${s.mtimeMs}:${s.size}`;
6459
- } catch {
6460
- return "missing";
6461
- }
6622
+ return abs === root2 || abs.startsWith(root2 + sep2) ? abs : void 0;
6462
6623
  };
6463
6624
  const readDoc = async (abs) => RigDocument.parse(JSON.parse(await readFile10(abs, "utf8")));
6464
6625
  const readDocAt = async (abs) => {
6465
- let v = await version(abs);
6626
+ let v = await rigVersion(abs);
6466
6627
  for (let i = 0; i < 3; i++) {
6467
6628
  const doc = await readDoc(abs);
6468
- const after = await version(abs);
6629
+ const after = await rigVersion(abs);
6469
6630
  if (after === v) return { doc, version: v };
6470
6631
  v = after;
6471
6632
  }
6472
6633
  return { doc: await readDoc(abs), version: v };
6473
6634
  };
6474
- const json = (res, status, body2) => {
6475
- res.writeHead(status, { "content-type": "application/json" });
6476
- res.end(JSON.stringify(body2));
6477
- };
6478
- let allowedHosts = /* @__PURE__ */ new Set();
6479
- const hostOk = (req) => allowedHosts.has(String(req.headers.host ?? "").toLowerCase());
6480
6635
  const clients = /* @__PURE__ */ new Map();
6481
6636
  const watchers = /* @__PURE__ */ new Map();
6482
6637
  function ensureWatch(abs) {
@@ -6488,7 +6643,7 @@ async function startEditorServer(opts) {
6488
6643
  const schedule = () => {
6489
6644
  clearTimeout(entry.timer);
6490
6645
  entry.timer = setTimeout(async () => {
6491
- const v = await version(abs);
6646
+ const v = await rigVersion(abs);
6492
6647
  if (v === entry.last) return;
6493
6648
  entry.last = v;
6494
6649
  for (const res of clients.get(abs) ?? []) res.write(`event: changed
@@ -6497,7 +6652,7 @@ data: ${JSON.stringify({ version: v })}
6497
6652
  `);
6498
6653
  }, 50);
6499
6654
  };
6500
- version(abs).then((v) => {
6655
+ rigVersion(abs).then((v) => {
6501
6656
  entry.last = v;
6502
6657
  });
6503
6658
  watchers.set(abs, entry);
@@ -6523,7 +6678,7 @@ data: ${JSON.stringify({ version: v })}
6523
6678
  name = String(JSON.parse(await readFile10(abs, "utf8")).meta?.name ?? "?");
6524
6679
  } catch {
6525
6680
  }
6526
- out.push({ rig: relative3(root2, abs).split(sep).join("/"), name, mtimeMs: (await stat3(abs)).mtimeMs });
6681
+ out.push({ rig: relative4(root2, abs).split(sep2).join("/"), name, mtimeMs: (await stat4(abs)).mtimeMs });
6527
6682
  }
6528
6683
  };
6529
6684
  await walk(root2);
@@ -6557,20 +6712,22 @@ data: ${JSON.stringify({ version: v })}
6557
6712
  }
6558
6713
  return Buffer.concat(chunks);
6559
6714
  }
6560
- const server = createServer(async (req, res) => {
6715
+ async function handle(req, res, rawUrl = req.url ?? "/") {
6561
6716
  try {
6562
- if (!hostOk(req)) return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
6563
- const url = new URL(req.url ?? "/", "http://x");
6717
+ const url = new URL(rawUrl, "http://x");
6564
6718
  if (url.pathname === "/") {
6565
- res.writeHead(302, { location: "/editor.html" + url.search });
6566
- return res.end();
6719
+ res.writeHead(302, { location: "editor.html" + url.search });
6720
+ return void res.end();
6721
+ }
6722
+ if (url.pathname.startsWith("/api/")) {
6723
+ await api(url, req, res);
6724
+ return;
6567
6725
  }
6568
- if (url.pathname.startsWith("/api/")) return await api(url, req, res);
6569
6726
  const STATIC = /* @__PURE__ */ new Set(["/editor.html", "/dist/editor.js", "/dist/editor.js.map", "/dist/editor.css", "/dist/editor.css.map"]);
6570
6727
  const abs = url.pathname.startsWith("/rig/") ? inside(decodeURIComponent(url.pathname.slice(5))) : STATIC.has(url.pathname) ? join10(assets, url.pathname) : void 0;
6571
6728
  if (!abs) {
6572
6729
  res.writeHead(403);
6573
- return res.end("forbidden");
6730
+ return void res.end("forbidden");
6574
6731
  }
6575
6732
  try {
6576
6733
  const data = await readFile10(abs);
@@ -6588,7 +6745,7 @@ data: ${JSON.stringify({ version: v })}
6588
6745
  }
6589
6746
  json(res, 500, { isError: true, text: `ERROR: ${e.message}` });
6590
6747
  }
6591
- });
6748
+ }
6592
6749
  async function api(url, req, res) {
6593
6750
  const rigOf = (rel) => {
6594
6751
  if (!rel) return void 0;
@@ -6605,7 +6762,7 @@ data: ${JSON.stringify({ version: v })}
6605
6762
  res.writeHead(403);
6606
6763
  return res.end();
6607
6764
  }
6608
- if (!await stat3(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root2}` });
6765
+ if (!await stat4(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root2}` });
6609
6766
  ensureWatch(abs);
6610
6767
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
6611
6768
  res.write(`: connected
@@ -6627,7 +6784,7 @@ data: ${JSON.stringify({ version: v })}
6627
6784
  if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
6628
6785
  ensureWatch(abs);
6629
6786
  return await withRigQueue(abs, async () => {
6630
- if (baseVersion !== void 0 && await version(abs) !== baseVersion) {
6787
+ if (baseVersion !== void 0 && await rigVersion(abs) !== baseVersion) {
6631
6788
  const at = await readDocAt(abs);
6632
6789
  return json(res, 409, {
6633
6790
  isError: true,
@@ -6636,8 +6793,15 @@ data: ${JSON.stringify({ version: v })}
6636
6793
  text: `ERROR: the rig changed on disk (expected ${baseVersion}, found ${at.version}); nothing was written`
6637
6794
  });
6638
6795
  }
6639
- const r = await callTool(url.pathname.slice(10), { ...args ?? {}, path: abs });
6640
- return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await version(abs) });
6796
+ const tool2 = url.pathname.slice(10);
6797
+ let given = { ...args ?? {}, path: abs };
6798
+ try {
6799
+ if (opts.args) given = opts.args(tool2, given);
6800
+ } catch (e) {
6801
+ return json(res, 400, { isError: true, text: `ERROR: ${e instanceof Error ? e.message : String(e)}` });
6802
+ }
6803
+ const r = await callTool(tool2, given);
6804
+ return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await rigVersion(abs) });
6641
6805
  });
6642
6806
  }
6643
6807
  if (url.pathname === "/api/rigs" && req.method === "GET") return json(res, 200, await listRigs());
@@ -6648,11 +6812,11 @@ data: ${JSON.stringify({ version: v })}
6648
6812
  if (typeof rig !== "string" || !/(^|\/)rig\.json$/.test(rig)) return json(res, 400, { isError: true, text: "ERROR: a project is a directory with a rig.json \u2014 the path must end with /rig.json" });
6649
6813
  const abs = inside(rig);
6650
6814
  if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
6651
- if (await stat3(abs).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${rig} already exists \u2014 open it, or pick another directory` });
6815
+ if (await stat4(abs).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${rig} already exists \u2014 open it, or pick another directory` });
6652
6816
  await mkdir7(dirname9(abs), { recursive: true });
6653
6817
  const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
6654
6818
  ensureWatch(abs);
6655
- return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await version(abs) });
6819
+ return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await rigVersion(abs) });
6656
6820
  }
6657
6821
  if (url.pathname === "/api/files" && req.method === "GET") {
6658
6822
  const abs = rigOf(url.searchParams.get("rig"));
@@ -6666,8 +6830,8 @@ data: ${JSON.stringify({ version: v })}
6666
6830
  if (!/^[A-Za-z0-9._-]+\.png$/i.test(name) || name.startsWith(".")) return json(res, 400, { isError: true, text: `ERROR: bad file name "${name}" \u2014 letters, digits, . _ - and a .png extension` });
6667
6831
  if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("image/png")) return json(res, 415, { isError: true, text: "ERROR: upload needs content-type: image/png" });
6668
6832
  const target = join10(dirname9(abs), name);
6669
- if (!inside(relative3(root2, target))) return json(res, 403, { isError: true, text: "file outside root" });
6670
- if (await stat3(target).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${name} already exists next to the rig \u2014 rename the file` });
6833
+ if (!inside(relative4(root2, target))) return json(res, 403, { isError: true, text: "file outside root" });
6834
+ if (await stat4(target).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${name} already exists next to the rig \u2014 rename the file` });
6671
6835
  const data = await bytes(req, UPLOAD_MAX);
6672
6836
  if (!data) return json(res, 413, { isError: true, text: `ERROR: ${name} is larger than ${UPLOAD_MAX / 1048576} MB` });
6673
6837
  let size;
@@ -6681,6 +6845,22 @@ data: ${JSON.stringify({ version: v })}
6681
6845
  }
6682
6846
  json(res, 404, { isError: true, text: "unknown api route" });
6683
6847
  }
6848
+ return { handle, close() {
6849
+ for (const w of watchers.values()) {
6850
+ clearTimeout(w.timer);
6851
+ w.w.close();
6852
+ }
6853
+ for (const set of clients.values()) for (const c of set) c.end();
6854
+ } };
6855
+ }
6856
+ async function startEditorServer(opts) {
6857
+ const editor = createEditorHandler(opts);
6858
+ let allowedHosts = /* @__PURE__ */ new Set();
6859
+ const server = createServer((req, res) => {
6860
+ if (!allowedHosts.has(String(req.headers.host ?? "").toLowerCase()))
6861
+ return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
6862
+ void editor.handle(req, res);
6863
+ });
6684
6864
  await new Promise((ok, fail2) => {
6685
6865
  server.once("error", fail2);
6686
6866
  server.listen(opts.port ?? 8765, "127.0.0.1", () => {
@@ -6691,15 +6871,12 @@ data: ${JSON.stringify({ version: v })}
6691
6871
  const port = server.address().port;
6692
6872
  allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
6693
6873
  return { port, url: `http://127.0.0.1:${port}`, close: async () => {
6694
- for (const w of watchers.values()) {
6695
- clearTimeout(w.timer);
6696
- w.w.close();
6697
- }
6698
- for (const set of clients.values()) for (const c of set) c.end();
6874
+ editor.close();
6699
6875
  await new Promise((r) => server.close(() => r()));
6700
6876
  } };
6701
6877
  }
6702
6878
  export {
6879
+ createEditorHandler,
6703
6880
  startEditorServer
6704
6881
  };
6705
6882
  //# sourceMappingURL=editor-entry.js.map