@energy8platform/golem 0.6.0 → 0.8.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 (73) hide show
  1. package/README.md +14 -0
  2. package/dist/editor.css +3 -4
  3. package/dist/editor.js +202 -101
  4. package/dist/lib/cli.js +225 -118
  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 +6865 -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 +7005 -0
  16. package/dist/lib/e8-host.js.map +7 -0
  17. package/dist/lib/e8-runtime.js +2079 -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 +34 -32
  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 +226 -118
  28. package/dist/lib/editor-entry.js.map +4 -4
  29. package/dist/lib/harness.js +54 -1
  30. package/dist/lib/interactive-editor.js.map +1 -1
  31. package/dist/lib/rig-check.d.ts +11 -0
  32. package/dist/lib/rig-control-qa.d.ts +8 -8
  33. package/dist/lib/rig-format.d.ts +184 -184
  34. package/dist/lib/rig-import-layers.d.ts +2 -0
  35. package/dist/lib/rig-symbol.d.ts +5 -2
  36. package/dist/lib/rig-tools.d.ts +2 -1
  37. package/dist/lib/rig-version.d.ts +12 -0
  38. package/dist/lib/runtime.js +60 -3
  39. package/dist/lib/runtime.js.map +3 -3
  40. package/dist/lib/tool-schema.d.ts +3 -0
  41. package/dist/lib/tools.d.ts +2 -0
  42. package/dist/lib/tools.js +179 -58
  43. package/dist/lib/tools.js.map +4 -4
  44. package/editor.html +2 -2
  45. package/package.json +38 -3
  46. package/skills/e8-golem/SKILL.md +71 -0
  47. package/skills/golem-symbol-animation/SKILL.md +87 -0
  48. package/skills/golem-symbol-animation/agents/openai.yaml +4 -0
  49. package/skills/golem-symbol-animation/references/facial-controls.md +48 -0
  50. package/skills/golem-symbol-animation/references/game-engine-integration.md +133 -0
  51. package/skills/golem-symbol-animation/references/golem-authoring.md +138 -0
  52. package/skills/golem-symbol-animation/references/interactive-editor.md +17 -0
  53. package/skills/golem-symbol-animation/references/motion-craft.md +116 -0
  54. package/skills/golem-symbol-animation/references/packed-delivery.md +88 -0
  55. package/skills/golem-symbol-animation/references/quantitative-qa.md +109 -0
  56. package/skills/golem-symbol-animation/references/spine-rive-study.md +87 -0
  57. package/skills/golem-symbol-animation/scripts/atlas_parts.py +87 -0
  58. package/skills/golem-symbol-animation/scripts/check_package.py +71 -0
  59. package/skills/golem-symbol-animation/scripts/image_gates.py +143 -0
  60. package/skills/golem-symbol-cutting/SKILL.md +70 -0
  61. package/skills/golem-symbol-cutting/agents/openai.yaml +4 -0
  62. package/skills/golem-symbol-cutting/assets/h1/h1.atlas.png +0 -0
  63. package/skills/golem-symbol-cutting/assets/h1/h1.png +0 -0
  64. package/skills/golem-symbol-cutting/references/cutting-workflow.md +92 -0
  65. package/skills/golem-symbol-cutting/references/facial-layers.md +44 -0
  66. package/skills/golem-symbol-cutting/references/generation-workflow.md +109 -0
  67. package/skills/golem-symbol-cutting/references/golem-handoff.md +62 -0
  68. package/skills/golem-symbol-cutting/references/h1-example.md +46 -0
  69. package/skills/golem-symbol-cutting/references/hybrid-workflow.md +119 -0
  70. package/skills/golem-symbol-cutting/references/registration-and-motion.md +89 -0
  71. package/skills/golem-symbol-cutting/references/spine-rive-construction.md +73 -0
  72. package/skills/golem-symbol-cutting/scripts/atlas_parts.py +87 -0
  73. package/skills/golem-symbol-cutting/scripts/hybrid_parts.py +279 -0
package/dist/lib/cli.js CHANGED
@@ -3,9 +3,9 @@ import { resolve as resolve3 } from "node:path";
3
3
 
4
4
  // src/editor/server.ts
5
5
  import { createServer } from "node:http";
