@energy8platform/golem 0.7.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.
package/dist/lib/cli.js CHANGED
@@ -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) }),
@@ -737,6 +737,57 @@ function smoothValue(track, i, t, component, loop) {
737
737
  const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;
738
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);
739
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
+ }
740
791
  function sample(track, t, loop = false) {
741
792
  const keys = track.keys;
742
793
  if (t <= keys[0].t) return keys[0].v;
@@ -748,11 +799,13 @@ function sample(track, t, loop = false) {
748
799
  if (a.ease === "step") return a.v;
749
800
  if (typeof a.v === "number" && typeof b.v === "number") {
750
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);
751
803
  const u = ease(a.ease)((t - a.t) / (b.t - a.t));
752
804
  return a.v + (b.v - a.v) * u;
753
805
  }
754
806
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
755
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));
756
809
  const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
757
810
  return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
758
811
  }
@@ -2484,7 +2537,8 @@ function adjustTrack(doc, animId, track, adj) {
2484
2537
  return { ...k, t: r42(adj.timeScale ? k.t * adj.timeScale : k.t), v: r42(v) };
2485
2538
  });
2486
2539
  const duration = Math.max(anim.duration, keys[keys.length - 1].t);
2487
- 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 };
2488
2542
  return { ...doc, animations: doc.animations.map((a) => a.id === animId ? next : a) };
2489
2543
  }
2490
2544
  function removeAnimation(doc, id) {
@@ -2901,19 +2955,20 @@ var requireSlot = (doc, id) => {
2901
2955
  if (!s2) throw new Error(`unknown slot "${id}"`);
2902
2956
  return s2;
2903
2957
  };
2904
- function sampled(duration, n, f, ease2 = "linear") {
2958
+ function sampled(duration, n, f) {
2905
2959
  const keys = [];
2906
2960
  for (let i = 0; i <= n; i++) {
2907
2961
  const t = +(duration * i / n).toFixed(4);
2908
- keys.push({ t, v: +f(t).toFixed(4) + 0, ease: ease2 });
2962
+ keys.push({ t, v: +f(t).toFixed(4) + 0, ease: "linear" });
2909
2963
  }
2910
2964
  return keys;
2911
2965
  }
2912
- function boneTrack(bone, prop, keys) {
2913
- 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 } };
2914
2969
  }
2915
- function deformTrack(slot, prop, keys) {
2916
- 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;
2917
2972
  }
2918
2973
  var pushTrack = (tracks, t) => {
2919
2974
  if (t) tracks.push(t);
@@ -2937,18 +2992,18 @@ var idleBreathing = {
2937
2992
  const armsR = byRole(doc, "arm_r", "arm_upper_r");
2938
2993
  const chest = slotOfBone(doc, torso);
2939
2994
  if (chest?.attachment) {
2940
- 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));
2941
2996
  } else {
2942
2997
  const r = torso.transform;
2943
- tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t), "ease_in_out")));
2944
- 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));
2945
3000
  }
2946
3001
  for (const h of heads)
2947
- 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));
2948
3003
  for (const a of armsL)
2949
- 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));
2950
3005
  for (const a of armsR)
2951
- 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));
2952
3007
  return finish(this.name, p.id, T2, true, tracks, p);
2953
3008
  }
2954
3009
  };
@@ -2957,22 +3012,20 @@ var winBounce = {
2957
3012
  defaults: { id: "win", duration: 1.2, height: 40, bounces: 2, squash: 0.15 },
2958
3013
  build(doc, p) {
2959
3014
  const b = root(doc), r = b.transform, tracks = [];
2960
- const yK = [[0, r.y, "ease_out"]];
2961
- const sxK = [[0, r.scaleX, "ease_out"]];
2962
- const syK = [[0, r.scaleY, "ease_out"]];
3015
+ const yK = [[0, r.y]], sxK = [[0, r.scaleX]], syK = [[0, r.scaleY]];
2963
3016
  const per = p.duration / p.bounces;
2964
3017
  for (let i = 0; i < p.bounces; i++) {
2965
3018
  const t0 = i * per, h = p.height * 0.55 ** i, s2 = p.squash * 0.7 ** i;
2966
- 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"]);
2967
- 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"]);
2968
- 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]);
2969
3022
  }
2970
3023
  yK.push([p.duration, r.y]);
