@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.
@@ -250,7 +250,7 @@ var Key = z.object({
250
250
  /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
251
251
  componentEase: z.array(Ease).min(1).optional()
252
252
  });
253
- var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
253
+ var Interpolation = { interpolation: z.enum(["segment", "pchip", "spline"]).optional() };
254
254
  var Track = z.discriminatedUnion("target", [
255
255
  z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
256
256
  z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
@@ -736,6 +736,57 @@ function smoothValue(track, i, t, component, loop) {
736
736
  const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;
737
737
  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);
738
738
  }
739
+ function tridiagonal(a, b, c, r) {
740
+ const n = b.length, cp = [], x = [];
741
+ let beta = b[0];
742
+ x[0] = r[0] / beta;
743
+ for (let i = 1; i < n; i++) {
744
+ cp[i] = c[i - 1] / beta;
745
+ beta = b[i] - a[i] * cp[i];
746
+ x[i] = (r[i] - a[i] * x[i - 1]) / beta;
747
+ }
748
+ for (let i = n - 2; i >= 0; i--) x[i] -= cp[i + 1] * x[i + 1];
749
+ return x;
750
+ }
751
+ function splineMoments(t, v, periodic) {
752
+ const n = t.length, h = (j2) => t[j2 + 1] - t[j2], d = (j2) => (v[j2 + 1] - v[j2]) / h(j2);
753
+ if (!periodic) {
754
+ const a2 = [], b2 = [], c2 = [], r2 = [];
755
+ for (let i = 0; i < n; i++) {
756
+ const hp = i > 0 ? h(i - 1) : 0, hn = i < n - 1 ? h(i) : 0;
757
+ a2.push(hp);
758
+ b2.push(2 * (hp + hn));
759
+ c2.push(hn);
760
+ r2.push(6 * ((i < n - 1 ? d(i) : 0) - (i > 0 ? d(i - 1) : 0)));
761
+ }
762
+ return tridiagonal(a2, b2, c2, r2);
763
+ }
764
+ const m = n - 1, a = [], b = [], c = [], r = [];
765
+ for (let i = 0; i < m; i++) {
766
+ const p = (i + m - 1) % m;
767
+ a.push(h(p));
768
+ b.push(2 * (h(p) + h(i)));
769
+ c.push(h(i));
770
+ r.push(6 * (d(i) - d(p)));
771
+ }
772
+ const top = a[0], bottom = c[m - 1], gamma = -b[0];
773
+ const bb = [...b];
774
+ bb[0] -= gamma;
775
+ bb[m - 1] -= bottom * top / gamma;
776
+ const x = tridiagonal(a, bb, c, r), u = new Array(m).fill(0);
777
+ u[0] = gamma;
778
+ u[m - 1] = bottom;
779
+ const z4 = tridiagonal(a, bb, c, u);
780
+ const f = (x[0] + top * x[m - 1] / gamma) / (1 + z4[0] + top * z4[m - 1] / gamma);
781
+ const M = x.map((xi, i) => xi - f * z4[i]);
782
+ return [...M, M[0]];
783
+ }
784
+ function splineValue(track, i, t, component, loop) {
785
+ const k = track.keys, ts = k.map((x) => x.t), vs = k.map((x) => component === void 0 ? x.v : x.v[component]);
786
+ const M = splineMoments(ts, vs, loop && k.length > 3 && vs[0] === vs[k.length - 1]);
787
+ const H = ts[i + 1] - ts[i], A = (ts[i + 1] - t) / H, B = (t - ts[i]) / H;
788
+ return A * vs[i] + B * vs[i + 1] + ((A ** 3 - A) * M[i] + (B ** 3 - B) * M[i + 1]) * H * H / 6;
789
+ }
739
790
  function sample(track, t, loop = false) {
740
791
  const keys = track.keys;
741
792
  if (t <= keys[0].t) return keys[0].v;
@@ -747,11 +798,13 @@ function sample(track, t, loop = false) {
747
798
  if (a.ease === "step") return a.v;
748
799
  if (typeof a.v === "number" && typeof b.v === "number") {
749
800
  if (track.interpolation === "pchip") return smoothValue(track, i, t, void 0, loop);
801
+ if (track.interpolation === "spline") return splineValue(track, i, t, void 0, loop);
750
802
  const u = ease(a.ease)((t - a.t) / (b.t - a.t));
751
803
  return a.v + (b.v - a.v) * u;
752
804
  }
753
805
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
754
806
  if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
807
+ if (track.interpolation === "spline") return a.v.map((_, j2) => splineValue(track, i, t, j2, loop));
755
808
  const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
756
809
  return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
757
810
  }
@@ -2483,7 +2536,8 @@ function adjustTrack(doc, animId, track, adj) {
2483
2536
  return { ...k, t: r42(adj.timeScale ? k.t * adj.timeScale : k.t), v: r42(v) };
2484
2537
  });
2485
2538
  const duration = Math.max(anim.duration, keys[keys.length - 1].t);
2486
- const next = { ...anim, duration, tracks: anim.tracks.map((t) => same(t) ? { ...t, keys } : t), preset: void 0 };
2539
+ const interpolation = track.interpolation ?? tr.interpolation;
2540
+ const next = { ...anim, duration, tracks: anim.tracks.map((t) => same(t) ? { ...t, keys, ...interpolation && { interpolation } } : t), preset: void 0 };
2487
2541
  return { ...doc, animations: doc.animations.map((a) => a.id === animId ? next : a) };
2488
2542
  }
