@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
@@ -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
@@ -248,7 +248,7 @@ var Key = z.object({
248
248
  /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
249
249
  componentEase: z.array(Ease).min(1).optional()
250
250
  });
251
- var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
251
+ var Interpolation = { interpolation: z.enum(["segment", "pchip", "spline"]).optional() };
252
252
  var Track = z.discriminatedUnion("target", [
253
253
  z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
254
254
  z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
@@ -568,7 +568,7 @@ function parse(input) {
568
568
  // src/rig-api.ts
569
569
  import { z as z3 } from "zod";
570
570
  import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
571
- 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";
572
572
 
573
573
  // src/rig-io.ts
574
574
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
@@ -591,8 +591,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
591
591
  // src/rig-queue.ts
592
592
  import { AsyncLocalStorage } from "node:async_hooks";
593
593
  import { resolve } from "node:path";
594
- var pending = /* @__PURE__ */ new Map();
595
- var active = new AsyncLocalStorage();
594
+ var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
595
+ pending: /* @__PURE__ */ new Map(),
596
+ active: new AsyncLocalStorage()
597
+ };
596
598
  async function withRigQueue(path, op) {
597
599
  const key = resolve(path);
598
600
  const held = active.getStore();
@@ -732,6 +734,57 @@ function smoothValue(track, i, t, component, loop) {
732
734
  const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;
733
735
  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);
734
736
  }
737
+ function tridiagonal(a, b, c, r) {
738
+ const n = b.length, cp = [], x = [];
739
+ let beta = b[0];
740
+ x[0] = r[0] / beta;
741
+ for (let i = 1; i < n; i++) {
742
+ cp[i] = c[i - 1] / beta;
743
+ beta = b[i] - a[i] * cp[i];
744
+ x[i] = (r[i] - a[i] * x[i - 1]) / beta;
745
+ }
746
+ for (let i = n - 2; i >= 0; i--) x[i] -= cp[i + 1] * x[i + 1];
747
+ return x;
748
+ }
749
+ function splineMoments(t, v, periodic) {
750
+ const n = t.length, h = (j2) => t[j2 + 1] - t[j2], d = (j2) => (v[j2 + 1] - v[j2]) / h(j2);
751
+ if (!periodic) {
752
+ const a2 = [], b2 = [], c2 = [], r2 = [];
753
+ for (let i = 0; i < n; i++) {
754
+ const hp = i > 0 ? h(i - 1) : 0, hn = i < n - 1 ? h(i) : 0;
755
+ a2.push(hp);
756
+ b2.push(2 * (hp + hn));
757
+ c2.push(hn);
758
+ r2.push(6 * ((i < n - 1 ? d(i) : 0) - (i > 0 ? d(i - 1) : 0)));
759
+ }
760
+ return tridiagonal(a2, b2, c2, r2);
761
+ }
762
+ const m = n - 1, a = [], b = [], c = [], r = [];
763
+ for (let i = 0; i < m; i++) {
764
+ const p = (i + m - 1) % m;
765
+ a.push(h(p));
766
+ b.push(2 * (h(p) + h(i)));
767
+ c.push(h(i));
768
+ r.push(6 * (d(i) - d(p)));
769
+ }
770
+ const top = a[0], bottom = c[m - 1], gamma = -b[0];
771
+ const bb = [...b];
772
+ bb[0] -= gamma;
773
+ bb[m - 1] -= bottom * top / gamma;
774
+ const x = tridiagonal(a, bb, c, r), u = new Array(m).fill(0);
775
+ u[0] = gamma;
776
+ u[m - 1] = bottom;
777
+ const z4 = tridiagonal(a, bb, c, u);
778
+ const f = (x[0] + top * x[m - 1] / gamma) / (1 + z4[0] + top * z4[m - 1] / gamma);
779
+ const M = x.map((xi, i) => xi - f * z4[i]);
780
+ return [...M, M[0]];
781
+ }
782
+ function splineValue(track, i, t, component, loop) {
783
+ const k = track.keys, ts = k.map((x) => x.t), vs = k.map((x) => component === void 0 ? x.v : x.v[component]);
784
+ const M = splineMoments(ts, vs, loop && k.length > 3 && vs[0] === vs[k.length - 1]);
785
+ const H = ts[i + 1] - ts[i], A = (ts[i + 1] - t) / H, B = (t - ts[i]) / H;
786
+ return A * vs[i] + B * vs[i + 1] + ((A ** 3 - A) * M[i] + (B ** 3 - B) * M[i + 1]) * H * H / 6;
787
+ }
735
788
  function sample(track, t, loop = false) {
736
789
  const keys = track.keys;
737
790
  if (t <= keys[0].t) return keys[0].v;
@@ -743,11 +796,13 @@ function sample(track, t, loop = false) {
743
796
  if (a.ease === "step") return a.v;
744
797
  if (typeof a.v === "number" && typeof b.v === "number") {
745
798
  if (track.interpolation === "pchip") return smoothValue(track, i, t, void 0, loop);
799
+ if (track.interpolation === "spline") return splineValue(track, i, t, void 0, loop);
746
800
  const u = ease(a.ease)((t - a.t) / (b.t - a.t));
747
801
  return a.v + (b.v - a.v) * u;
748
802
  }
749
803
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
750
804
  if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
805
+ if (track.interpolation === "spline") return a.v.map((_, j2) => splineValue(track, i, t, j2, loop));
751
806
  const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
752
807
  return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
753
808
  }
@@ -2008,8 +2063,9 @@ function createDoc(name, width, height, notes) {
2008
2063
  function createSymbol(name, asset, margin = 0.15, notes) {
2009
2064
  const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
2010
2065
  const width = asset.width + 2 * mx, height = asset.height + 2 * my;
2011
- const fromFile = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/^[^a-z]+/, "");
2012
- const id = asset.id ?? (fromFile || name);
2066
+ const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
2067
+ const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
2068
+ const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
2013
2069
  let doc = createDoc(name, width, height, notes);
2014
2070
  doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
2015
2071
  doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
@@ -2478,7 +2534,8 @@ function adjustTrack(doc, animId, track, adj) {
2478
2534
  return { ...k, t: r42(adj.timeScale ? k.t * adj.timeScale : k.t), v: r42(v) };
2479
2535
  });
2480
2536
  const duration = Math.max(anim.duration, keys[keys.length - 1].t);
2481
- const next = { ...anim, duration, tracks: anim.tracks.map((t) => same(t) ? { ...t, keys } : t), preset: void 0 };
2537
+ const interpolation = track.interpolation ?? tr.interpolation;
2538
+ const next = { ...anim, duration, tracks: anim.tracks.map((t) => same(t) ? { ...t, keys, ...interpolation && { interpolation } } : t), preset: void 0 };
2482
2539
  return { ...doc, animations: doc.animations.map((a) => a.id === animId ? next : a) };
2483
2540
  }
2484
2541
  function removeAnimation(doc, id) {
@@ -2765,7 +2822,7 @@ function setGoal(doc, bone, x, y, opts) {
2765
2822
 
2766
2823
  // src/rig-import-layers.ts
2767
2824
  import { readdir, readFile } from "node:fs/promises";
2768
- import { join as join2, basename as basename2 } from "node:path";
2825
+ import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
2769
2826
  var DEFAULT_PARENT = {
2770
2827
  head: "neck",
2771
2828
  neck: "torso",
@@ -2791,6 +2848,10 @@ function pngSize(buf) {
2791
2848
  if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
2792
2849
  return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
2793
2850
  }
2851
+ function outsideDir(dir, file) {
2852
+ const r = relative(dir, join2(dir, file));
2853
+ return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
2854
+ }
2794
2855
  async function importLayers(doc, dir, relPrefix = "layers") {
2795
2856
  let specs;
2796
2857
  try {
@@ -2803,6 +2864,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
2803
2864
  let next = doc;
2804
2865
  const parsed = [];
2805
2866
  for (const s of specs) {
2867
+ if (outsideDir(dir, s.file)) throw new Error(`layers.json file "${s.file}" is outside the layers folder (${dir})`);
2806
2868
  const buf = await readFile(join2(dir, s.file));
2807
2869
  const [w, h] = pngSize(buf);
2808
2870
  const m = basename2(s.file, ".png").match(/^(?:(\d+)_)?(.+)$/);
@@ -2890,19 +2952,20 @@ var requireSlot = (doc, id) => {
2890
2952
  if (!s) throw new Error(`unknown slot "${id}"`);
2891
2953
  return s;
2892
2954
  };
2893
- function sampled(duration, n, f, ease2 = "linear") {
2955
+ function sampled(duration, n, f) {
2894
2956
  const keys = [];
2895
2957
  for (let i = 0; i <= n; i++) {
2896
2958
  const t = +(duration * i / n).toFixed(4);
2897
- keys.push({ t, v: +f(t).toFixed(4) + 0, ease: ease2 });
2959
+ keys.push({ t, v: +f(t).toFixed(4) + 0, ease: "linear" });
2898
2960
  }
2899
2961
  return keys;
2900
2962
  }
2901
- function boneTrack(bone, prop, keys) {
2902
- return { target: "bone", id: bone.id, prop, keys };
2963
+ var smooth = "spline";
2964
+ function boneTrack(bone, prop, keys, interpolation) {
2965
+ return { target: "bone", id: bone.id, prop, keys, ...interpolation && { interpolation } };
2903
2966
  }
2904
- function deformTrack(slot, prop, keys) {
2905
- return slot.attachment ? { target: "deform", id: slot.attachment, prop, keys } : void 0;
2967
+ function deformTrack(slot, prop, keys, interpolation) {
2968
+ return slot.attachment ? { target: "deform", id: slot.attachment, prop, keys, ...interpolation && { interpolation } } : void 0;
2906
2969
  }
2907
2970
  var pushTrack = (tracks, t) => {
2908
2971
  if (t) tracks.push(t);
@@ -2926,18 +2989,18 @@ var idleBreathing = {
2926
2989
  const armsR = byRole(doc, "arm_r", "arm_upper_r");
2927
2990
  const chest = slotOfBone(doc, torso);
2928
2991
  if (chest?.attachment) {
2929
- pushTrack(tracks, deformTrack(chest, "bulge", sampled(T2, 8, (t) => p.amplitude * Math.sin(w * t), "ease_in_out")));
2992
+ pushTrack(tracks, deformTrack(chest, "bulge", sampled(T2, 8, (t) => p.amplitude * Math.sin(w * t)), smooth));
2930
2993
  } else {
2931
2994
  const r = torso.transform;
2932
- tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t), "ease_in_out")));
2933
- tracks.push(boneTrack(torso, "scaleX", sampled(T2, 8, (t) => r.scaleX - p.amplitude * 0.4 * Math.sin(w * t), "ease_in_out")));
2995
+ tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t)), smooth));
2996
+ tracks.push(boneTrack(torso, "scaleX", sampled(T2, 8, (t) => r.scaleX - p.amplitude * 0.4 * Math.sin(w * t)), smooth));
2934
2997
  }
2935
2998
  for (const h of heads)
2936
- tracks.push(boneTrack(h, "y", sampled(T2, 8, (t) => h.transform.y - p.headBob * Math.sin(w * t + 0.4), "ease_in_out")));
2999
+ tracks.push(boneTrack(h, "y", sampled(T2, 8, (t) => h.transform.y - p.headBob * Math.sin(w * t + 0.4)), smooth));
2937
3000
  for (const a of armsL)
2938
- tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation + p.armSway * Math.sin(w * t + 0.8), "ease_in_out")));
3001
+ tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation + p.armSway * Math.sin(w * t + 0.8)), smooth));
2939
3002
  for (const a of armsR)