6
- import { readFile as readFile10, stat as stat3, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
6
+ import { readFile as readFile10, stat as stat4, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
7
7
  import { watch as fsWatch } from "node:fs";
8
- import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as relative3 } from "node:path";
8
+ 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";
9
9
  import { fileURLToPath as fileURLToPath3 } from "node:url";
10
10
 
11
11
  // src/rig-format.ts
@@ -251,7 +251,7 @@ var Key = z.object({
251
251
  /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
252
252
  componentEase: z.array(Ease).min(1).optional()
253
253
  });
254
- var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
254
+ var Interpolation = { interpolation: z.enum(["segment", "pchip", "spline"]).optional() };
255
255
  var Track = z.discriminatedUnion("target", [
256
256
  z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
257
257
  z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
@@ -571,7 +571,7 @@ function parse(input) {
571
571
  // src/rig-api.ts
572
572
  import { z as z3 } from "zod";
573
573
  import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
574
- import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
574
+ import { dirname as dirname8, join as join9, relative as relative3, isAbsolute as isAbsolute2 } from "node:path";
575
575
 
576
576
  // src/rig-io.ts
577
577
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
@@ -594,8 +594,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
594
594
  // src/rig-queue.ts
595
595
  import { AsyncLocalStorage } from "node:async_hooks";
596
596
  import { resolve } from "node:path";
597
- var pending = /* @__PURE__ */ new Map();
598
- var active = new AsyncLocalStorage();
597
+ var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
598
+ pending: /* @__PURE__ */ new Map(),
599
+ active: new AsyncLocalStorage()
600
+ };
599
601
  async function withRigQueue(path, op) {
600
602
  const key = resolve(path);
601
603
  const held = active.getStore();
@@ -735,6 +737,57 @@ function smoothValue(track, i, t, component, loop) {
735
737
  const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;
736
738
  return (2 * u3 - 3 * u2 + 1) * v(i) + (u3 - 2 * u2 + u) * h(i) * tangent(i) + (-2 * u3 + 3 * u2) * v(i + 1) + (u3 - u2) * h(i) * tangent(i + 1);
737
739
  }
740
+ function tridiagonal(a, b, c, r) {
741
+ const n = b.length, cp = [], x = [];
742
+ let beta = b[0];
743
+ x[0] = r[0] / beta;
744
+ for (let i = 1; i < n; i++) {
745
+ cp[i] = c[i - 1] / beta;
746
+ beta = b[i] - a[i] * cp[i];
747
+ x[i] = (r[i] - a[i] * x[i - 1]) / beta;
748
+ }
749
+ for (let i = n - 2; i >= 0; i--) x[i] -= cp[i + 1] * x[i + 1];
750
+ return x;
751
+ }
752
+ function splineMoments(t, v, periodic) {
753
+ const n = t.length, h = (j2) => t[j2 + 1] - t[j2], d = (j2) => (v[j2 + 1] - v[j2]) / h(j2);
754
+ if (!periodic) {
755
+ const a2 = [], b2 = [], c2 = [], r2 = [];
756
+ for (let i = 0; i < n; i++) {
757
+ const hp = i > 0 ? h(i - 1) : 0, hn = i < n - 1 ? h(i) : 0;
758
+ a2.push(hp);
759
+ b2.push(2 * (hp + hn));
760
+ c2.push(hn);
761
+ r2.push(6 * ((i < n - 1 ? d(i) : 0) - (i > 0 ? d(i - 1) : 0)));
762
+ }
763
+ return tridiagonal(a2, b2, c2, r2);
764
+ }
765
+ const m = n - 1, a = [], b = [], c = [], r = [];
766
+ for (let i = 0; i < m; i++) {
767
+ const p = (i + m - 1) % m;
768
+ a.push(h(p));
769
+ b.push(2 * (h(p) + h(i)));
770
+ c.push(h(i));
771
+ r.push(6 * (d(i) - d(p)));
772
+ }
773
+ const top = a[0], bottom = c[m - 1], gamma = -b[0];
774
+ const bb = [...b];
775
+ bb[0] -= gamma;
776
+ bb[m - 1] -= bottom * top / gamma;
777
+ const x = tridiagonal(a, bb, c, r), u = new Array(m).fill(0);
778
+ u[0] = gamma;
779
+ u[m - 1] = bottom;
780
+ const z4 = tridiagonal(a, bb, c, u);
781
+ const f = (x[0] + top * x[m - 1] / gamma) / (1 + z4[0] + top * z4[m - 1] / gamma);
782
+ const M = x.map((xi, i) => xi - f * z4[i]);
783
+ return [...M, M[0]];
784
+ }
785
+ function splineValue(track, i, t, component, loop) {
786
+ const k = track.keys, ts = k.map((x) => x.t), vs = k.map((x) => component === void 0 ? x.v : x.v[component]);
787
+ const M = splineMoments(ts, vs, loop && k.length > 3 && vs[0] === vs[k.length - 1]);
788
+ const H = ts[i + 1] - ts[i], A = (ts[i + 1] - t) / H, B = (t - ts[i]) / H;
789
+ return A * vs[i] + B * vs[i + 1] + ((A ** 3 - A) * M[i] + (B ** 3 - B) * M[i + 1]) * H * H / 6;
790
+ }
738
791
  function sample(track, t, loop = false) {
739
792
  const keys = track.keys;
740
793
  if (t <= keys[0].t) return keys[0].v;
@@ -746,11 +799,13 @@ function sample(track, t, loop = false) {
746
799
  if (a.ease === "step") return a.v;
747
800
  if (typeof a.v === "number" && typeof b.v === "number") {
748
801
  if (track.interpolation === "pchip") return smoothValue(track, i, t, void 0, loop);
802
+ if (track.interpolation === "spline") return splineValue(track, i, t, void 0, loop);
749
803
  const u = ease(a.ease)((t - a.t) / (b.t - a.t));
750
804
  return a.v + (b.v - a.v) * u;
751
805
  }
752
806
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
753
807
  if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
808
+ if (track.interpolation === "spline") return a.v.map((_, j2) => splineValue(track, i, t, j2, loop));
754
809
  const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
755
810
  return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
756
811
  }
@@ -2011,8 +2066,9 @@ function createDoc(name, width, height, notes) {
2011
2066
  function createSymbol(name, asset, margin = 0.15, notes) {
2012
2067
  const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
2013
2068
  const width = asset.width + 2 * mx, height = asset.height + 2 * my;
2014
- const fromFile = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/^[^a-z]+/, "");
2015
- const id = asset.id ?? (fromFile || name);
2069
+ const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
2070
+ const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
2071
+ const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
2016
2072
  let doc = createDoc(name, width, height, notes);
2017
2073
  doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
2018
2074
  doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
@@ -2481,7 +2537,8 @@ function adjustTrack(doc, animId, track, adj) {
2481
2537
  return { ...k, t: r42(adj.timeScale ? k.t * adj.timeScale : k.t), v: r42(v) };
2482
2538
  });
2483
2539
  const duration = Math.max(anim.duration, keys[keys.length - 1].t);
2484
- const next = { ...anim, duration, tracks: anim.tracks.map((t) => same(t) ? { ...t, keys } : t), preset: void 0 };
2540
+ const interpolation = track.interpolation ?? tr.interpolation;
2541
+ const next = { ...anim, duration, tracks: anim.tracks.map((t) => same(t) ? { ...t, keys, ...interpolation && { interpolation } } : t), preset: void 0 };
2485
2542
  return { ...doc, animations: doc.animations.map((a) => a.id === animId ? next : a) };
2486
2543
  }
2487
2544
  function removeAnimation(doc, id) {
@@ -2768,7 +2825,7 @@ function setGoal(doc, bone, x, y, opts) {
2768
2825
 
2769
2826
  // src/rig-import-layers.ts
2770
2827
  import { readdir, readFile } from "node:fs/promises";
2771
- import { join as join2, basename as basename2 } from "node:path";
2828
+ import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
2772
2829
  var DEFAULT_PARENT = {
2773
2830
  head: "neck",
2774
2831
  neck: "torso",
@@ -2794,6 +2851,10 @@ function pngSize(buf) {
2794
2851
  if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
2795
2852
  return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
2796
2853
  }
2854
+ function outsideDir(dir, file) {
2855
+ const r = relative(dir, join2(dir, file));
2856
+ return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
2857
+ }
2797
2858
  async function importLayers(doc, dir, relPrefix = "layers") {
2798
2859
  let specs;
2799
2860
  try {
@@ -2806,6 +2867,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
2806
2867
  let next = doc;
2807
2868
  const parsed = [];
2808
2869
  for (const s2 of specs) {
2870
+ if (outsideDir(dir, s2.file)) throw new Error(`layers.json file "${s2.file}" is outside the layers folder (${dir})`);
2809
2871
  const buf = await readFile(join2(dir, s2.file));
2810
2872
  const [w, h] = pngSize(buf);
2811
2873
  const m = basename2(s2.file, ".png").match(/^(?:(\d+)_)?(.+)$/);
@@ -2893,19 +2955,20 @@ var requireSlot = (doc, id) => {
2893
2955
  if (!s2) throw new Error(`unknown slot "${id}"`);
2894
2956
  return s2;
2895
2957
  };
2896
- function sampled(duration, n, f, ease2 = "linear") {
2958
+ function sampled(duration, n, f) {
2897
2959
  const keys = [];
2898
2960
  for (let i = 0; i <= n; i++) {
2899
2961
  const t = +(duration * i / n).toFixed(4);
2900
- keys.push({ t, v: +f(t).toFixed(4) + 0, ease: ease2 });
2962
+ keys.push({ t, v: +f(t).toFixed(4) + 0, ease: "linear" });
2901
2963
  }
2902
2964
  return keys;
2903
2965
  }
2904
- function boneTrack(bone, prop, keys) {
2905
- return { target: "bone", id: bone.id, prop, keys };
2966
+ var smooth = "spline";
2967
+ function boneTrack(bone, prop, keys, interpolation) {
2968
+ return { target: "bone", id: bone.id, prop, keys, ...interpolation && { interpolation } };
2906
2969
  }
2907
- function deformTrack(slot, prop, keys) {
2908
- return slot.attachment ? { target: "deform", id: slot.attachment, prop, keys } : void 0;
2970
+ function deformTrack(slot, prop, keys, interpolation) {
2971
+ return slot.attachment ? { target: "deform", id: slot.attachment, prop, keys, ...interpolation && { interpolation } } : void 0;
2909
2972
  }
2910
2973
  var pushTrack = (tracks, t) => {
2911
2974
  if (t) tracks.push(t);
@@ -2929,18 +2992,18 @@ var idleBreathing = {
2929
2992
  const armsR = byRole(doc, "arm_r", "arm_upper_r");
2930
2993
  const chest = slotOfBone(doc, torso);
2931
2994
  if (chest?.attachment) {
2932
- pushTrack(tracks, deformTrack(chest, "bulge", sampled(T2, 8, (t) => p.amplitude * Math.sin(w * t), "ease_in_out")));
2995
+ pushTrack(tracks, deformTrack(chest, "bulge", sampled(T2, 8, (t) => p.amplitude * Math.sin(w * t)), smooth));
2933
2996
  } else {
2934
2997
  const r = torso.transform;
2935
- tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t), "ease_in_out")));
2936
- tracks.push(boneTrack(torso, "scaleX", sampled(T2, 8, (t) => r.scaleX - p.amplitude * 0.4 * Math.sin(w * t), "ease_in_out")));
2998
+ tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t)), smooth));
2999
+ tracks.push(boneTrack(torso, "scaleX", sampled(T2, 8, (t) => r.scaleX - p.amplitude * 0.4 * Math.sin(w * t)), smooth));
2937
3000
  }
2938
3001
  for (const h of heads)
2939
- tracks.push(boneTrack(h, "y", sampled(T2, 8, (t) => h.transform.y - p.headBob * Math.sin(w * t + 0.4), "ease_in_out")));
3002
+ tracks.push(boneTrack(h, "y", sampled(T2, 8, (t) => h.transform.y - p.headBob * Math.sin(w * t + 0.4)), smooth));
2940
3003
  for (const a of armsL)
2941
- tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation + p.armSway * Math.sin(w * t + 0.8), "ease_in_out")));
3004
+ tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation + p.armSway * Math.sin(w * t + 0.8)), smooth));
2942
3005
  for (const a of armsR)
2943
- tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation - p.armSway * Math.sin(w * t + 0.8), "ease_in_out")));
3006
+ tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation - p.armSway * Math.sin(w * t + 0.8)), smooth));
2944
3007
  return finish(this.name, p.id, T2, true, tracks, p);