2971
3024
  sxK.push([p.duration, r.scaleX]);
2972
3025
  syK.push([p.duration, r.scaleY]);
2973
- 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));
2974
3027
  for (const h of byRole(doc, "head"))
2975
- 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));
2976
3029
  const anim = finish(this.name, p.id, p.duration, false, tracks, p);
2977
3030
  anim.events.push({ t: 0, name: "win_start" }, { t: per * 0.95, name: "land" });
2978
3031
  return anim;
@@ -2985,8 +3038,8 @@ var anticipationShake = {
2985
3038
  const b = root(doc), r = b.transform, tracks = [];
2986
3039
  const n = Math.ceil(p.duration * p.frequency * 4);
2987
3040
  const env = (t) => p.grow ? t / p.duration : 1;
2988
- 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")));
2989
- 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));
2990
3043
  tracks.push(boneTrack(b, "scaleX", K([0, r.scaleX, "ease_in"], [p.duration, r.scaleX * p.scaleUp])));
2991
3044
  tracks.push(boneTrack(b, "scaleY", K([0, r.scaleY, "ease_in"], [p.duration, r.scaleY * p.scaleUp])));
2992
3045
  return finish(this.name, p.id, p.duration, false, tracks, p);
@@ -3011,6 +3064,7 @@ var mouthTalk = {
3011
3064
  if (!p.frames.length) throw new Error("mouth_talk: frames must not be empty");
3012
3065
  const n = Math.max(1, Math.round(p.duration * p.rate));
3013
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]));
3014
3068
  return finish(this.name, p.id, p.duration, true, [slotTrack(slot, "attachment", keys)], p);
3015
3069
  }
3016
3070
  };
@@ -3028,10 +3082,10 @@ var symbolIdle = {
3028
3082
  build(doc, p) {
3029
3083
  const b = root(doc), r = b.transform, T2 = p.period, w = 2 * Math.PI / T2, tracks = [];
3030
3084
  const rise = (t) => 0.5 - 0.5 * Math.cos(w * t);
3031
- tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t), "ease_in_out")));
3032
- 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));
3033
3087
  if (p.pulse) for (const s2 of doc.slots.filter((s3) => s3.bone === b.id && s3.attachment))
3034
- 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));
3035
3089
  return finish(this.name, p.id, T2, true, tracks, p);
3036
3090
  }
3037
3091
  };
@@ -3041,12 +3095,12 @@ var symbolWin = {
3041
3095
  build(doc, p) {
3042
3096
  const b = root(doc), r = b.transform, d = p.duration, tracks = [];
3043
3097
  for (const [prop, rest] of [["scaleX", r.scaleX], ["scaleY", r.scaleY]])
3044
- 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));
3045
3099
  if (p.wobble) {
3046
3100
  const n = Math.max(8, p.wobbles * 4);
3047
- 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));
3048
3102
  keys[keys.length - 1].v = 0;
3049
- 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));
3050
3104
  }
3051
3105
  const anim = finish(this.name, p.id, d, false, tracks, p);
3052
3106
  anim.events.push({ t: 0, name: "win_start" });
@@ -3060,11 +3114,11 @@ var weightShift = {
3060
3114
  const T2 = p.period, w = 2 * Math.PI / T2, N = 16, tracks = [];
3061
3115
  const hips = byRole(doc, "hips")[0] ?? root(doc), hr = hips.transform;
3062
3116
  const s2 = (t) => Math.sin(w * t);
3063
- tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s2(t))));
3064
- 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));
3065
3119
  const torso = byRole(doc, "torso", "body")[0];
3066
- if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s2(t))));
3067
- 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));
3068
3122
  for (const leg of byRole(doc, "leg_l", "leg_r")) {
3069
3123
  const slot = slotOfBone(doc, leg), att = slot && doc.attachments.find((a) => a.id === slot.attachment && a.type === "region");
3070
3124
  const asset = att && att.type === "region" ? doc.assets.find((a) => a.id === att.asset) : void 0;
@@ -3074,8 +3128,8 @@ var weightShift = {
3074
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))));
3075
3129
  }
3076
3130
  if (!(length > p.shift)) continue;
3077
- tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s2(t) / length) * 180 / Math.PI)));
3078
- 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));
3079
3133
  }
3080
3134
  return finish(this.name, p.id, T2, true, tracks, p);