2489
2543
  function removeAnimation(doc, id) {
@@ -2900,19 +2954,20 @@ var requireSlot = (doc, id) => {
2900
2954
  if (!s) throw new Error(`unknown slot "${id}"`);
2901
2955
  return s;
2902
2956
  };
2903
- function sampled(duration, n, f, ease2 = "linear") {
2957
+ function sampled(duration, n, f) {
2904
2958
  const keys = [];
2905
2959
  for (let i = 0; i <= n; i++) {
2906
2960
  const t = +(duration * i / n).toFixed(4);
2907
- keys.push({ t, v: +f(t).toFixed(4) + 0, ease: ease2 });
2961
+ keys.push({ t, v: +f(t).toFixed(4) + 0, ease: "linear" });
2908
2962
  }
2909
2963
  return keys;
2910
2964
  }
2911
- function boneTrack(bone, prop, keys) {
2912
- return { target: "bone", id: bone.id, prop, keys };
2965
+ var smooth = "spline";
2966
+ function boneTrack(bone, prop, keys, interpolation) {
2967
+ return { target: "bone", id: bone.id, prop, keys, ...interpolation && { interpolation } };
2913
2968
  }
2914
- function deformTrack(slot, prop, keys) {
2915
- return slot.attachment ? { target: "deform", id: slot.attachment, prop, keys } : void 0;
2969
+ function deformTrack(slot, prop, keys, interpolation) {
2970
+ return slot.attachment ? { target: "deform", id: slot.attachment, prop, keys, ...interpolation && { interpolation } } : void 0;
2916
2971
  }
2917
2972
  var pushTrack = (tracks, t) => {
2918
2973
  if (t) tracks.push(t);
@@ -2936,18 +2991,18 @@ var idleBreathing = {
2936
2991
  const armsR = byRole(doc, "arm_r", "arm_upper_r");
2937
2992
  const chest = slotOfBone(doc, torso);
2938
2993
  if (chest?.attachment) {
2939
- pushTrack(tracks, deformTrack(chest, "bulge", sampled(T2, 8, (t) => p.amplitude * Math.sin(w * t), "ease_in_out")));
2994
+ pushTrack(tracks, deformTrack(chest, "bulge", sampled(T2, 8, (t) => p.amplitude * Math.sin(w * t)), smooth));
2940
2995
  } else {
2941
2996
  const r = torso.transform;
2942
- tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t), "ease_in_out")));
2943
- tracks.push(boneTrack(torso, "scaleX", sampled(T2, 8, (t) => r.scaleX - p.amplitude * 0.4 * Math.sin(w * t), "ease_in_out")));
2997
+ tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t)), smooth));
2998
+ tracks.push(boneTrack(torso, "scaleX", sampled(T2, 8, (t) => r.scaleX - p.amplitude * 0.4 * Math.sin(w * t)), smooth));
2944
2999
  }