2945
3008
  }
2946
3009
  };
@@ -2949,22 +3012,20 @@ var winBounce = {
2949
3012
  defaults: { id: "win", duration: 1.2, height: 40, bounces: 2, squash: 0.15 },
2950
3013
  build(doc, p) {
2951
3014
  const b = root(doc), r = b.transform, tracks = [];
2952
- const yK = [[0, r.y, "ease_out"]];
2953
- const sxK = [[0, r.scaleX, "ease_out"]];
2954
- const syK = [[0, r.scaleY, "ease_out"]];
3015
+ const yK = [[0, r.y]], sxK = [[0, r.scaleX]], syK = [[0, r.scaleY]];
2955
3016
  const per = p.duration / p.bounces;
2956
3017
  for (let i = 0; i < p.bounces; i++) {
2957
3018
  const t0 = i * per, h = p.height * 0.55 ** i, s2 = p.squash * 0.7 ** i;
2958
- sxK.push([t0 + per * 0.08, r.scaleX * (1 + s2), "ease_out"], [t0 + per * 0.5, r.scaleX * (1 - s2 * 0.6), "ease_in"], [t0 + per * 0.95, r.scaleX * (1 + s2), "ease_out"]);
2959
- syK.push([t0 + per * 0.08, r.scaleY * (1 - s2), "ease_out"], [t0 + per * 0.5, r.scaleY * (1 + s2 * 0.6), "ease_in"], [t0 + per * 0.95, r.scaleY * (1 - s2), "ease_out"]);
2960
- yK.push([t0 + per * 0.08, r.y, "ease_out"], [t0 + per * 0.5, r.y - h, "ease_in"], [t0 + per * 0.95, r.y, "ease_out"]);
3019
+ sxK.push([t0 + per * 0.08, r.scaleX * (1 + s2)], [t0 + per * 0.5, r.scaleX * (1 - s2 * 0.6)], [t0 + per * 0.95, r.scaleX * (1 + s2)]);
3020
+ syK.push([t0 + per * 0.08, r.scaleY * (1 - s2)], [t0 + per * 0.5, r.scaleY * (1 + s2 * 0.6)], [t0 + per * 0.95, r.scaleY * (1 - s2)]);
3021
+ yK.push([t0 + per * 0.08, r.y], [t0 + per * 0.5, r.y - h], [t0 + per * 0.95, r.y]);
2961
3022
  }
2962
3023
  yK.push([p.duration, r.y]);
2963
3024
  sxK.push([p.duration, r.scaleX]);
2964
3025
  syK.push([p.duration, r.scaleY]);
2965
- tracks.push(boneTrack(b, "y", K(...yK)), boneTrack(b, "scaleX", K(...sxK)), boneTrack(b, "scaleY", K(...syK)));
3026
+ tracks.push(boneTrack(b, "y", K(...yK), "pchip"), boneTrack(b, "scaleX", K(...sxK), smooth), boneTrack(b, "scaleY", K(...syK), smooth));
2966
3027
  for (const h of byRole(doc, "head"))
2967
- tracks.push(boneTrack(h, "rotation", K([0, h.transform.rotation], [p.duration * 0.25, h.transform.rotation - 6, "back_out"], [p.duration * 0.6, h.transform.rotation + 4], [p.duration, h.transform.rotation])));
3028
+ tracks.push(boneTrack(h, "rotation", K([0, h.transform.rotation], [p.duration * 0.25, h.transform.rotation - 6], [p.duration * 0.6, h.transform.rotation + 4], [p.duration, h.transform.rotation]), smooth));
2968
3029
  const anim = finish(this.name, p.id, p.duration, false, tracks, p);
2969
3030
  anim.events.push({ t: 0, name: "win_start" }, { t: per * 0.95, name: "land" });
2970
3031
  return anim;
@@ -2977,8 +3038,8 @@ var anticipationShake = {
2977
3038
  const b = root(doc), r = b.transform, tracks = [];
2978
3039
  const n = Math.ceil(p.duration * p.frequency * 4);
2979
3040
  const env = (t) => p.grow ? t / p.duration : 1;
2980
- tracks.push(boneTrack(b, "x", sampled(p.duration, n, (t) => r.x + p.amplitude * env(t) * Math.sin(2 * Math.PI * p.frequency * t), "linear")));
2981
- tracks.push(boneTrack(b, "rotation", sampled(p.duration, n, (t) => r.rotation + 0.6 * p.amplitude * env(t) * Math.sin(2 * Math.PI * p.frequency * t + 1.2), "linear")));
3041
+ tracks.push(boneTrack(b, "x", sampled(p.duration, n, (t) => r.x + p.amplitude * env(t) * Math.sin(2 * Math.PI * p.frequency * t)), smooth));
3042
+ tracks.push(boneTrack(b, "rotation", sampled(p.duration, n, (t) => r.rotation + 0.6 * p.amplitude * env(t) * Math.sin(2 * Math.PI * p.frequency * t + 1.2)), smooth));
2982
3043
  tracks.push(boneTrack(b, "scaleX", K([0, r.scaleX, "ease_in"], [p.duration, r.scaleX * p.scaleUp])));
2983
3044
  tracks.push(boneTrack(b, "scaleY", K([0, r.scaleY, "ease_in"], [p.duration, r.scaleY * p.scaleUp])));
2984
3045
  return finish(this.name, p.id, p.duration, false, tracks, p);
@@ -3003,6 +3064,7 @@ var mouthTalk = {
3003
3064
  if (!p.frames.length) throw new Error("mouth_talk: frames must not be empty");
3004
3065
  const n = Math.max(1, Math.round(p.duration * p.rate));
3005
3066
  const keys = S(...Array.from({ length: n }, (_, i) => [+(i / p.rate).toFixed(4), p.frames[i % p.frames.length]]));
3067
+ if (keys.at(-1).t < p.duration && keys.at(-1).v !== keys[0].v) keys.push(...S([p.duration, keys[0].v]));
3006
3068
  return finish(this.name, p.id, p.duration, true, [slotTrack(slot, "attachment", keys)], p);
3007
3069
  }
3008
3070
  };
@@ -3020,10 +3082,10 @@ var symbolIdle = {
3020
3082
  build(doc, p) {
3021
3083
  const b = root(doc), r = b.transform, T2 = p.period, w = 2 * Math.PI / T2, tracks = [];
3022
3084
  const rise = (t) => 0.5 - 0.5 * Math.cos(w * t);
3023
- tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t), "ease_in_out")));
3024
- if (p.tilt) tracks.push(boneTrack(b, "rotation", sampled(T2, 8, (t) => r.rotation + p.tilt * Math.sin(w * t), "ease_in_out")));
3085
+ tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t)), smooth));
3086
+ if (p.tilt) tracks.push(boneTrack(b, "rotation", sampled(T2, 8, (t) => r.rotation + p.tilt * Math.sin(w * t)), smooth));
3025
3087
  if (p.pulse) for (const s2 of doc.slots.filter((s3) => s3.bone === b.id && s3.attachment))