2940
- tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation - p.armSway * Math.sin(w * t + 0.8), "ease_in_out")));
3003
+ tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation - p.armSway * Math.sin(w * t + 0.8)), smooth));
2941
3004
  return finish(this.name, p.id, T2, true, tracks, p);
2942
3005
  }
2943
3006
  };
@@ -2946,22 +3009,20 @@ var winBounce = {
2946
3009
  defaults: { id: "win", duration: 1.2, height: 40, bounces: 2, squash: 0.15 },
2947
3010
  build(doc, p) {
2948
3011
  const b = root(doc), r = b.transform, tracks = [];
2949
- const yK = [[0, r.y, "ease_out"]];
2950
- const sxK = [[0, r.scaleX, "ease_out"]];
2951
- const syK = [[0, r.scaleY, "ease_out"]];
3012
+ const yK = [[0, r.y]], sxK = [[0, r.scaleX]], syK = [[0, r.scaleY]];
2952
3013
  const per = p.duration / p.bounces;
2953
3014
  for (let i = 0; i < p.bounces; i++) {
2954
3015
  const t0 = i * per, h = p.height * 0.55 ** i, s = p.squash * 0.7 ** i;
2955
- sxK.push([t0 + per * 0.08, r.scaleX * (1 + s), "ease_out"], [t0 + per * 0.5, r.scaleX * (1 - s * 0.6), "ease_in"], [t0 + per * 0.95, r.scaleX * (1 + s), "ease_out"]);
2956
- syK.push([t0 + per * 0.08, r.scaleY * (1 - s), "ease_out"], [t0 + per * 0.5, r.scaleY * (1 + s * 0.6), "ease_in"], [t0 + per * 0.95, r.scaleY * (1 - s), "ease_out"]);
2957
- 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"]);
3016
+ sxK.push([t0 + per * 0.08, r.scaleX * (1 + s)], [t0 + per * 0.5, r.scaleX * (1 - s * 0.6)], [t0 + per * 0.95, r.scaleX * (1 + s)]);
3017
+ syK.push([t0 + per * 0.08, r.scaleY * (1 - s)], [t0 + per * 0.5, r.scaleY * (1 + s * 0.6)], [t0 + per * 0.95, r.scaleY * (1 - s)]);
3018
+ yK.push([t0 + per * 0.08, r.y], [t0 + per * 0.5, r.y - h], [t0 + per * 0.95, r.y]);
2958
3019
  }
2959
3020
  yK.push([p.duration, r.y]);
2960
3021
  sxK.push([p.duration, r.scaleX]);
2961
3022
  syK.push([p.duration, r.scaleY]);
2962
- tracks.push(boneTrack(b, "y", K(...yK)), boneTrack(b, "scaleX", K(...sxK)), boneTrack(b, "scaleY", K(...syK)));
3023
+ tracks.push(boneTrack(b, "y", K(...yK), "pchip"), boneTrack(b, "scaleX", K(...sxK), smooth), boneTrack(b, "scaleY", K(...syK), smooth));
2963
3024
  for (const h of byRole(doc, "head"))
2964
- 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])));
3025
+ 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));
2965
3026
  const anim = finish(this.name, p.id, p.duration, false, tracks, p);