3081
3135
  }
@@ -3318,13 +3372,13 @@ function motionQuality(doc, anim, opts = {}) {
3318
3372
  const before = delta(values(k.v), values(p.v)), after = delta(values(n.v), values(k.v));
3319
3373
  const pass = before.some((v, j2) => v * after[j2] > 1e-6);
3320
3374
  const speed = Math.max(magnitude(before) / (k.t - p.t), magnitude(after) / (n.t - k.t));
3321
- 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);
3322
3376
  const jump = magnitude(delta(left, right));
3323
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);
3324
3378
  const acceleration = (a, b, c) => values(at(b)).map((v, j2) => (values(at(c))[j2] - 2 * v + values(at(a))[j2]) / (h * h));
3325
3379
  const al = acceleration(k.t - 2 * h, k.t - h, k.t), ar = acceleration(k.t, k.t + h, k.t + 2 * h);
3326
3380
  const aj = magnitude(delta(al, ar));
3327
- 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");
3328
3382
  }
3329
3383
  }
3330
3384
  const steps = Math.max(2, Math.ceil(D * 60));
@@ -3806,6 +3860,17 @@ function checkRig(doc, opts = {}) {
3806
3860
  out.push({ level: "warn", code: "never_visible", subject: s2.id, message: `slot "${s2.id}" has no attachment and no animation ever shows one` });
3807
3861
  return out;
3808
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` : ""}`;
3809
3874
  function summarize(findings, max = 12) {
3810
3875
  if (!findings.length) return "check: OK";
3811
3876
  const rank = { error: 0, warn: 1, info: 2 };
@@ -3820,7 +3885,7 @@ function summarize(findings, max = 12) {
3820
3885
  const lines = [];
3821
3886
  for (const f of sorted) {
3822
3887
  const shown = shownPerCode.get(f.code) ?? 0;
3823
- 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}`);
3824
3889
  else if (shown === PER_CODE) lines.push(` \u2026 ${codeTotal.get(f.code) - PER_CODE} more ${f.code}`);
3825
3890
  shownPerCode.set(f.code, shown + 1);
3826
3891
  }
@@ -4011,7 +4076,7 @@ function packRects(rects, opts = {}) {
4011
4076
  shelf = Math.max(shelf, r.height);
4012
4077
  }
4013
4078
  const height = y + shelf + pad;
4014
- 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 };
4015
4080
  if (width >= max) throw new Error(`atlas does not fit in ${max}\xD7${max} (needs ${width}\xD7${height})`);
4016
4081
  width *= 2;
4017
4082
  }
@@ -5618,7 +5683,8 @@ function previewCheck(doc, animation) {
5618
5683
  const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
5619
5684
  return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
5620
5685
  }
5621
- 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 };
5622
5688
  tool(
5623
5689
  "rig_set_control",
5624
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.",
@@ -5811,11 +5877,21 @@ tool(
5811
5877
  );
5812
5878
  tool(
5813
5879
  "rig_check",
5814
- "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.",
5815
- { 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() },
5816
- 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 }) => {
5817
5893
  try {
5818
- return text(summarize(checkRig(await load(path), opts), 100));
5894
+ return text(summarizeIgnoring(checkRig(await load(path), opts), ignore, 100));
5819
5895
  } catch (e) {
5820
5896
  return fail(e);
5821
5897
  }
@@ -6432,9 +6508,9 @@ tool(
6432
6508
  );
6433
6509
  tool(
6434
6510
  "rig_adjust_track",
6435
- "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.",
6436
6512
  { path: Path, animation: z3.string(), ...TrackRef, amplitude: z3.number().optional(), offset: z3.number().optional(), timeScale: z3.number().positive().optional() },
6437
- 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))
6438
6514
  );
6439
6515
  tool(
6440
6516
  "rig_set_animation",
@@ -6452,7 +6528,7 @@ tool(
6452
6528
  tool(
6453
6529
  "rig_remove_keys",
6454
6530
  "Remove the keys of one track at the given times; an emptied track is dropped. Drops the preset link.",
6455
- { 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) },
6456
6532
  async ({ path, animation, target, id, prop, times: times2 }) => mutate(path, (d) => removeKeys(d, animation, { target, id, prop }, times2))
6457
6533
  );
6458
6534
  tool(