2945
3000
  for (const h of heads)
2946
- tracks.push(boneTrack(h, "y", sampled(T2, 8, (t) => h.transform.y - p.headBob * Math.sin(w * t + 0.4), "ease_in_out")));
3001
+ tracks.push(boneTrack(h, "y", sampled(T2, 8, (t) => h.transform.y - p.headBob * Math.sin(w * t + 0.4)), smooth));
2947
3002
  for (const a of armsL)
2948
- 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));
2949
3004
  for (const a of armsR)
2950
- tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation - p.armSway * Math.sin(w * t + 0.8), "ease_in_out")));
3005
+ tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation - p.armSway * Math.sin(w * t + 0.8)), smooth));
2951
3006
  return finish(this.name, p.id, T2, true, tracks, p);
2952
3007
  }
2953
3008
  };
@@ -2956,22 +3011,20 @@ var winBounce = {
2956
3011
  defaults: { id: "win", duration: 1.2, height: 40, bounces: 2, squash: 0.15 },
2957
3012
  build(doc, p) {
2958
3013
  const b = root(doc), r = b.transform, tracks = [];
2959
- const yK = [[0, r.y, "ease_out"]];
2960
- const sxK = [[0, r.scaleX, "ease_out"]];
2961
- const syK = [[0, r.scaleY, "ease_out"]];
3014
+ const yK = [[0, r.y]], sxK = [[0, r.scaleX]], syK = [[0, r.scaleY]];
2962
3015
  const per = p.duration / p.bounces;
2963
3016
  for (let i = 0; i < p.bounces; i++) {
2964
3017
  const t0 = i * per, h = p.height * 0.55 ** i, s = p.squash * 0.7 ** i;
2965
- 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"]);
2966
- 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"]);
2967
- 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"]);
3018
+ 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)]);
3019
+ 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)]);
3020
+ yK.push([t0 + per * 0.08, r.y], [t0 + per * 0.5, r.y - h], [t0 + per * 0.95, r.y]);
2968
3021
  }
2969
3022
  yK.push([p.duration, r.y]);
2970
3023
  sxK.push([p.duration, r.scaleX]);
2971
3024
  syK.push([p.duration, r.scaleY]);
2972
- tracks.push(boneTrack(b, "y", K(...yK)), boneTrack(b, "scaleX", K(...sxK)), boneTrack(b, "scaleY", K(...syK)));
3025
+ tracks.push(boneTrack(b, "y", K(...yK), "pchip"), boneTrack(b, "scaleX", K(...sxK), smooth), boneTrack(b, "scaleY", K(...syK), smooth));
2973
3026
  for (const h of byRole(doc, "head"))
2974
- 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])));
3027
+ 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));
2975
3028
  const anim = finish(this.name, p.id, p.duration, false, tracks, p);
2976
3029
  anim.events.push({ t: 0, name: "win_start" }, { t: per * 0.95, name: "land" });
2977
3030
  return anim;
@@ -2984,8 +3037,8 @@ var anticipationShake = {
2984
3037
  const b = root(doc), r = b.transform, tracks = [];
2985
3038
  const n = Math.ceil(p.duration * p.frequency * 4);
2986
3039
  const env = (t) => p.grow ? t / p.duration : 1;
2987
- 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")));
2988
- 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")));
3040
+ 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));
3041
+ 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));
2989
3042
  tracks.push(boneTrack(b, "scaleX", K([0, r.scaleX, "ease_in"], [p.duration, r.scaleX * p.scaleUp])));
2990
3043
  tracks.push(boneTrack(b, "scaleY", K([0, r.scaleY, "ease_in"], [p.duration, r.scaleY * p.scaleUp])));
2991
3044
  return finish(this.name, p.id, p.duration, false, tracks, p);
@@ -3010,6 +3063,7 @@ var mouthTalk = {
3010
3063
  if (!p.frames.length) throw new Error("mouth_talk: frames must not be empty");
3011
3064
  const n = Math.max(1, Math.round(p.duration * p.rate));
3012
3065
  const keys = S(...Array.from({ length: n }, (_, i) => [+(i / p.rate).toFixed(4), p.frames[i % p.frames.length]]));
3066
+ if (keys.at(-1).t < p.duration && keys.at(-1).v !== keys[0].v) keys.push(...S([p.duration, keys[0].v]));
3013
3067
  return finish(this.name, p.id, p.duration, true, [slotTrack(slot, "attachment", keys)], p);
3014
3068
  }