2966
3027
  anim.events.push({ t: 0, name: "win_start" }, { t: per * 0.95, name: "land" });
2967
3028
  return anim;
@@ -2974,8 +3035,8 @@ var anticipationShake = {
2974
3035
  const b = root(doc), r = b.transform, tracks = [];
2975
3036
  const n = Math.ceil(p.duration * p.frequency * 4);
2976
3037
  const env = (t) => p.grow ? t / p.duration : 1;
2977
- 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")));
2978
- 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")));
3038
+ 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));
3039
+ 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));
2979
3040
  tracks.push(boneTrack(b, "scaleX", K([0, r.scaleX, "ease_in"], [p.duration, r.scaleX * p.scaleUp])));
2980
3041
  tracks.push(boneTrack(b, "scaleY", K([0, r.scaleY, "ease_in"], [p.duration, r.scaleY * p.scaleUp])));
2981
3042
  return finish(this.name, p.id, p.duration, false, tracks, p);
@@ -3000,6 +3061,7 @@ var mouthTalk = {
3000
3061
  if (!p.frames.length) throw new Error("mouth_talk: frames must not be empty");
3001
3062
  const n = Math.max(1, Math.round(p.duration * p.rate));
3002
3063
  const keys = S(...Array.from({ length: n }, (_, i) => [+(i / p.rate).toFixed(4), p.frames[i % p.frames.length]]));
3064
+ if (keys.at(-1).t < p.duration && keys.at(-1).v !== keys[0].v) keys.push(...S([p.duration, keys[0].v]));
3003
3065
  return finish(this.name, p.id, p.duration, true, [slotTrack(slot, "attachment", keys)], p);
3004
3066
  }
3005
3067
  };
@@ -3017,10 +3079,10 @@ var symbolIdle = {
3017
3079
  build(doc, p) {
3018
3080
  const b = root(doc), r = b.transform, T2 = p.period, w = 2 * Math.PI / T2, tracks = [];
3019
3081
  const rise = (t) => 0.5 - 0.5 * Math.cos(w * t);
3020
- tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t), "ease_in_out")));
3021
- if (p.tilt) tracks.push(boneTrack(b, "rotation", sampled(T2, 8, (t) => r.rotation + p.tilt * Math.sin(w * t), "ease_in_out")));
3082
+ tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t)), smooth));
3083
+ if (p.tilt) tracks.push(boneTrack(b, "rotation", sampled(T2, 8, (t) => r.rotation + p.tilt * Math.sin(w * t)), smooth));
3022
3084
  if (p.pulse) for (const s of doc.slots.filter((s2) => s2.bone === b.id && s2.attachment))
3023
- pushTrack(tracks, deformTrack(s, "bulge", sampled(T2, 8, (t) => p.pulse * rise(t), "ease_in_out")));
3085
+ pushTrack(tracks, deformTrack(s, "bulge", sampled(T2, 8, (t) => p.pulse * rise(t)), smooth));
3024
3086
  return finish(this.name, p.id, T2, true, tracks, p);
3025
3087
  }
3026
3088
  };