3026
- pushTrack(tracks, deformTrack(s2, "bulge", sampled(T2, 8, (t) => p.pulse * rise(t), "ease_in_out")));
3088
+ pushTrack(tracks, deformTrack(s2, "bulge", sampled(T2, 8, (t) => p.pulse * rise(t)), smooth));
3027
3089
  return finish(this.name, p.id, T2, true, tracks, p);
3028
3090
  }
3029
3091
  };
@@ -3033,12 +3095,12 @@ var symbolWin = {
3033
3095
  build(doc, p) {
3034
3096
  const b = root(doc), r = b.transform, d = p.duration, tracks = [];
3035
3097
  for (const [prop, rest] of [["scaleX", r.scaleX], ["scaleY", r.scaleY]])
3036
- tracks.push(boneTrack(b, prop, K([0, rest, "ease_out"], [d * 0.2, rest * (1 + p.pop), "ease_in_out"], [d * 0.5, rest * (1 - p.pop * 0.3), "ease_out"], [d, rest])));
3098
+ tracks.push(boneTrack(b, prop, K([0, rest], [d * 0.2, rest * (1 + p.pop)], [d * 0.5, rest * (1 - p.pop * 0.3)], [d, rest]), smooth));
3037
3099
  if (p.wobble) {
3038
3100
  const n = Math.max(8, p.wobbles * 4);
3039
- const keys = sampled(d, n, (t) => p.wobble * Math.exp(-3 * t / d) * Math.sin(2 * Math.PI * p.wobbles * t / d), "ease_in_out");
3101
+ const keys = sampled(d, n, (t) => p.wobble * Math.exp(-3 * t / d) * Math.sin(2 * Math.PI * p.wobbles * t / d));
3040
3102
  keys[keys.length - 1].v = 0;
3041
- for (const s2 of doc.slots.filter((s3) => s3.bone === b.id && s3.attachment)) pushTrack(tracks, deformTrack(s2, "sway", keys));
3103
+ for (const s2 of doc.slots.filter((s3) => s3.bone === b.id && s3.attachment)) pushTrack(tracks, deformTrack(s2, "sway", keys, smooth));
3042
3104
  }
3043
3105
  const anim = finish(this.name, p.id, d, false, tracks, p);
3044
3106
  anim.events.push({ t: 0, name: "win_start" });
@@ -3052,11 +3114,11 @@ var weightShift = {
3052
3114
  const T2 = p.period, w = 2 * Math.PI / T2, N = 16, tracks = [];
3053
3115
  const hips = byRole(doc, "hips")[0] ?? root(doc), hr = hips.transform;
3054
3116
  const s2 = (t) => Math.sin(w * t);
3055
- tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s2(t))));
3056
- if (p.dip) tracks.push(boneTrack(hips, "y", sampled(T2, N, (t) => hr.y + p.dip * s2(t) ** 2)));
3117
+ tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s2(t)), smooth));
3118
+ if (p.dip) tracks.push(boneTrack(hips, "y", sampled(T2, N, (t) => hr.y + p.dip * s2(t) ** 2), smooth));
3057
3119
  const torso = byRole(doc, "torso", "body")[0];
3058
- if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s2(t))));
3059
- for (const h of byRole(doc, "head")) tracks.push(boneTrack(h, "rotation", sampled(T2, N, (t) => h.transform.rotation + p.lean / 2 * s2(t))));
3120
+ if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s2(t)), smooth));
3121
+ for (const h of byRole(doc, "head")) tracks.push(boneTrack(h, "rotation", sampled(T2, N, (t) => h.transform.rotation + p.lean / 2 * s2(t)), smooth));
3060
3122
  for (const leg of byRole(doc, "leg_l", "leg_r")) {
3061
3123
  const slot = slotOfBone(doc, leg), att = slot && doc.attachments.find((a) => a.id === slot.attachment && a.type === "region");
3062
3124
  const asset = att && att.type === "region" ? doc.assets.find((a) => a.id === att.asset) : void 0;
@@ -3066,8 +3128,8 @@ var weightShift = {
3066
3128
  length = Math.max(...[[0, -h / 2], [w2 / 2, 0], [0, h / 2], [-w2 / 2, 0]].map(([x, y]) => Math.hypot(...apply(m, x, y))));
3067
3129
  }
3068
3130
  if (!(length > p.shift)) continue;
3069
- tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s2(t) / length) * 180 / Math.PI)));
3070
- if (p.dip) tracks.push(boneTrack(leg, "y", sampled(T2, N, (t) => leg.transform.y - p.dip * s2(t) ** 2)));
3131
+ tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s2(t) / length) * 180 / Math.PI), smooth));
3132
+ if (p.dip) tracks.push(boneTrack(leg, "y", sampled(T2, N, (t) => leg.transform.y - p.dip * s2(t) ** 2), smooth));
3071
3133
  }
3072
3134
  return finish(this.name, p.id, T2, true, tracks, p);
3073
3135
  }
@@ -3310,13 +3372,13 @@ function motionQuality(doc, anim, opts = {}) {
3310
3372
  const before = delta(values(k.v), values(p.v)), after = delta(values(n.v), values(k.v));
3311
3373
  const pass = before.some((v, j2) => v * after[j2] > 1e-6);
3312
3374
  const speed = Math.max(magnitude(before) / (k.t - p.t), magnitude(after) / (n.t - k.t));
3313
- if (pass && Math.max(magnitude(left), magnitude(right)) < speed * 0.02) report("pass_key_stop", id, `motion stops at a pass-through key; use track interpolation pchip or matched tangents`, k.t);
3375
+ if (pass && Math.max(magnitude(left), magnitude(right)) < speed * 0.02) report("pass_key_stop", id, `motion stops at a pass-through key; use track interpolation pchip or spline, or matched tangents`, k.t);
3314
3376
  const jump = magnitude(delta(left, right));
3315
3377
  if (jump > Math.max(0.1, speed * 0.1)) report("velocity_jump", id, `velocity jumps ${jump.toFixed(3)} units/s across this key`, k.t);
3316
3378
  const acceleration = (a, b, c) => values(at(b)).map((v, j2) => (values(at(c))[j2] - 2 * v + values(at(a))[j2]) / (h * h));
3317
3379
  const al = acceleration(k.t - 2 * h, k.t - h, k.t), ar = acceleration(k.t, k.t + h, k.t + 2 * h);
3318
3380
  const aj = magnitude(delta(al, ar));
3319
- if (aj > Math.max(1, speed / Math.min(k.t - p.t, n.t - k.t) * 2)) report("acceleration_jump", id, `acceleration changes by ${aj.toFixed(2)} units/s\xB2 at this key; PCHIP guarantees velocity, not acceleration continuity`, k.t, "info");
3381
+ if (aj > Math.max(1, speed / Math.min(k.t - p.t, n.t - k.t) * 2)) report("acceleration_jump", id, `acceleration changes by ${aj.toFixed(2)} units/s\xB2 at this key; PCHIP guarantees velocity, not acceleration continuity \u2014 spline does both`, k.t, "info");
3320
3382
  }