3015
3069
  };
@@ -3027,10 +3081,10 @@ var symbolIdle = {
3027
3081
  build(doc, p) {
3028
3082
  const b = root(doc), r = b.transform, T2 = p.period, w = 2 * Math.PI / T2, tracks = [];
3029
3083
  const rise = (t) => 0.5 - 0.5 * Math.cos(w * t);
3030
- tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t), "ease_in_out")));
3031
- if (p.tilt) tracks.push(boneTrack(b, "rotation", sampled(T2, 8, (t) => r.rotation + p.tilt * Math.sin(w * t), "ease_in_out")));
3084
+ tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t)), smooth));
3085
+ if (p.tilt) tracks.push(boneTrack(b, "rotation", sampled(T2, 8, (t) => r.rotation + p.tilt * Math.sin(w * t)), smooth));
3032
3086
  if (p.pulse) for (const s of doc.slots.filter((s2) => s2.bone === b.id && s2.attachment))
3033
- pushTrack(tracks, deformTrack(s, "bulge", sampled(T2, 8, (t) => p.pulse * rise(t), "ease_in_out")));
3087
+ pushTrack(tracks, deformTrack(s, "bulge", sampled(T2, 8, (t) => p.pulse * rise(t)), smooth));
3034
3088
  return finish(this.name, p.id, T2, true, tracks, p);
3035
3089
  }
3036
3090
  };
@@ -3040,12 +3094,12 @@ var symbolWin = {
3040
3094
  build(doc, p) {
3041
3095
  const b = root(doc), r = b.transform, d = p.duration, tracks = [];
3042
3096
  for (const [prop, rest] of [["scaleX", r.scaleX], ["scaleY", r.scaleY]])
3043
- 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])));
3097
+ 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));
3044
3098
  if (p.wobble) {
3045
3099
  const n = Math.max(8, p.wobbles * 4);
3046
- 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");
3100
+ const keys = sampled(d, n, (t) => p.wobble * Math.exp(-3 * t / d) * Math.sin(2 * Math.PI * p.wobbles * t / d));
3047
3101
  keys[keys.length - 1].v = 0;
3048
- for (const s of doc.slots.filter((s2) => s2.bone === b.id && s2.attachment)) pushTrack(tracks, deformTrack(s, "sway", keys));
3102
+ for (const s of doc.slots.filter((s2) => s2.bone === b.id && s2.attachment)) pushTrack(tracks, deformTrack(s, "sway", keys, smooth));
3049
3103
  }
3050
3104
  const anim = finish(this.name, p.id, d, false, tracks, p);
3051
3105
  anim.events.push({ t: 0, name: "win_start" });
@@ -3059,11 +3113,11 @@ var weightShift = {
3059
3113
  const T2 = p.period, w = 2 * Math.PI / T2, N = 16, tracks = [];
3060
3114
  const hips = byRole(doc, "hips")[0] ?? root(doc), hr = hips.transform;
3061
3115
  const s = (t) => Math.sin(w * t);
3062
- tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s(t))));
3063
- if (p.dip) tracks.push(boneTrack(hips, "y", sampled(T2, N, (t) => hr.y + p.dip * s(t) ** 2)));
3116
+ tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s(t)), smooth));
3117
+ if (p.dip) tracks.push(boneTrack(hips, "y", sampled(T2, N, (t) => hr.y + p.dip * s(t) ** 2), smooth));
3064
3118
  const torso = byRole(doc, "torso", "body")[0];