@@ -3030,12 +3092,12 @@ var symbolWin = {
3030
3092
  build(doc, p) {
3031
3093
  const b = root(doc), r = b.transform, d = p.duration, tracks = [];
3032
3094
  for (const [prop, rest] of [["scaleX", r.scaleX], ["scaleY", r.scaleY]])
3033
- 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])));
3095
+ 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));
3034
3096
  if (p.wobble) {
3035
3097
  const n = Math.max(8, p.wobbles * 4);
3036
- 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");
3098
+ const keys = sampled(d, n, (t) => p.wobble * Math.exp(-3 * t / d) * Math.sin(2 * Math.PI * p.wobbles * t / d));
3037
3099
  keys[keys.length - 1].v = 0;
3038
- for (const s of doc.slots.filter((s2) => s2.bone === b.id && s2.attachment)) pushTrack(tracks, deformTrack(s, "sway", keys));
3100
+ for (const s of doc.slots.filter((s2) => s2.bone === b.id && s2.attachment)) pushTrack(tracks, deformTrack(s, "sway", keys, smooth));
3039
3101
  }
3040
3102
  const anim = finish(this.name, p.id, d, false, tracks, p);
3041
3103
  anim.events.push({ t: 0, name: "win_start" });
@@ -3049,11 +3111,11 @@ var weightShift = {
3049
3111
  const T2 = p.period, w = 2 * Math.PI / T2, N = 16, tracks = [];
3050
3112
  const hips = byRole(doc, "hips")[0] ?? root(doc), hr = hips.transform;
3051
3113
  const s = (t) => Math.sin(w * t);
3052
- tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s(t))));
3053
- if (p.dip) tracks.push(boneTrack(hips, "y", sampled(T2, N, (t) => hr.y + p.dip * s(t) ** 2)));
3114
+ tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s(t)), smooth));
3115
+ if (p.dip) tracks.push(boneTrack(hips, "y", sampled(T2, N, (t) => hr.y + p.dip * s(t) ** 2), smooth));
3054
3116
  const torso = byRole(doc, "torso", "body")[0];
3055
- if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s(t))));
3056
- for (const h of byRole(doc, "head")) tracks.push(boneTrack(h, "rotation", sampled(T2, N, (t) => h.transform.rotation + p.lean / 2 * s(t))));
3117
+ if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s(t)), smooth));
3118
+ for (const h of byRole(doc, "head")) tracks.push(boneTrack(h, "rotation", sampled(T2, N, (t) => h.transform.rotation + p.lean / 2 * s(t)), smooth));
3057
3119
  for (const leg of byRole(doc, "leg_l", "leg_r")) {
3058
3120
  const slot = slotOfBone(doc, leg), att = slot && doc.attachments.find((a) => a.id === slot.attachment && a.type === "region");
3059
3121
  const asset = att && att.type === "region" ? doc.assets.find((a) => a.id === att.asset) : void 0;
@@ -3063,8 +3125,8 @@ var weightShift = {
3063
3125
  length = Math.max(...[[0, -h / 2], [w2 / 2, 0], [0, h / 2], [-w2 / 2, 0]].map(([x, y]) => Math.hypot(...apply(m, x, y))));
3064
3126
  }
3065
3127
  if (!(length > p.shift)) continue;
3066
- tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s(t) / length) * 180 / Math.PI)));
3067
- if (p.dip) tracks.push(boneTrack(leg, "y", sampled(T2, N, (t) => leg.transform.y - p.dip * s(t) ** 2)));
3128
+ tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s(t) / length) * 180 / Math.PI), smooth));
3129
+ if (p.dip) tracks.push(boneTrack(leg, "y", sampled(T2, N, (t) => leg.transform.y - p.dip * s(t) ** 2), smooth));
3068
3130
  }
3069
3131
  return finish(this.name, p.id, T2, true, tracks, p);
3070
3132
  }
@@ -3300,13 +3362,13 @@ function motionQuality(doc, anim, opts = {}) {
3300
3362
  const before = delta(values(k.v), values(p.v)), after = delta(values(n.v), values(k.v));
3301
3363
  const pass = before.some((v, j2) => v * after[j2] > 1e-6);
3302
3364
  const speed = Math.max(magnitude(before) / (k.t - p.t), magnitude(after) / (n.t - k.t));
3303
- 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);
3365
+ 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);
3304
3366
  const jump = magnitude(delta(left, right));
3305
3367
  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);
3306
3368
  const acceleration = (a, b, c) => values(at(b)).map((v, j2) => (values(at(c))[j2] - 2 * v + values(at(a))[j2]) / (h * h));
3307
3369
  const al = acceleration(k.t - 2 * h, k.t - h, k.t), ar = acceleration(k.t, k.t + h, k.t + 2 * h);
3308
3370
  const aj = magnitude(delta(al, ar));
3309
- 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");
3371
+ 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");
3310
3372
  }
3311
3373
  }
3312
3374
  const steps = Math.max(2, Math.ceil(D * 60));
@@ -3788,6 +3850,17 @@ function checkRig(doc, opts = {}) {
3788
3850
  out.push({ level: "warn", code: "never_visible", subject: s.id, message: `slot "${s.id}" has no attachment and no animation ever shows one` });
3789
3851
  return out;
3790
3852
  }