3321
3383
  }
3322
3384
  const steps = Math.max(2, Math.ceil(D * 60));
@@ -3798,6 +3860,17 @@ function checkRig(doc, opts = {}) {
3798
3860
  out.push({ level: "warn", code: "never_visible", subject: s2.id, message: `slot "${s2.id}" has no attachment and no animation ever shows one` });
3799
3861
  return out;
3800
3862
  }
3863
+ function summarizeIgnoring(findings, rules, max) {
3864
+ const hits = (r, f) => r.code === f.code && (r.subject ?? f.subject) === f.subject && (r.animation ?? f.animation) === f.animation;
3865
+ const kept = findings.filter((f) => !rules.some((r) => hits(r, f)));
3866
+ const idle = rules.filter((r) => !findings.some((f) => hits(r, f)));
3867
+ const notes = [
3868
+ ...kept.length < findings.length ? [` (${findings.length - kept.length} ignored)`] : [],
3869
+ ...idle.map((r) => ` ignore matched nothing: ${JSON.stringify(r)}`)
3870
+ ];
3871
+ return [summarize(kept, max), ...notes].join("\n");
3872
+ }
3873
+ var where = (f) => `${f.subject}${f.animation ? ` in ${f.animation}` : ""}${f.time !== void 0 ? ` @${+f.time.toFixed(4)}s` : ""}`;
3801
3874
  function summarize(findings, max = 12) {
3802
3875
  if (!findings.length) return "check: OK";
3803
3876
  const rank = { error: 0, warn: 1, info: 2 };
@@ -3812,7 +3885,7 @@ function summarize(findings, max = 12) {
3812
3885
  const lines = [];
3813
3886
  for (const f of sorted) {
3814
3887
  const shown = shownPerCode.get(f.code) ?? 0;
3815
- if (shown < PER_CODE) lines.push(` [${f.level}] ${f.code}: ${f.message}`);
3888
+ if (shown < PER_CODE) lines.push(` [${f.level}] ${f.code} ${where(f)}: ${f.message}`);
3816
3889
  else if (shown === PER_CODE) lines.push(` \u2026 ${codeTotal.get(f.code) - PER_CODE} more ${f.code}`);
3817
3890
  shownPerCode.set(f.code, shown + 1);
3818
3891
  }
@@ -4003,7 +4076,7 @@ function packRects(rects, opts = {}) {
4003
4076
  shelf = Math.max(shelf, r.height);
4004
4077
  }
4005
4078
  const height = y + shelf + pad;
4006
- if (height <= max) return { width, height, frames };
4079
+ if (height <= max) return { width: Math.max(pad, ...[...frames.values()].map(([fx, , fw]) => fx + fw)) + pad, height, frames };
4007
4080
  if (width >= max) throw new Error(`atlas does not fit in ${max}\xD7${max} (needs ${width}\xD7${height})`);
4008
4081
  width *= 2;
4009
4082
  }
@@ -4062,7 +4135,7 @@ async function buildAtlas(doc, assetsDir, outDir, opts = {}) {
4062
4135
 
4063
4136
  // src/spine-import.ts
4064
4137
  import { readFile as readFile7, stat, writeFile as writeFile5 } from "node:fs/promises";
4065
- import { basename as basename4, dirname as dirname6, extname, join as join8, relative } from "node:path";
4138
+ import { basename as basename4, dirname as dirname6, extname, join as join8, relative as relative2 } from "node:path";
4066
4139
 
4067
4140
  // src/ktx2.ts
4068
4141
  import { readFile as readFile6 } from "node:fs/promises";
@@ -4211,13 +4284,13 @@ var Ids = class {
4211
4284
  var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
4212
4285
  var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
4213
4286
  var hex8 = (c) => `#${(c ?? "ffffffff").toLowerCase().padEnd(8, "f").slice(0, 8)}`;
4214
- function importSpine(json, atlasText, opts = {}) {
4287
+ function importSpine(json2, atlasText, opts = {}) {
4215
4288
  const warnings = [];
4216
4289
  const atlas = parseAtlas(atlasText);
4217
- const version = String(json.skeleton?.spine ?? "4.0");
4218
- if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
4290
+ const version = String(json2.skeleton?.spine ?? "4.0");
4291
+ if (json2.physics?.length) warnings.push(`${json2.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
4219
4292
  const major = parseFloat(version);
4220
- const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
4293
+ const bounds = { x: json2.skeleton?.x ?? 0, y: json2.skeleton?.y ?? 0, w: json2.skeleton?.width ?? 0, h: json2.skeleton?.height ?? 0 };
4221
4294
  const margin = opts.margin ?? 0;
4222
4295
  let originX = -bounds.x + margin, originY = bounds.y + bounds.h + margin;
4223
4296
  const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
@@ -4234,7 +4307,7 @@ function importSpine(json, atlasText, opts = {}) {
4234
4307
  }
4235
4308
  return id;
4236
4309
  };
4237
- const spineBones = json.bones ?? [];
4310
+ const spineBones = json2.bones ?? [];
4238
4311
  const boneIndexByName = new Map(spineBones.map((b, i) => [b.name, i]));
4239
4312
  const bones = [{ id: ROOT, transform: { x: r4(originX), y: r4(originY) }, role: "root" }];
4240
4313
  for (const b of spineBones) {
@@ -4247,7 +4320,7 @@ function importSpine(json, atlasText, opts = {}) {
4247
4320
  ...b.color ? { color: b.color } : {}
4248
4321
  });
4249
4322
  }
4250
- const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
4323
+ const skins = Array.isArray(json2.skins) ? json2.skins : Object.entries(json2.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
4251
4324
  const selectedSkin = opts.skin ? skins.find((s2) => s2.name === opts.skin) : skins.find((s2) => s2.name === "default") ?? skins[0];
4252
4325
  if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s2) => s2.name).join(", ")}`);
4253
4326
  const lookupSkins = [selectedSkin, skins.find((s2) => s2.name === "default")].filter((s2, i, list) => s2 && list.indexOf(s2) === i);
@@ -4271,7 +4344,7 @@ function importSpine(json, atlasText, opts = {}) {
4271
4344
  const resolved = skinnedName.get(`${slot} ${name}`);
4272
4345
  return resolved === void 0 ? null : attachmentId(slot, resolved);
4273
4346
  };
4274
- const spineSlots = json.slots ?? [];
4347
+ const spineSlots = json2.slots ?? [];
4275
4348
  const slotIndexByName = new Map(spineSlots.map((s2, i) => [s2.name, i]));
4276
4349
  const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
4277
4350
  const slots = spineSlots.map((s2, i) => ({
@@ -4437,7 +4510,7 @@ function importSpine(json, atlasText, opts = {}) {
4437
4510
  }
4438
4511
  }
4439
4512
  const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s2) => s2[kind]?.includes(c.name));
4440
- const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
4513
+ const ik = (json2.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
4441
4514
  id: ikIds.get(c.name),
4442
4515
  order: c.order ?? 0,
4443
4516
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4450,7 +4523,7 @@ function importSpine(json, atlasText, opts = {}) {
4450
4523
  stretch: !!c.stretch,
4451
4524
  uniform: !!c.uniform
4452
4525
  }));
4453
- const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4526
+ const transform = (json2.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4454
4527
  id: tcIds.get(c.name),
4455
4528
  order: c.order ?? 0,
4456
4529
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4470,7 +4543,7 @@ function importSpine(json, atlasText, opts = {}) {
4470
4543
  mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
4471
4544
  mixShearY: c.mixShearY ?? c.shearMix ?? 1
4472
4545
  }));
4473
- const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4546
+ const path = (json2.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4474
4547
  id: pcIds.get(c.name),
4475
4548
  order: c.order ?? 0,
4476
4549
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4524,7 +4597,7 @@ function importSpine(json, atlasText, opts = {}) {
4524
4597
  return keys;
4525
4598
  };
4526
4599
  const animations = [];
4527
- for (const [animName, a] of Object.entries(json.animations ?? {})) {
4600
+ for (const [animName, a] of Object.entries(json2.animations ?? {})) {
4528
4601
  const tracks = [], events = [];
4529
4602
  let duration = 0;
4530
4603
  const seen = (frames) => {
@@ -4805,7 +4878,7 @@ function importSpine(json, atlasText, opts = {}) {
4805
4878
  const build = () => ({
4806
4879
  format: "energy8.rig",
4807
4880
  version: 2,
4808
- meta: { name: opts.name ?? json.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
4881
+ meta: { name: opts.name ?? json2.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
4809
4882
  assets: [...assets.values()],
4810
4883
  bones,
4811
4884
  slots,
@@ -4858,7 +4931,7 @@ async function firstExisting(candidates, what) {
4858
4931
  throw new Error(`${what} not found, tried: ${candidates.join(", ")}`);
4859
4932
  }
4860
4933
  async function importSpineProject(opts) {
4861
- const json = JSON.parse(await readFile7(opts.json, "utf8"));
4934
+ const json2 = JSON.parse(await readFile7(opts.json, "utf8"));
4862
4935
  const stem = opts.json.replace(/\.json$/i, "");
4863
4936
  const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
4864
4937
  const atlasText = await readFile7(atlasPath, "utf8");
@@ -4868,6 +4941,7 @@ async function importSpineProject(opts) {
4868
4941
  const rigDir = dirname6(opts.rigPath);
4869
4942
  const pagePaths = /* @__PURE__ */ new Map();
4870
4943
  for (const page2 of pages) {
4944
+ if (!opts.image && outsideDir(dirname6(atlasPath), page2.name)) throw new Error(`atlas page "${page2.name}" is outside the atlas folder (${dirname6(atlasPath)})`);
4871
4945
  const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
4872
4946
  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}`);
4873
4947
  let pngPath = imagePath;
@@ -4880,10 +4954,10 @@ async function importSpineProject(opts) {
4880
4954
  }
4881
4955
  pagePaths.set(page2.name, pngPath);
4882
4956
  }
4883
- const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
4957
+ const pageImage = (page2) => relative2(rigDir, pagePaths.get(page2)).split("\\").join("/");
4884
4958
  const name = opts.name ?? basename4(stem);
4885
4959
  const imagePaths = [...pagePaths.values()];
4886
- return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
4960
+ return { ...importSpine(json2, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
4887
4961
  }
4888
4962
 
4889
4963
  // src/rig-templates.ts
@@ -5573,8 +5647,8 @@ async function load(path) {
5573
5647
  }
5574
5648
  var save = (path, doc) => saveJson(path, doc);
5575
5649
  var exists2 = (p) => stat2(p).then(() => true, () => false);
5576
- var resolveFrom = (rigPath, p) => isAbsolute(p) ? p : join9(dirname8(rigPath), p);
5577
- var srcFor = (rigPath, file) => relative2(dirname8(rigPath), file).split("\\").join("/");
5650
+ var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
5651
+ var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
5578
5652
  var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
5579
5653
  async function pngInfo(rigPath, file) {
5580
5654
  const abs = resolveFrom(rigPath, file);
@@ -5609,7 +5683,8 @@ function previewCheck(doc, animation) {
5609
5683
  const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
5610
5684
  return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
5611
5685
  }
5612
- var TrackRef = { interpolation: z3.enum(["segment", "pchip"]).optional().describe("pchip: smooth through numeric keys, preserving extrema; segment: per-key easing"), target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder", "control"]), id: z3.string(), prop: z3.string() };
5686
+ var TrackId = { target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder", "control"]), id: z3.string(), prop: z3.string() };
5687
+ var TrackRef = { interpolation: z3.enum(["segment", "pchip", "spline"]).optional().describe("segment: per-key easing; pchip: smooth through numeric keys, never past them (continuous velocity); spline: C2 cubic through them, continuous acceleration too, may overshoot between keys \u2014 for sampled curves"), ...TrackId };
5613
5688
  tool(
5614
5689
  "rig_set_control",
5615
5690
  "Create or replace a semantic control with a finite min/max/default range. Animate target=control, prop=value. Changing a range requires matching binding grids.",
@@ -5802,11 +5877,21 @@ tool(
5802
5877
  );
5803
5878
  tool(
5804
5879
  "rig_check",
5805
- "Cheap self-critique without rendering: out_of_canvas (slot leaves the canvas, worst time), static_track, loop_seam, deform_flip (lattice folds over itself \u2014 an error), mesh_fold (a skinned/deformed mesh folds over itself: error when \u2265 5 % of the mesh folds, warning below \u2014 a dense mesh bent hard at a joint pinches the odd hairline triangle), weights_sum (skinned mesh vertex weights not summing to 1), patch_offset (a slot sits away from its asset's source bbox), rest_overlap, unused_asset, never_visible, and for constraints: constraint_dead (every mix is 0 at rest and no track lifts one \u2014 the solver skips it), constraint_cycle (the target is a bone the constraint drives, or hangs off one: it reads what it writes \u2014 an error), ik_unreachable (the target sits beyond the chain's reach for over 20 % of an animation, so the limb hangs straight), path_missing (a path constraint whose target slot never shows a path attachment \u2014 an error). Motion diagnostics also cover pass_key_stop, velocity_jump, acceleration_jump, loop_velocity and constraint_order; provide secondaryBones, frontOf slot pairs and named attachment UV landmarks for brief-specific frozen-motion, ordering and swap-registration checks. Findings are sorted by severity. Run it before spending vision tokens on a preview.",
5806
- { path: Path, animation: z3.string().optional().describe("limit to one animation (rest pose is always checked)"), fps: z3.number().positive().optional(), deformFps: z3.number().positive().optional(), secondaryBones: z3.array(z3.string()).optional(), frontOf: z3.array(z3.tuple([z3.string(), z3.string()])).optional(), landmarks: z3.record(z3.record(z3.tuple([z3.number(), z3.number()]))).optional(), landmarkTolerance: z3.number().nonnegative().optional() },
5807
- async ({ path, ...opts }) => {
5880
+ "Cheap self-critique without rendering: out_of_canvas (slot leaves the canvas, worst time), static_track, loop_seam, deform_flip (lattice folds over itself \u2014 an error), mesh_fold (a skinned/deformed mesh folds over itself: error when \u2265 5 % of the mesh folds, warning below \u2014 a dense mesh bent hard at a joint pinches the odd hairline triangle), weights_sum (skinned mesh vertex weights not summing to 1), patch_offset (a slot sits away from its asset's source bbox), rest_overlap, unused_asset, never_visible, and for constraints: constraint_dead (every mix is 0 at rest and no track lifts one \u2014 the solver skips it), constraint_cycle (the target is a bone the constraint drives, or hangs off one: it reads what it writes \u2014 an error), ik_unreachable (the target sits beyond the chain's reach for over 20 % of an animation, so the limb hangs straight), path_missing (a path constraint whose target slot never shows a path attachment \u2014 an error). Motion diagnostics also cover pass_key_stop, velocity_jump, acceleration_jump, loop_velocity and constraint_order; provide secondaryBones, frontOf slot pairs and named attachment UV landmarks for brief-specific frozen-motion, ordering and swap-registration checks. Findings are sorted by severity; each line names its subject (a track as target.id.prop), animation and time. `ignore` drops findings you have read and accept \u2014 by code, narrowed by subject and animation; the reply counts them and names a rule that matched nothing. Run it before spending vision tokens on a preview.",
5881
+ {
5882
+ path: Path,
5883
+ animation: z3.string().optional().describe("limit to one animation (rest pose is always checked)"),
5884
+ fps: z3.number().positive().optional(),
5885
+ deformFps: z3.number().positive().optional(),
5886
+ secondaryBones: z3.array(z3.string()).optional(),
5887
+ frontOf: z3.array(z3.tuple([z3.string(), z3.string()])).optional(),
5888
+ landmarks: z3.record(z3.record(z3.tuple([z3.number(), z3.number()]))).optional(),
5889
+ landmarkTolerance: z3.number().nonnegative().optional(),
5890
+ ignore: z3.array(z3.object({ code: z3.string(), subject: z3.string().optional(), animation: z3.string().optional() }).strict()).optional()
5891
+ },
5892
+ async ({ path, ignore = [], ...opts }) => {
5808
5893
  try {
5809
- return text(summarize(checkRig(await load(path), opts), 100));
5894
+ return text(summarizeIgnoring(checkRig(await load(path), opts), ignore, 100));
5810
5895
  } catch (e) {
5811
5896
  return fail(e);
5812
5897
  }
@@ -5825,9 +5910,9 @@ tool(
5825
5910
  margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
5826
5911
  notes: z3.string().optional()
5827
5912
  },
5828
- async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
5913
+ async ({ path, json: json2, atlas, image, name, skin: skin2, margin, notes }) => {
5829
5914
  try {
5830
- 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 });
5915
+ 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 });
5831
5916
  const res = await create(path, parse(r.doc));
5832
5917
  if (r.warnings.length) res.text += `
5833
5918
  warnings:
@@ -6423,9 +6508,9 @@ tool(
6423
6508
  );
6424
6509
  tool(
6425
6510
  "rig_adjust_track",
6426
- "Re-tune an existing track without retyping keys: amplitude scales its deviation from the rest value (0.5 = half as strong, 2 = twice; scale props as ratios), offset adds a constant, timeScale stretches its key times (duration grows to fit). Drops the preset link.",
6511
+ "Re-tune an existing track without retyping keys: amplitude scales its deviation from the rest value (0.5 = half as strong, 2 = twice; scale props as ratios), offset adds a constant, timeScale stretches its key times (duration grows to fit), interpolation switches segment / pchip / spline. Drops the preset link.",
6427
6512
  { path: Path, animation: z3.string(), ...TrackRef, amplitude: z3.number().optional(), offset: z3.number().optional(), timeScale: z3.number().positive().optional() },
6428
- async ({ path, animation, target, id, prop, ...adj }) => mutate(path, (d) => adjustTrack(d, animation, { target, id, prop }, adj))
6513
+ async ({ path, animation, target, id, prop, interpolation, ...adj }) => mutate(path, (d) => adjustTrack(d, animation, { target, id, prop, interpolation }, adj))
6429
6514
  );
6430
6515
  tool(
6431
6516
  "rig_set_animation",
@@ -6443,7 +6528,7 @@ tool(
6443
6528
  tool(
6444
6529
  "rig_remove_keys",
6445
6530
  "Remove the keys of one track at the given times; an emptied track is dropped. Drops the preset link.",
6446
- { path: Path, animation: z3.string(), ...TrackRef, times: z3.array(z3.number()).min(1) },
6531
+ { path: Path, animation: z3.string(), ...TrackId, times: z3.array(z3.number()).min(1) },
6447
6532
  async ({ path, animation, target, id, prop, times: times2 }) => mutate(path, (d) => removeKeys(d, animation, { target, id, prop }, times2))
6448
6533
  );
6449
6534
  tool(
@@ -6599,40 +6684,41 @@ tool(
6599
6684
  }
6600
6685
  );
6601
6686
 
6687
+ // src/rig-version.ts
6688
+ import { stat as stat3 } from "node:fs/promises";
6689
+ async function rigVersion(abs) {
6690
+ try {
6691
+ const s2 = await stat3(abs);
6692
+ return `${s2.mtimeMs}:${s2.size}`;
6693
+ } catch {
6694
+ return "missing";
6695
+ }
6696
+ }
6697
+
6602
6698
  // src/editor/server.ts
6603
6699
  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" };
6604
- async function startEditorServer(opts) {
6700
+ var json = (res, status, body) => {
6701
+ res.writeHead(status, { "content-type": "application/json" });
6702
+ res.end(JSON.stringify(body));
6703
+ };
6704
+ function createEditorHandler(opts) {
6605
6705
  const root3 = resolve2(opts.root);
6606
6706
  const assets = resolve2(opts.assets ?? fileURLToPath3(new URL("../..", import.meta.url)));
6607
6707
  const inside = (rel) => {
6608
6708
  const abs = resolve2(root3, rel);
6609
- return abs === root3 || abs.startsWith(root3 + sep) ? abs : void 0;
6610
- };
6611
- const version = async (abs) => {
6612
- try {
6613
- const s2 = await stat3(abs);
6614
- return `${s2.mtimeMs}:${s2.size}`;
6615
- } catch {
6616
- return "missing";
6617
- }
6709
+ return abs === root3 || abs.startsWith(root3 + sep2) ? abs : void 0;
6618
6710
  };
6619
6711
  const readDoc = async (abs) => RigDocument.parse(JSON.parse(await readFile10(abs, "utf8")));
6620
6712
  const readDocAt = async (abs) => {
6621
- let v = await version(abs);
6713
+ let v = await rigVersion(abs);
6622
6714
  for (let i = 0; i < 3; i++) {
6623
6715
  const doc = await readDoc(abs);
6624
- const after = await version(abs);
6716
+ const after = await rigVersion(abs);
6625
6717
  if (after === v) return { doc, version: v };
6626
6718
  v = after;
6627
6719
  }
6628
6720
  return { doc: await readDoc(abs), version: v };
6629
6721
  };
6630
- const json = (res, status, body2) => {
6631
- res.writeHead(status, { "content-type": "application/json" });
6632
- res.end(JSON.stringify(body2));
6633
- };
6634
- let allowedHosts = /* @__PURE__ */ new Set();
6635
- const hostOk = (req) => allowedHosts.has(String(req.headers.host ?? "").toLowerCase());
6636
6722
  const clients = /* @__PURE__ */ new Map();
6637
6723
  const watchers = /* @__PURE__ */ new Map();
6638
6724
  function ensureWatch(abs) {
@@ -6644,7 +6730,7 @@ async function startEditorServer(opts) {
6644
6730
  const schedule = () => {
6645
6731
  clearTimeout(entry.timer);
6646
6732
  entry.timer = setTimeout(async () => {
6647
- const v = await version(abs);
6733
+ const v = await rigVersion(abs);
6648
6734
  if (v === entry.last) return;
6649
6735
  entry.last = v;
6650
6736
  for (const res of clients.get(abs) ?? []) res.write(`event: changed
@@ -6653,7 +6739,7 @@ data: ${JSON.stringify({ version: v })}
6653
6739
  `);
6654
6740
  }, 50);
6655
6741
  };
6656
- version(abs).then((v) => {
6742
+ rigVersion(abs).then((v) => {
6657
6743
  entry.last = v;
6658
6744
  });
6659
6745
  watchers.set(abs, entry);
@@ -6679,7 +6765,7 @@ data: ${JSON.stringify({ version: v })}
6679
6765
  name = String(JSON.parse(await readFile10(abs, "utf8")).meta?.name ?? "?");
6680
6766
  } catch {
6681
6767
  }
6682
- out.push({ rig: relative3(root3, abs).split(sep).join("/"), name, mtimeMs: (await stat3(abs)).mtimeMs });
6768
+ out.push({ rig: relative4(root3, abs).split(sep2).join("/"), name, mtimeMs: (await stat4(abs)).mtimeMs });
6683
6769
  }
6684
6770
  };
6685
6771
  await walk(root3);
@@ -6713,20 +6799,22 @@ data: ${JSON.stringify({ version: v })}
6713
6799
  }
6714
6800
  return Buffer.concat(chunks);
6715
6801
  }
6716
- const server = createServer(async (req, res) => {
6802
+ async function handle(req, res, rawUrl = req.url ?? "/") {
6717
6803
  try {
6718
- 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` });
6719
- const url = new URL(req.url ?? "/", "http://x");
6804
+ const url = new URL(rawUrl, "http://x");
6720
6805
  if (url.pathname === "/") {
6721
- res.writeHead(302, { location: "/editor.html" + url.search });
6722
- return res.end();
6806
+ res.writeHead(302, { location: "editor.html" + url.search });
6807
+ return void res.end();
6808
+ }
6809
+ if (url.pathname.startsWith("/api/")) {
6810
+ await api(url, req, res);
6811
+ return;
6723
6812
  }
6724
- if (url.pathname.startsWith("/api/")) return await api(url, req, res);
6725
6813
  const STATIC = /* @__PURE__ */ new Set(["/editor.html", "/dist/editor.js", "/dist/editor.js.map", "/dist/editor.css", "/dist/editor.css.map"]);
6726
6814
  const abs = url.pathname.startsWith("/rig/") ? inside(decodeURIComponent(url.pathname.slice(5))) : STATIC.has(url.pathname) ? join10(assets, url.pathname) : void 0;
6727
6815
  if (!abs) {
6728
6816
  res.writeHead(403);
6729
- return res.end("forbidden");
6817
+ return void res.end("forbidden");
6730
6818
  }
6731
6819
  try {
6732
6820
  const data = await readFile10(abs);
@@ -6744,7 +6832,7 @@ data: ${JSON.stringify({ version: v })}
6744
6832
  }
6745
6833
  json(res, 500, { isError: true, text: `ERROR: ${e.message}` });
6746
6834
  }
6747
- });
6835
+ }
6748
6836
  async function api(url, req, res) {
6749
6837
  const rigOf = (rel) => {
6750
6838
  if (!rel) return void 0;
@@ -6761,7 +6849,7 @@ data: ${JSON.stringify({ version: v })}
6761
6849
  res.writeHead(403);
6762
6850
  return res.end();
6763
6851
  }
6764
- if (!await stat3(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root3}` });
6852
+ if (!await stat4(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root3}` });
6765
6853
  ensureWatch(abs);
6766
6854
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
6767
6855
  res.write(`: connected
@@ -6783,7 +6871,7 @@ data: ${JSON.stringify({ version: v })}
6783
6871
  if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
6784
6872
  ensureWatch(abs);
6785
6873
  return await withRigQueue(abs, async () => {
6786
- if (baseVersion !== void 0 && await version(abs) !== baseVersion) {
6874
+ if (baseVersion !== void 0 && await rigVersion(abs) !== baseVersion) {
6787
6875
  const at = await readDocAt(abs);
6788
6876
  return json(res, 409, {
6789
6877
  isError: true,
@@ -6792,8 +6880,15 @@ data: ${JSON.stringify({ version: v })}
6792
6880
  text: `ERROR: the rig changed on disk (expected ${baseVersion}, found ${at.version}); nothing was written`
6793
6881
  });
6794
6882
  }
6795
- const r = await callTool(url.pathname.slice(10), { ...args2 ?? {}, path: abs });
6796
- return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await version(abs) });
6883
+ const tool2 = url.pathname.slice(10);
6884
+ let given = { ...args2 ?? {}, path: abs };
6885
+ try {
6886
+ if (opts.args) given = opts.args(tool2, given);
6887
+ } catch (e) {
6888
+ return json(res, 400, { isError: true, text: `ERROR: ${e instanceof Error ? e.message : String(e)}` });
6889
+ }
6890
+ const r = await callTool(tool2, given);
6891
+ return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await rigVersion(abs) });
6797
6892
  });
6798
6893
  }
6799
6894
  if (url.pathname === "/api/rigs" && req.method === "GET") return json(res, 200, await listRigs());
@@ -6804,11 +6899,11 @@ data: ${JSON.stringify({ version: v })}
6804
6899
  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" });
6805
6900
  const abs = inside(rig);
6806
6901
  if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
6807
- 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` });
6902
+ 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` });
6808
6903
  await mkdir7(dirname9(abs), { recursive: true });
6809
6904
  const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
6810
6905
  ensureWatch(abs);
6811
- return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await version(abs) });
6906
+ return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await rigVersion(abs) });
6812
6907
  }
6813
6908
  if (url.pathname === "/api/files" && req.method === "GET") {
6814
6909
  const abs = rigOf(url.searchParams.get("rig"));
@@ -6822,8 +6917,8 @@ data: ${JSON.stringify({ version: v })}
6822
6917
  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` });
6823
6918
  if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("image/png")) return json(res, 415, { isError: true, text: "ERROR: upload needs content-type: image/png" });
6824
6919
  const target = join10(dirname9(abs), name);
6825
- if (!inside(relative3(root3, target))) return json(res, 403, { isError: true, text: "file outside root" });
6826
- 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` });
6920
+ if (!inside(relative4(root3, target))) return json(res, 403, { isError: true, text: "file outside root" });
6921
+ 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` });
6827
6922
  const data = await bytes(req, UPLOAD_MAX);
6828
6923
  if (!data) return json(res, 413, { isError: true, text: `ERROR: ${name} is larger than ${UPLOAD_MAX / 1048576} MB` });
6829
6924
  let size;
@@ -6837,6 +6932,22 @@ data: ${JSON.stringify({ version: v })}
6837
6932
  }
6838
6933
  json(res, 404, { isError: true, text: "unknown api route" });
6839
6934
  }
6935
+ return { handle, close() {
6936
+ for (const w of watchers.values()) {
6937
+ clearTimeout(w.timer);
6938
+ w.w.close();
6939
+ }
6940
+ for (const set of clients.values()) for (const c of set) c.end();
6941
+ } };
6942
+ }
6943
+ async function startEditorServer(opts) {
6944
+ const editor = createEditorHandler(opts);
6945
+ let allowedHosts = /* @__PURE__ */ new Set();
6946
+ const server = createServer((req, res) => {
6947
+ if (!allowedHosts.has(String(req.headers.host ?? "").toLowerCase()))
6948
+ return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
6949
+ void editor.handle(req, res);
6950
+ });
6840
6951
  await new Promise((ok, fail2) => {
6841
6952
  server.once("error", fail2);
6842
6953
  server.listen(opts.port ?? 8765, "127.0.0.1", () => {
@@ -6847,11 +6958,7 @@ data: ${JSON.stringify({ version: v })}
6847
6958
  const port2 = server.address().port;
6848
6959
  allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${port2}`, `localhost:${port2}`, `[::1]:${port2}`]);
6849
6960
  return { port: port2, url: `http://127.0.0.1:${port2}`, close: async () => {
6850
- for (const w of watchers.values()) {
6851
- clearTimeout(w.timer);
6852
- w.w.close();
6853
- }
6854
- for (const set of clients.values()) for (const c of set) c.end();
6961
+ editor.close();
6855
6962
  await new Promise((r) => server.close(() => r()));
6856
6963
  } };
6857
6964
  }