3065
- if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s(t))));
3066
- for (const h of byRole(doc, "head")) tracks.push(boneTrack(h, "rotation", sampled(T2, N, (t) => h.transform.rotation + p.lean / 2 * s(t))));
3119
+ if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s(t)), smooth));
3120
+ 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));
3067
3121
  for (const leg of byRole(doc, "leg_l", "leg_r")) {
3068
3122
  const slot = slotOfBone(doc, leg), att = slot && doc.attachments.find((a) => a.id === slot.attachment && a.type === "region");
3069
3123
  const asset = att && att.type === "region" ? doc.assets.find((a) => a.id === att.asset) : void 0;
@@ -3073,8 +3127,8 @@ var weightShift = {
3073
3127
  length = Math.max(...[[0, -h / 2], [w2 / 2, 0], [0, h / 2], [-w2 / 2, 0]].map(([x, y]) => Math.hypot(...apply(m, x, y))));
3074
3128
  }
3075
3129
  if (!(length > p.shift)) continue;
3076
- tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s(t) / length) * 180 / Math.PI)));
3077
- if (p.dip) tracks.push(boneTrack(leg, "y", sampled(T2, N, (t) => leg.transform.y - p.dip * s(t) ** 2)));
3130
+ tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s(t) / length) * 180 / Math.PI), smooth));
3131
+ if (p.dip) tracks.push(boneTrack(leg, "y", sampled(T2, N, (t) => leg.transform.y - p.dip * s(t) ** 2), smooth));
3078
3132
  }
3079
3133
  return finish(this.name, p.id, T2, true, tracks, p);
3080
3134
  }
@@ -3317,13 +3371,13 @@ function motionQuality(doc, anim, opts = {}) {
3317
3371
  const before = delta(values(k.v), values(p.v)), after = delta(values(n.v), values(k.v));
3318
3372
  const pass = before.some((v, j2) => v * after[j2] > 1e-6);
3319
3373
  const speed = Math.max(magnitude(before) / (k.t - p.t), magnitude(after) / (n.t - k.t));
3320
- 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);
3374
+ 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);
3321
3375
  const jump = magnitude(delta(left, right));
3322
3376
  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);
3323
3377
  const acceleration = (a, b, c) => values(at(b)).map((v, j2) => (values(at(c))[j2] - 2 * v + values(at(a))[j2]) / (h * h));
3324
3378
  const al = acceleration(k.t - 2 * h, k.t - h, k.t), ar = acceleration(k.t, k.t + h, k.t + 2 * h);
3325
3379
  const aj = magnitude(delta(al, ar));
3326
- 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");
3380
+ 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");
3327
3381
  }
3328
3382
  }
3329
3383
  const steps = Math.max(2, Math.ceil(D * 60));
@@ -3805,6 +3859,17 @@ function checkRig(doc, opts = {}) {
3805
3859
  out.push({ level: "warn", code: "never_visible", subject: s.id, message: `slot "${s.id}" has no attachment and no animation ever shows one` });
3806
3860
  return out;
3807
3861
  }