3853
+ function summarizeIgnoring(findings, rules, max) {
3854
+ const hits = (r, f) => r.code === f.code && (r.subject ?? f.subject) === f.subject && (r.animation ?? f.animation) === f.animation;
3855
+ const kept = findings.filter((f) => !rules.some((r) => hits(r, f)));
3856
+ const idle = rules.filter((r) => !findings.some((f) => hits(r, f)));
3857
+ const notes = [
3858
+ ...kept.length < findings.length ? [` (${findings.length - kept.length} ignored)`] : [],
3859
+ ...idle.map((r) => ` ignore matched nothing: ${JSON.stringify(r)}`)
3860
+ ];
3861
+ return [summarize(kept, max), ...notes].join("\n");
3862
+ }
3863
+ var where = (f) => `${f.subject}${f.animation ? ` in ${f.animation}` : ""}${f.time !== void 0 ? ` @${+f.time.toFixed(4)}s` : ""}`;
3791
3864
  function summarize(findings, max = 12) {
3792
3865
  if (!findings.length) return "check: OK";
3793
3866
  const rank = { error: 0, warn: 1, info: 2 };
@@ -3802,7 +3875,7 @@ function summarize(findings, max = 12) {
3802
3875
  const lines = [];
3803
3876
  for (const f of sorted) {
3804
3877
  const shown = shownPerCode.get(f.code) ?? 0;
3805
- if (shown < PER_CODE) lines.push(` [${f.level}] ${f.code}: ${f.message}`);
3878
+ if (shown < PER_CODE) lines.push(` [${f.level}] ${f.code} ${where(f)}: ${f.message}`);
3806
3879
  else if (shown === PER_CODE) lines.push(` \u2026 ${codeTotal.get(f.code) - PER_CODE} more ${f.code}`);
3807
3880
  shownPerCode.set(f.code, shown + 1);
3808
3881
  }
@@ -3993,7 +4066,7 @@ function packRects(rects, opts = {}) {
3993
4066
  shelf = Math.max(shelf, r.height);
3994
4067
  }
3995
4068
  const height = y + shelf + pad;
3996
- if (height <= max) return { width, height, frames };
4069
+ if (height <= max) return { width: Math.max(pad, ...[...frames.values()].map(([fx, , fw]) => fx + fw)) + pad, height, frames };
3997
4070
  if (width >= max) throw new Error(`atlas does not fit in ${max}\xD7${max} (needs ${width}\xD7${height})`);
3998
4071
  width *= 2;
3999
4072
  }
@@ -4052,7 +4125,7 @@ async function buildAtlas(doc, assetsDir, outDir, opts = {}) {
4052
4125
 
4053
4126
  // src/spine-import.ts
4054
4127
  import { readFile as readFile7, stat, writeFile as writeFile5 } from "node:fs/promises";
4055
- import { basename as basename4, dirname as dirname6, extname, join as join8, relative } from "node:path";
4128
+ import { basename as basename4, dirname as dirname6, extname, join as join8, relative as relative2 } from "node:path";
4056
4129
 
4057
4130
  // src/ktx2.ts
4058
4131
  import { readFile as readFile6 } from "node:fs/promises";
@@ -4201,13 +4274,13 @@ var Ids = class {
4201
4274
  var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
4202
4275
  var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
4203
4276
  var hex8 = (c) => `#${(c ?? "ffffffff").toLowerCase().padEnd(8, "f").slice(0, 8)}`;
4204
- function importSpine(json, atlasText, opts = {}) {
4277
+ function importSpine(json2, atlasText, opts = {}) {
4205
4278
  const warnings = [];
4206
4279
  const atlas = parseAtlas(atlasText);
4207
- const version = String(json.skeleton?.spine ?? "4.0");
4208
- if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
4280
+ const version = String(json2.skeleton?.spine ?? "4.0");
4281
+ if (json2.physics?.length) warnings.push(`${json2.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
4209
4282
  const major = parseFloat(version);
4210
- const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
4283
+ const bounds = { x: json2.skeleton?.x ?? 0, y: json2.skeleton?.y ?? 0, w: json2.skeleton?.width ?? 0, h: json2.skeleton?.height ?? 0 };
4211
4284
  const margin = opts.margin ?? 0;
4212
4285
  let originX = -bounds.x + margin, originY = bounds.y + bounds.h + margin;
4213
4286
  const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
@@ -4224,7 +4297,7 @@ function importSpine(json, atlasText, opts = {}) {
4224
4297
  }
4225
4298
  return id;
4226
4299
  };
4227
- const spineBones = json.bones ?? [];
4300
+ const spineBones = json2.bones ?? [];
4228
4301
  const boneIndexByName = new Map(spineBones.map((b, i) => [b.name, i]));
4229
4302
  const bones = [{ id: ROOT, transform: { x: r4(originX), y: r4(originY) }, role: "root" }];
4230
4303
  for (const b of spineBones) {
@@ -4237,7 +4310,7 @@ function importSpine(json, atlasText, opts = {}) {
4237
4310
  ...b.color ? { color: b.color } : {}
4238
4311
  });
4239
4312
  }
4240
- const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
4313
+ const skins = Array.isArray(json2.skins) ? json2.skins : Object.entries(json2.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
4241
4314
  const selectedSkin = opts.skin ? skins.find((s) => s.name === opts.skin) : skins.find((s) => s.name === "default") ?? skins[0];
4242
4315
  if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s) => s.name).join(", ")}`);
4243
4316
  const lookupSkins = [selectedSkin, skins.find((s) => s.name === "default")].filter((s, i, list) => s && list.indexOf(s) === i);
@@ -4261,7 +4334,7 @@ function importSpine(json, atlasText, opts = {}) {
4261
4334
  const resolved = skinnedName.get(`${slot} ${name}`);
4262
4335
  return resolved === void 0 ? null : attachmentId(slot, resolved);
4263
4336
  };
4264
- const spineSlots = json.slots ?? [];
4337
+ const spineSlots = json2.slots ?? [];
4265
4338
  const slotIndexByName = new Map(spineSlots.map((s, i) => [s.name, i]));
4266
4339
  const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
4267
4340
  const slots = spineSlots.map((s, i) => ({
@@ -4427,7 +4500,7 @@ function importSpine(json, atlasText, opts = {}) {
4427
4500
  }
4428
4501
  }
4429
4502
  const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s) => s[kind]?.includes(c.name));
4430
- const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
4503
+ const ik = (json2.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
4431
4504
  id: ikIds.get(c.name),
4432
4505
  order: c.order ?? 0,
4433
4506
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4440,7 +4513,7 @@ function importSpine(json, atlasText, opts = {}) {
4440
4513
  stretch: !!c.stretch,
4441
4514
  uniform: !!c.uniform
4442
4515
  }));
4443
- const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4516
+ const transform = (json2.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4444
4517
  id: tcIds.get(c.name),
4445
4518
  order: c.order ?? 0,
4446
4519
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4460,7 +4533,7 @@ function importSpine(json, atlasText, opts = {}) {
4460
4533
  mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
4461
4534
  mixShearY: c.mixShearY ?? c.shearMix ?? 1
4462
4535
  }));
4463
- const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4536
+ const path = (json2.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4464
4537
  id: pcIds.get(c.name),
4465
4538
  order: c.order ?? 0,
4466
4539
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4514,7 +4587,7 @@ function importSpine(json, atlasText, opts = {}) {
4514
4587
  return keys;
4515
4588
  };
4516
4589
  const animations = [];
4517
- for (const [animName, a] of Object.entries(json.animations ?? {})) {
4590
+ for (const [animName, a] of Object.entries(json2.animations ?? {})) {
4518
4591
  const tracks = [], events = [];
4519
4592
  let duration = 0;
4520
4593
  const seen = (frames) => {
@@ -4795,7 +4868,7 @@ function importSpine(json, atlasText, opts = {}) {
4795
4868
  const build = () => ({
4796
4869
  format: "energy8.rig",
4797
4870
  version: 2,
4798
- meta: { name: opts.name ?? json.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
4871
+ meta: { name: opts.name ?? json2.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
4799
4872
  assets: [...assets.values()],
4800
4873
  bones,
4801
4874
  slots,
@@ -4848,7 +4921,7 @@ async function firstExisting(candidates, what) {
4848
4921
  throw new Error(`${what} not found, tried: ${candidates.join(", ")}`);
4849
4922
  }
4850
4923
  async function importSpineProject(opts) {
4851
- const json = JSON.parse(await readFile7(opts.json, "utf8"));
4924
+ const json2 = JSON.parse(await readFile7(opts.json, "utf8"));
4852
4925
  const stem = opts.json.replace(/\.json$/i, "");
4853
4926
  const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
4854
4927
  const atlasText = await readFile7(atlasPath, "utf8");
@@ -4858,6 +4931,7 @@ async function importSpineProject(opts) {
4858
4931
  const rigDir = dirname6(opts.rigPath);
4859
4932
  const pagePaths = /* @__PURE__ */ new Map();
4860
4933
  for (const page2 of pages) {
4934
+ if (!opts.image && outsideDir(dirname6(atlasPath), page2.name)) throw new Error(`atlas page "${page2.name}" is outside the atlas folder (${dirname6(atlasPath)})`);
4861
4935
  const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
4862
4936
  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}`);
4863
4937
  let pngPath = imagePath;
@@ -4870,10 +4944,10 @@ async function importSpineProject(opts) {
4870
4944
  }
4871
4945
  pagePaths.set(page2.name, pngPath);
4872
4946
  }
4873
- const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
4947
+ const pageImage = (page2) => relative2(rigDir, pagePaths.get(page2)).split("\\").join("/");
4874
4948
  const name = opts.name ?? basename4(stem);
4875
4949
  const imagePaths = [...pagePaths.values()];
4876
- return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
4950
+ return { ...importSpine(json2, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
4877
4951
  }
4878
4952
 
4879
4953
  // src/rig-templates.ts
@@ -5562,8 +5636,8 @@ async function load(path) {
5562
5636
  }
5563
5637
  var save = (path, doc) => saveJson(path, doc);
5564
5638
  var exists2 = (p) => stat2(p).then(() => true, () => false);
5565
- var resolveFrom = (rigPath, p) => isAbsolute(p) ? p : join9(dirname8(rigPath), p);
5566
- var srcFor = (rigPath, file) => relative2(dirname8(rigPath), file).split("\\").join("/");
5639
+ var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
5640
+ var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
5567
5641
  var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
5568
5642
  async function pngInfo(rigPath, file) {
5569
5643
  const abs = resolveFrom(rigPath, file);
@@ -5598,7 +5672,8 @@ function previewCheck(doc, animation) {
5598
5672
  const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
5599
5673
  return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
5600
5674
  }
5601
- 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() };
5675
+ var TrackId = { target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder", "control"]), id: z3.string(), prop: z3.string() };
5676
+ 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 };
5602
5677
  tool(
5603
5678
  "rig_set_control",
5604
5679
  "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.",
@@ -5791,11 +5866,21 @@ tool(
5791
5866
  );
5792
5867
  tool(
5793
5868
  "rig_check",
5794
- "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.",
5795
- { 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() },
5796
- async ({ path, ...opts }) => {
5869
+ "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.",
5870
+ {
5871
+ path: Path,
5872
+ animation: z3.string().optional().describe("limit to one animation (rest pose is always checked)"),
5873
+ fps: z3.number().positive().optional(),
5874
+ deformFps: z3.number().positive().optional(),
5875
+ secondaryBones: z3.array(z3.string()).optional(),
5876
+ frontOf: z3.array(z3.tuple([z3.string(), z3.string()])).optional(),
5877
+ landmarks: z3.record(z3.record(z3.tuple([z3.number(), z3.number()]))).optional(),
5878
+ landmarkTolerance: z3.number().nonnegative().optional(),
5879
+ ignore: z3.array(z3.object({ code: z3.string(), subject: z3.string().optional(), animation: z3.string().optional() }).strict()).optional()
5880
+ },
5881
+ async ({ path, ignore = [], ...opts }) => {
5797
5882
  try {
5798
- return text(summarize(checkRig(await load(path), opts), 100));
5883
+ return text(summarizeIgnoring(checkRig(await load(path), opts), ignore, 100));
5799
5884
  } catch (e) {
5800
5885
  return fail(e);
5801
5886
  }
@@ -5814,9 +5899,9 @@ tool(
5814
5899
  margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
5815
5900
  notes: z3.string().optional()
5816
5901
  },
5817
- async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
5902
+ async ({ path, json: json2, atlas, image, name, skin: skin2, margin, notes }) => {
5818
5903
  try {
5819
- 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 });
5904
+ 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 });
5820
5905
  const res = await create(path, parse(r.doc));
5821
5906
  if (r.warnings.length) res.text += `
5822
5907
  warnings:
@@ -6412,9 +6497,9 @@ tool(
6412
6497
  );
6413
6498
  tool(
6414
6499
  "rig_adjust_track",
6415
- "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.",
6500
+ "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.",
6416
6501
  { path: Path, animation: z3.string(), ...TrackRef, amplitude: z3.number().optional(), offset: z3.number().optional(), timeScale: z3.number().positive().optional() },
6417
- async ({ path, animation, target, id, prop, ...adj }) => mutate(path, (d) => adjustTrack(d, animation, { target, id, prop }, adj))
6502
+ async ({ path, animation, target, id, prop, interpolation, ...adj }) => mutate(path, (d) => adjustTrack(d, animation, { target, id, prop, interpolation }, adj))
6418
6503
  );
6419
6504
  tool(
6420
6505
  "rig_set_animation",
@@ -6432,7 +6517,7 @@ tool(
6432
6517
  tool(
6433
6518
  "rig_remove_keys",
6434
6519
  "Remove the keys of one track at the given times; an emptied track is dropped. Drops the preset link.",
6435
- { path: Path, animation: z3.string(), ...TrackRef, times: z3.array(z3.number()).min(1) },
6520
+ { path: Path, animation: z3.string(), ...TrackId, times: z3.array(z3.number()).min(1) },
6436
6521
  async ({ path, animation, target, id, prop, times: times2 }) => mutate(path, (d) => removeKeys(d, animation, { target, id, prop }, times2))
6437
6522
  );
6438
6523
  tool(
@@ -6588,40 +6673,41 @@ tool(
6588
6673
  }
6589
6674
  );
6590
6675
 
6676
+ // src/rig-version.ts
6677
+ import { stat as stat3 } from "node:fs/promises";
6678
+ async function rigVersion(abs) {
6679
+ try {
6680
+ const s = await stat3(abs);
6681
+ return `${s.mtimeMs}:${s.size}`;
6682
+ } catch {
6683
+ return "missing";
6684
+ }
6685
+ }
6686
+
6591
6687
  // src/editor/server.ts
6592
6688
  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" };
6593
- async function startEditorServer(opts) {
6689
+ var json = (res, status, body) => {
6690
+ res.writeHead(status, { "content-type": "application/json" });
6691
+ res.end(JSON.stringify(body));
6692
+ };
6693
+ function createEditorHandler(opts) {
6594
6694
  const root2 = resolve2(opts.root);
6595
6695
  const assets = resolve2(opts.assets ?? fileURLToPath3(new URL("../..", import.meta.url)));
6596
6696
  const inside = (rel) => {
6597
6697
  const abs = resolve2(root2, rel);
6598
- return abs === root2 || abs.startsWith(root2 + sep) ? abs : void 0;
6599
- };
6600
- const version = async (abs) => {
6601
- try {
6602
- const s = await stat3(abs);
6603
- return `${s.mtimeMs}:${s.size}`;
6604
- } catch {
6605
- return "missing";
6606
- }
6698
+ return abs === root2 || abs.startsWith(root2 + sep2) ? abs : void 0;
6607
6699
  };
6608
6700
  const readDoc = async (abs) => RigDocument.parse(JSON.parse(await readFile10(abs, "utf8")));
6609
6701
  const readDocAt = async (abs) => {
6610
- let v = await version(abs);
6702
+ let v = await rigVersion(abs);
6611
6703
  for (let i = 0; i < 3; i++) {
6612
6704
  const doc = await readDoc(abs);
6613
- const after = await version(abs);
6705
+ const after = await rigVersion(abs);
6614
6706
  if (after === v) return { doc, version: v };
6615
6707
  v = after;
6616
6708
  }
6617
6709
  return { doc: await readDoc(abs), version: v };
6618
6710
  };
6619
- const json = (res, status, body2) => {
6620
- res.writeHead(status, { "content-type": "application/json" });
6621
- res.end(JSON.stringify(body2));
6622
- };
6623
- let allowedHosts = /* @__PURE__ */ new Set();
6624
- const hostOk = (req) => allowedHosts.has(String(req.headers.host ?? "").toLowerCase());
6625
6711
  const clients = /* @__PURE__ */ new Map();
6626
6712
  const watchers = /* @__PURE__ */ new Map();
6627
6713
  function ensureWatch(abs) {
@@ -6633,7 +6719,7 @@ async function startEditorServer(opts) {
6633
6719
  const schedule = () => {
6634
6720
  clearTimeout(entry.timer);
6635
6721
  entry.timer = setTimeout(async () => {
6636
- const v = await version(abs);
6722
+ const v = await rigVersion(abs);
6637
6723
  if (v === entry.last) return;
6638
6724
  entry.last = v;
6639
6725
  for (const res of clients.get(abs) ?? []) res.write(`event: changed
@@ -6642,7 +6728,7 @@ data: ${JSON.stringify({ version: v })}
6642
6728
  `);
6643
6729
  }, 50);
6644
6730
  };
6645
- version(abs).then((v) => {
6731
+ rigVersion(abs).then((v) => {
6646
6732
  entry.last = v;
6647
6733
  });
6648
6734
  watchers.set(abs, entry);
@@ -6668,7 +6754,7 @@ data: ${JSON.stringify({ version: v })}
6668
6754
  name = String(JSON.parse(await readFile10(abs, "utf8")).meta?.name ?? "?");
6669
6755
  } catch {
6670
6756
  }
6671
- out.push({ rig: relative3(root2, abs).split(sep).join("/"), name, mtimeMs: (await stat3(abs)).mtimeMs });
6757
+ out.push({ rig: relative4(root2, abs).split(sep2).join("/"), name, mtimeMs: (await stat4(abs)).mtimeMs });
6672
6758
  }
6673
6759
  };
6674
6760
  await walk(root2);
@@ -6702,20 +6788,22 @@ data: ${JSON.stringify({ version: v })}
6702
6788
  }
6703
6789
  return Buffer.concat(chunks);
6704
6790
  }
6705
- const server = createServer(async (req, res) => {
6791
+ async function handle(req, res, rawUrl = req.url ?? "/") {
6706
6792
  try {
6707
- 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` });
6708
- const url = new URL(req.url ?? "/", "http://x");
6793
+ const url = new URL(rawUrl, "http://x");
6709
6794
  if (url.pathname === "/") {
6710
- res.writeHead(302, { location: "/editor.html" + url.search });
6711
- return res.end();
6795
+ res.writeHead(302, { location: "editor.html" + url.search });
6796
+ return void res.end();
6797
+ }
6798
+ if (url.pathname.startsWith("/api/")) {
6799
+ await api(url, req, res);
6800
+ return;
6712
6801
  }
6713
- if (url.pathname.startsWith("/api/")) return await api(url, req, res);
6714
6802
  const STATIC = /* @__PURE__ */ new Set(["/editor.html", "/dist/editor.js", "/dist/editor.js.map", "/dist/editor.css", "/dist/editor.css.map"]);
6715
6803
  const abs = url.pathname.startsWith("/rig/") ? inside(decodeURIComponent(url.pathname.slice(5))) : STATIC.has(url.pathname) ? join10(assets, url.pathname) : void 0;
6716
6804
  if (!abs) {
6717
6805
  res.writeHead(403);
6718
- return res.end("forbidden");
6806
+ return void res.end("forbidden");
6719
6807
  }
6720
6808
  try {
6721
6809
  const data = await readFile10(abs);
@@ -6733,7 +6821,7 @@ data: ${JSON.stringify({ version: v })}
6733
6821
  }
6734
6822
  json(res, 500, { isError: true, text: `ERROR: ${e.message}` });
6735
6823
  }
6736
- });
6824
+ }
6737
6825
  async function api(url, req, res) {
6738
6826
  const rigOf = (rel) => {
6739
6827
  if (!rel) return void 0;
@@ -6750,7 +6838,7 @@ data: ${JSON.stringify({ version: v })}
6750
6838
  res.writeHead(403);
6751
6839
  return res.end();
6752
6840
  }
6753
- if (!await stat3(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root2}` });
6841
+ if (!await stat4(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root2}` });
6754
6842
  ensureWatch(abs);
6755
6843
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
6756
6844
  res.write(`: connected
@@ -6772,7 +6860,7 @@ data: ${JSON.stringify({ version: v })}
6772
6860
  if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
6773
6861
  ensureWatch(abs);
6774
6862
  return await withRigQueue(abs, async () => {
6775
- if (baseVersion !== void 0 && await version(abs) !== baseVersion) {
6863
+ if (baseVersion !== void 0 && await rigVersion(abs) !== baseVersion) {
6776
6864
  const at = await readDocAt(abs);
6777
6865
  return json(res, 409, {
6778
6866
  isError: true,
@@ -6781,8 +6869,15 @@ data: ${JSON.stringify({ version: v })}
6781
6869
  text: `ERROR: the rig changed on disk (expected ${baseVersion}, found ${at.version}); nothing was written`
6782
6870
  });
6783
6871
  }
6784
- const r = await callTool(url.pathname.slice(10), { ...args ?? {}, path: abs });
6785
- return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await version(abs) });
6872
+ const tool2 = url.pathname.slice(10);
6873
+ let given = { ...args ?? {}, path: abs };
6874
+ try {
6875
+ if (opts.args) given = opts.args(tool2, given);
6876
+ } catch (e) {
6877
+ return json(res, 400, { isError: true, text: `ERROR: ${e instanceof Error ? e.message : String(e)}` });
6878
+ }
6879
+ const r = await callTool(tool2, given);
6880
+ return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await rigVersion(abs) });
6786
6881
  });
6787
6882
  }
6788
6883
  if (url.pathname === "/api/rigs" && req.method === "GET") return json(res, 200, await listRigs());
@@ -6793,11 +6888,11 @@ data: ${JSON.stringify({ version: v })}
6793
6888
  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" });
6794
6889
  const abs = inside(rig);
6795
6890
  if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
6796
- 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` });
6891
+ 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` });
6797
6892
  await mkdir7(dirname9(abs), { recursive: true });
6798
6893
  const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
6799
6894
  ensureWatch(abs);
6800
- return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await version(abs) });
6895
+ return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await rigVersion(abs) });
6801
6896
  }
6802
6897
  if (url.pathname === "/api/files" && req.method === "GET") {
6803
6898
  const abs = rigOf(url.searchParams.get("rig"));
@@ -6811,8 +6906,8 @@ data: ${JSON.stringify({ version: v })}
6811
6906
  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` });
6812
6907
  if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("image/png")) return json(res, 415, { isError: true, text: "ERROR: upload needs content-type: image/png" });
6813
6908
  const target = join10(dirname9(abs), name);
6814
- if (!inside(relative3(root2, target))) return json(res, 403, { isError: true, text: "file outside root" });
6815
- 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` });
6909
+ if (!inside(relative4(root2, target))) return json(res, 403, { isError: true, text: "file outside root" });
6910
+ 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` });
6816
6911
  const data = await bytes(req, UPLOAD_MAX);
6817
6912
  if (!data) return json(res, 413, { isError: true, text: `ERROR: ${name} is larger than ${UPLOAD_MAX / 1048576} MB` });
6818
6913
  let size;
@@ -6826,6 +6921,22 @@ data: ${JSON.stringify({ version: v })}
6826
6921
  }
6827
6922
  json(res, 404, { isError: true, text: "unknown api route" });
6828
6923
  }
6924
+ return { handle, close() {
6925
+ for (const w of watchers.values()) {
6926
+ clearTimeout(w.timer);
6927
+ w.w.close();
6928
+ }
6929
+ for (const set of clients.values()) for (const c of set) c.end();
6930
+ } };
6931
+ }
6932
+ async function startEditorServer(opts) {
6933
+ const editor = createEditorHandler(opts);
6934
+ let allowedHosts = /* @__PURE__ */ new Set();
6935
+ const server = createServer((req, res) => {
6936
+ if (!allowedHosts.has(String(req.headers.host ?? "").toLowerCase()))
6937
+ return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
6938
+ void editor.handle(req, res);
6939
+ });
6829
6940
  await new Promise((ok, fail2) => {
6830
6941
  server.once("error", fail2);
6831
6942
  server.listen(opts.port ?? 8765, "127.0.0.1", () => {
@@ -6836,15 +6947,12 @@ data: ${JSON.stringify({ version: v })}
6836
6947
  const port = server.address().port;
6837
6948
  allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
6838
6949
  return { port, url: `http://127.0.0.1:${port}`, close: async () => {
6839
- for (const w of watchers.values()) {
6840
- clearTimeout(w.timer);
6841
- w.w.close();
6842
- }
6843
- for (const set of clients.values()) for (const c of set) c.end();
6950
+ editor.close();
6844
6951
  await new Promise((r) => server.close(() => r()));
6845
6952
  } };
6846
6953
  }
6847
6954
  export {
6955
+ createEditorHandler,
6848
6956
  startEditorServer
6849
6957
  };
6850
6958
  //# sourceMappingURL=editor-entry.js.map