3862
+ function summarizeIgnoring(findings, rules, max) {
3863
+ const hits = (r, f) => r.code === f.code && (r.subject ?? f.subject) === f.subject && (r.animation ?? f.animation) === f.animation;
3864
+ const kept = findings.filter((f) => !rules.some((r) => hits(r, f)));
3865
+ const idle = rules.filter((r) => !findings.some((f) => hits(r, f)));
3866
+ const notes = [
3867
+ ...kept.length < findings.length ? [` (${findings.length - kept.length} ignored)`] : [],
3868
+ ...idle.map((r) => ` ignore matched nothing: ${JSON.stringify(r)}`)
3869
+ ];
3870
+ return [summarize(kept, max), ...notes].join("\n");
3871
+ }
3872
+ var where = (f) => `${f.subject}${f.animation ? ` in ${f.animation}` : ""}${f.time !== void 0 ? ` @${+f.time.toFixed(4)}s` : ""}`;
3808
3873
  function summarize(findings, max = 12) {
3809
3874
  if (!findings.length) return "check: OK";
3810
3875
  const rank = { error: 0, warn: 1, info: 2 };
@@ -3819,7 +3884,7 @@ function summarize(findings, max = 12) {
3819
3884
  const lines = [];
3820
3885
  for (const f of sorted) {
3821
3886
  const shown = shownPerCode.get(f.code) ?? 0;
3822
- if (shown < PER_CODE) lines.push(` [${f.level}] ${f.code}: ${f.message}`);
3887
+ if (shown < PER_CODE) lines.push(` [${f.level}] ${f.code} ${where(f)}: ${f.message}`);
3823
3888
  else if (shown === PER_CODE) lines.push(` \u2026 ${codeTotal.get(f.code) - PER_CODE} more ${f.code}`);
3824
3889
  shownPerCode.set(f.code, shown + 1);
3825
3890
  }
@@ -4010,7 +4075,7 @@ function packRects(rects, opts = {}) {
4010
4075
  shelf = Math.max(shelf, r.height);
4011
4076
  }
4012
4077
  const height = y + shelf + pad;
4013
- if (height <= max) return { width, height, frames };
4078
+ if (height <= max) return { width: Math.max(pad, ...[...frames.values()].map(([fx, , fw]) => fx + fw)) + pad, height, frames };
4014
4079
  if (width >= max) throw new Error(`atlas does not fit in ${max}\xD7${max} (needs ${width}\xD7${height})`);
4015
4080
  width *= 2;
4016
4081
  }
@@ -5617,7 +5682,8 @@ function previewCheck(doc, animation) {
5617
5682
  const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
5618
5683
  return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
5619
5684
  }
5620
- 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() };
5685
+ var TrackId = { target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder", "control"]), id: z3.string(), prop: z3.string() };
5686
+ 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 };
5621
5687
  tool(
5622
5688
  "rig_set_control",
5623
5689
  "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.",
@@ -5810,11 +5876,21 @@ tool(
5810
5876
  );
5811
5877
  tool(
5812
5878
  "rig_check",
5813
- "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.",
5814
- { 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() },
5815
- async ({ path, ...opts }) => {
5879
+ "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.",
5880
+ {
5881
+ path: Path,
5882
+ animation: z3.string().optional().describe("limit to one animation (rest pose is always checked)"),
5883
+ fps: z3.number().positive().optional(),
5884
+ deformFps: z3.number().positive().optional(),
5885
+ secondaryBones: z3.array(z3.string()).optional(),
5886
+ frontOf: z3.array(z3.tuple([z3.string(), z3.string()])).optional(),
5887
+ landmarks: z3.record(z3.record(z3.tuple([z3.number(), z3.number()]))).optional(),
5888
+ landmarkTolerance: z3.number().nonnegative().optional(),
5889
+ ignore: z3.array(z3.object({ code: z3.string(), subject: z3.string().optional(), animation: z3.string().optional() }).strict()).optional()
5890
+ },
5891
+ async ({ path, ignore = [], ...opts }) => {
5816
5892
  try {
5817
- return text(summarize(checkRig(await load(path), opts), 100));
5893
+ return text(summarizeIgnoring(checkRig(await load(path), opts), ignore, 100));
5818
5894
  } catch (e) {
5819
5895
  return fail(e);
5820
5896
  }
@@ -6431,9 +6507,9 @@ tool(
6431
6507
  );
6432
6508
  tool(
6433
6509
  "rig_adjust_track",
6434
- "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.",
6510
+ "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.",
6435
6511
  { path: Path, animation: z3.string(), ...TrackRef, amplitude: z3.number().optional(), offset: z3.number().optional(), timeScale: z3.number().positive().optional() },
6436
- async ({ path, animation, target, id, prop, ...adj }) => mutate(path, (d) => adjustTrack(d, animation, { target, id, prop }, adj))
6512
+ async ({ path, animation, target, id, prop, interpolation, ...adj }) => mutate(path, (d) => adjustTrack(d, animation, { target, id, prop, interpolation }, adj))
6437
6513
  );
6438
6514
  tool(
6439
6515
  "rig_set_animation",
@@ -6451,7 +6527,7 @@ tool(
6451
6527
  tool(
6452
6528
  "rig_remove_keys",
6453
6529
  "Remove the keys of one track at the given times; an emptied track is dropped. Drops the preset link.",
6454
- { path: Path, animation: z3.string(), ...TrackRef, times: z3.array(z3.number()).min(1) },
6530
+ { path: Path, animation: z3.string(), ...TrackId, times: z3.array(z3.number()).min(1) },
6455
6531
  async ({ path, animation, target, id, prop, times: times2 }) => mutate(path, (d) => removeKeys(d, animation, { target, id, prop }, times2))
6456
6532
  );
6457
6533
  tool(