@energy8platform/golem 0.2.0 → 0.3.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/editor.js CHANGED
@@ -2177,8 +2177,8 @@ var init_Matrix = __esm({
2177
2177
  const pivot = transform.pivot;
2178
2178
  const skewX = -Math.atan2(-c5, d5);
2179
2179
  const skewY = Math.atan2(b4, a5);
2180
- const delta = Math.abs(skewX + skewY);
2181
- if (delta < 1e-5 || Math.abs(PI_2 - delta) < 1e-5) {
2180
+ const delta2 = Math.abs(skewX + skewY);
2181
+ if (delta2 < 1e-5 || Math.abs(PI_2 - delta2) < 1e-5) {
2182
2182
  transform.rotation = skewY;
2183
2183
  transform.skew.x = transform.skew.y = 0;
2184
2184
  } else {
@@ -8294,11 +8294,11 @@ var init_Ticker = __esm({
8294
8294
  }
8295
8295
  elapsedMS *= this.speed;
8296
8296
  if (this._minElapsedMS) {
8297
- const delta = currentTime - this._lastFrame | 0;
8298
- if (delta < this._minElapsedMS) {
8297
+ const delta2 = currentTime - this._lastFrame | 0;
8298
+ if (delta2 < this._minElapsedMS) {
8299
8299
  return;
8300
8300
  }
8301
- this._lastFrame = currentTime - delta % this._minElapsedMS;
8301
+ this._lastFrame = currentTime - delta2 % this._minElapsedMS;
8302
8302
  }
8303
8303
  this.deltaMS = elapsedMS;
8304
8304
  this.deltaTime = this.deltaMS * _Ticker2.targetFPMS;
@@ -35314,19 +35314,19 @@ function parse2(path2) {
35314
35314
  path2.replace(segment, (_4, cmd, args) => {
35315
35315
  let type = cmd.toLowerCase();
35316
35316
  let command = cmd;
35317
- const values = parseValues(args);
35318
- if (type === "m" && values.length > 2) {
35319
- data.push([command, ...values.splice(0, 2)]);
35317
+ const values2 = parseValues(args);
35318
+ if (type === "m" && values2.length > 2) {
35319
+ data.push([command, ...values2.splice(0, 2)]);
35320
35320
  type = "l";
35321
35321
  command = command === "m" ? "l" : "L";
35322
35322
  }
35323
35323
  while (true) {
35324
- if (values.length === length[type]) {
35325
- data.push([command, ...values]);
35324
+ if (values2.length === length[type]) {
35325
+ data.push([command, ...values2]);
35326
35326
  return "";
35327
35327
  }
35328
- if (values.length < length[type]) throw new Error("malformed path data");
35329
- data.push([command, ...values.splice(0, length[type])]);
35328
+ if (values2.length < length[type]) throw new Error("malformed path data");
35329
+ data.push([command, ...values2.splice(0, length[type])]);
35330
35330
  }
35331
35331
  });
35332
35332
  return data;
@@ -36596,10 +36596,10 @@ var init_ShapePath = __esm({
36596
36596
  regularPoly(x4, y4, radius, sides, rotation = 0, transform) {
36597
36597
  sides = Math.max(sides | 0, 3);
36598
36598
  const startAngle = -1 * Math.PI / 2 + rotation;
36599
- const delta = Math.PI * 2 / sides;
36599
+ const delta2 = Math.PI * 2 / sides;
36600
36600
  const polygon = [];
36601
36601
  for (let i6 = 0; i6 < sides; i6++) {
36602
- const angle = startAngle - i6 * delta;
36602
+ const angle = startAngle - i6 * delta2;
36603
36603
  polygon.push(
36604
36604
  x4 + radius * Math.cos(angle),
36605
36605
  y4 + radius * Math.sin(angle)
@@ -36628,10 +36628,10 @@ var init_ShapePath = __esm({
36628
36628
  const sideLength = radius * Math.sin(Math.PI / sides) - 1e-3;
36629
36629
  corner = Math.min(corner, sideLength);
36630
36630
  const startAngle = -1 * Math.PI / 2 + rotation;
36631
- const delta = Math.PI * 2 / sides;
36631
+ const delta2 = Math.PI * 2 / sides;
36632
36632
  const internalAngle = (sides - 2) * Math.PI / sides / 2;
36633
36633
  for (let i6 = 0; i6 < sides; i6++) {
36634
- const angle = i6 * delta + startAngle;
36634
+ const angle = i6 * delta2 + startAngle;
36635
36635
  const x0 = x4 + radius * Math.cos(angle);
36636
36636
  const y0 = y4 + radius * Math.sin(angle);
36637
36637
  const a1 = angle + Math.PI + internalAngle;
@@ -37114,11 +37114,11 @@ var init_GraphicsPath = __esm({
37114
37114
  innerRadius || (innerRadius = radius / 2);
37115
37115
  const startAngle = -1 * Math.PI / 2 + rotation;
37116
37116
  const len = points * 2;
37117
- const delta = Math.PI * 2 / len;
37117
+ const delta2 = Math.PI * 2 / len;
37118
37118
  const polygon = [];
37119
37119
  for (let i6 = 0; i6 < len; i6++) {
37120
37120
  const r5 = i6 % 2 ? innerRadius : radius;
37121
- const angle = i6 * delta + startAngle;
37121
+ const angle = i6 * delta2 + startAngle;
37122
37122
  polygon.push(
37123
37123
  x4 + r5 * Math.cos(angle),
37124
37124
  y4 + r5 * Math.sin(angle)
@@ -42707,10 +42707,10 @@ var init_TextStyle = __esm({
42707
42707
  * @param values - Partial style options to assign
42708
42708
  * @returns This TextStyle instance for chaining
42709
42709
  */
42710
- assign(values) {
42711
- for (const key in values) {
42710
+ assign(values2) {
42711
+ for (const key in values2) {
42712
42712
  const thisKey = key;
42713
- this[thisKey] = values[key];
42713
+ this[thisKey] = values2[key];
42714
42714
  }
42715
42715
  return this;
42716
42716
  }
@@ -48027,9 +48027,9 @@ ZodLiteral.create = (value, params2) => {
48027
48027
  ...processCreateParams(params2)
48028
48028
  });
48029
48029
  };
48030
- function createZodEnum(values, params2) {
48030
+ function createZodEnum(values2, params2) {
48031
48031
  return new ZodEnum({
48032
- values,
48032
+ values: values2,
48033
48033
  typeName: ZodFirstPartyTypeKind.ZodEnum,
48034
48034
  ...processCreateParams(params2)
48035
48035
  });
@@ -48085,14 +48085,14 @@ var ZodEnum = class _ZodEnum extends ZodType {
48085
48085
  }
48086
48086
  return enumValues;
48087
48087
  }
48088
- extract(values, newDef = this._def) {
48089
- return _ZodEnum.create(values, {
48088
+ extract(values2, newDef = this._def) {
48089
+ return _ZodEnum.create(values2, {
48090
48090
  ...this._def,
48091
48091
  ...newDef
48092
48092
  });
48093
48093
  }
48094
- exclude(values, newDef = this._def) {
48095
- return _ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), {
48094
+ exclude(values2, newDef = this._def) {
48095
+ return _ZodEnum.create(this.options.filter((opt) => !values2.includes(opt)), {
48096
48096
  ...this._def,
48097
48097
  ...newDef
48098
48098
  });
@@ -48130,9 +48130,9 @@ var ZodNativeEnum = class extends ZodType {
48130
48130
  return this._def.values;
48131
48131
  }
48132
48132
  };
48133
- ZodNativeEnum.create = (values, params2) => {
48133
+ ZodNativeEnum.create = (values2, params2) => {
48134
48134
  return new ZodNativeEnum({
48135
- values,
48135
+ values: values2,
48136
48136
  typeName: ZodFirstPartyTypeKind.ZodNativeEnum,
48137
48137
  ...processCreateParams(params2)
48138
48138
  });
@@ -48719,7 +48719,7 @@ function minJacobian(offsets, w5, h6, samples = 17) {
48719
48719
  var ID_RE = /^[a-z][a-z0-9_]*$/;
48720
48720
  var Id = external_exports.string().regex(ID_RE, "snake_case id");
48721
48721
  var Ease = external_exports.union([
48722
- external_exports.enum(["linear", "step", "ease_in", "ease_out", "ease_in_out", "back_out", "elastic_out"]),
48722
+ external_exports.enum(["linear", "step", "ease_in", "ease_out", "ease_in_out", "minimum_jerk", "back_out", "elastic_out"]),
48723
48723
  // cubic-bezier control points, same semantics as CSS
48724
48724
  external_exports.object({ bezier: external_exports.tuple([external_exports.number(), external_exports.number(), external_exports.number(), external_exports.number()]) })
48725
48725
  ]);
@@ -48751,6 +48751,7 @@ var Asset = external_exports.object({
48751
48751
  /** where this layer came from — provenance for agents */
48752
48752
  origin: external_exports.object({
48753
48753
  sourceImage: external_exports.string().optional(),
48754
+ bboxSpace: external_exports.enum(["source", "document"]).optional(),
48754
48755
  /** bbox in source image: [x, y, w, h] */
48755
48756
  bbox: external_exports.tuple([external_exports.number(), external_exports.number(), external_exports.number(), external_exports.number()]).optional(),
48756
48757
  inpainted: external_exports.boolean().default(false),
@@ -48773,6 +48774,8 @@ var Bone = external_exports.object({
48773
48774
  var Slot = external_exports.object({
48774
48775
  id: Id,
48775
48776
  bone: Id,
48777
+ /** Closed path attachment used as a moving clipping mask. */
48778
+ clip: Id.optional(),
48776
48779
  /** active attachment id; undefined = hidden */
48777
48780
  attachment: Id.optional(),
48778
48781
  alpha: external_exports.number().min(0).max(1).default(1),
@@ -48835,6 +48838,8 @@ var IkConstraint = external_exports.object({
48835
48838
  order: external_exports.number().int().default(0),
48836
48839
  bones: external_exports.array(Id).min(1),
48837
48840
  target: Id,
48841
+ iterations: external_exports.number().int().min(1).max(1e3).optional(),
48842
+ tolerance: external_exports.number().positive().optional(),
48838
48843
  mix: external_exports.number().default(1),
48839
48844
  softness: external_exports.number().default(0),
48840
48845
  bendPositive: external_exports.boolean().default(true),
@@ -48887,7 +48892,7 @@ var BoneProp = external_exports.enum(["x", "y", "rotation", "scaleX", "scaleY",
48887
48892
  var SlotProp = external_exports.enum(["alpha", "attachment", "z", "color"]);
48888
48893
  var DeformProp = external_exports.string().regex(/^(bulge|bend|sway|squash|p[0-2][0-2][xy]|vertices)$/, "deform prop must be bulge|bend|sway|squash|p<row><col>x|y|vertices");
48889
48894
  var IkProp = external_exports.enum(["mix", "softness", "bendPositive", "compress", "stretch"]);
48890
- var TransformProp = external_exports.enum(["mixRotate", "mixX", "mixY", "mixScaleX", "mixScaleY", "mixShearY"]);
48895
+ var TransformProp = external_exports.enum(["mixRotate", "mixX", "mixY", "mixScaleX", "mixScaleY", "mixShearY", "x", "y", "rotation", "scaleX", "scaleY", "shearY"]);
48891
48896
  var PathProp = external_exports.enum(["position", "spacing", "mixRotate", "mixX", "mixY"]);
48892
48897
  var KeyValue = external_exports.union([external_exports.number(), external_exports.string(), external_exports.null(), external_exports.array(external_exports.number()), external_exports.array(external_exports.string())]);
48893
48898
  var Key = external_exports.object({
@@ -48896,14 +48901,15 @@ var Key = external_exports.object({
48896
48901
  /** easing INTO the next key */
48897
48902
  ease: Ease.default("ease_in_out")
48898
48903
  });
48904
+ var Interpolation = { interpolation: external_exports.enum(["segment", "pchip"]).optional() };
48899
48905
  var Track = external_exports.discriminatedUnion("target", [
48900
- external_exports.object({ target: external_exports.literal("bone"), id: Id, prop: BoneProp, keys: external_exports.array(Key).min(1) }),
48901
- external_exports.object({ target: external_exports.literal("slot"), id: Id, prop: SlotProp, keys: external_exports.array(Key).min(1) }),
48902
- external_exports.object({ target: external_exports.literal("deform"), id: Id, prop: DeformProp, keys: external_exports.array(Key).min(1) }),
48903
- external_exports.object({ target: external_exports.literal("ik"), id: Id, prop: IkProp, keys: external_exports.array(Key).min(1) }),
48904
- external_exports.object({ target: external_exports.literal("transform"), id: Id, prop: TransformProp, keys: external_exports.array(Key).min(1) }),
48905
- external_exports.object({ target: external_exports.literal("path"), id: Id, prop: PathProp, keys: external_exports.array(Key).min(1) }),
48906
- external_exports.object({ target: external_exports.literal("drawOrder"), id: Id.default("order"), prop: external_exports.literal("order").default("order"), keys: external_exports.array(Key).min(1) })
48906
+ external_exports.object({ target: external_exports.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
48907
+ external_exports.object({ target: external_exports.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
48908
+ external_exports.object({ target: external_exports.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
48909
+ external_exports.object({ target: external_exports.literal("ik"), id: Id, prop: IkProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
48910
+ external_exports.object({ target: external_exports.literal("transform"), id: Id, prop: TransformProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
48911
+ external_exports.object({ target: external_exports.literal("path"), id: Id, prop: PathProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
48912
+ external_exports.object({ target: external_exports.literal("drawOrder"), id: Id.default("order"), prop: external_exports.literal("order").default("order"), ...Interpolation, keys: external_exports.array(Key).min(1) })
48907
48913
  ]);
48908
48914
  var Event2 = external_exports.object({
48909
48915
  t: external_exports.number().nonnegative(),
@@ -49045,6 +49051,10 @@ function validate(doc2) {
49045
49051
  if (a5.frame && !a5.trim && !a5.rotate && (a5.frame[2] !== a5.width || a5.frame[3] !== a5.height)) errs.push(`asset "${a5.id}": frame size must equal width\xD7height (or declare trim/rotate)`);
49046
49052
  }
49047
49053
  for (const s6 of doc2.slots) {
49054
+ if (s6.clip) {
49055
+ const mask = attachments.get(s6.clip);
49056
+ if (mask?.type !== "path" || !mask.closed) errs.push(`slot "${s6.id}": clip must name a closed path attachment`);
49057
+ }
49048
49058
  if (!bones.has(s6.bone)) errs.push(`slot "${s6.id}": unknown bone "${s6.bone}"`);
49049
49059
  if (s6.attachment) {
49050
49060
  const a5 = attachments.get(s6.attachment);
@@ -49076,7 +49086,8 @@ function validate(doc2) {
49076
49086
  }
49077
49087
  }
49078
49088
  for (const c5 of doc2.ik) {
49079
- if (c5.bones.length > 2) errs.push(`ik "${c5.id}": must have 1 or 2 bones`);
49089
+ for (let i6 = 1; i6 < c5.bones.length; i6++) if (doc2.bones.find((b4) => b4.id === c5.bones[i6])?.parent !== c5.bones[i6 - 1])
49090
+ errs.push(`ik "${c5.id}": bones must form a contiguous parent-to-child chain`);
49080
49091
  for (const b4 of c5.bones) if (!bones.has(b4)) errs.push(`ik "${c5.id}": unknown bone "${b4}"`);
49081
49092
  if (!bones.has(c5.target)) errs.push(`ik "${c5.id}": unknown target bone "${c5.target}"`);
49082
49093
  }
@@ -49148,6 +49159,7 @@ var named = {
49148
49159
  ease_in: (t5) => t5 * t5,
49149
49160
  ease_out: (t5) => 1 - (1 - t5) * (1 - t5),
49150
49161
  ease_in_out: (t5) => t5 < 0.5 ? 2 * t5 * t5 : 1 - (-2 * t5 + 2) ** 2 / 2,
49162
+ minimum_jerk: (t5) => t5 * t5 * t5 * (10 + t5 * (-15 + 6 * t5)),
49151
49163
  back_out: (t5) => 1 + 2.70158 * (t5 - 1) ** 3 + 1.70158 * (t5 - 1) ** 2,
49152
49164
  elastic_out: (t5) => t5 === 0 ? 0 : t5 === 1 ? 1 : 2 ** (-10 * t5) * Math.sin((t5 * 10 - 0.75) * (2 * Math.PI / 3)) + 1
49153
49165
  };
@@ -49179,7 +49191,25 @@ function ease(spec) {
49179
49191
  return f6;
49180
49192
  }
49181
49193
  var pk = (target, id, prop) => `${target}:${id}:${prop}`;
49182
- function sample(track, t5) {
49194
+ function smoothValue(track, i6, t5, component, loop2) {
49195
+ const k4 = track.keys, n4 = k4.length;
49196
+ const v6 = (j5) => component === void 0 ? k4[j5].v : k4[j5].v[component];
49197
+ const h6 = (j5) => k4[j5 + 1].t - k4[j5].t;
49198
+ const d5 = (j5) => (v6(j5 + 1) - v6(j5)) / h6(j5);
49199
+ const harmonic = (a5, b4, h0, h1) => a5 * b4 <= 0 ? 0 : 3 * (h0 + h1) / ((2 * h1 + h0) / a5 + (h1 + 2 * h0) / b4);
49200
+ const tangent = (j5) => {
49201
+ if (j5 === 0 || j5 === n4 - 1) {
49202
+ if (loop2 && n4 > 2 && v6(0) === v6(n4 - 1) && k4[0].ease !== "step" && k4[n4 - 2].ease !== "step")
49203
+ return harmonic(d5(n4 - 2), d5(0), h6(n4 - 2), h6(0));
49204
+ return 0;
49205
+ }
49206
+ if (k4[j5 - 1].ease === "step" || k4[j5].ease === "step") return 0;
49207
+ return harmonic(d5(j5 - 1), d5(j5), h6(j5 - 1), h6(j5));
49208
+ };
49209
+ const u6 = (t5 - k4[i6].t) / h6(i6), u22 = u6 * u6, u32 = u22 * u6;
49210
+ return (2 * u32 - 3 * u22 + 1) * v6(i6) + (u32 - 2 * u22 + u6) * h6(i6) * tangent(i6) + (-2 * u32 + 3 * u22) * v6(i6 + 1) + (u32 - u22) * h6(i6) * tangent(i6 + 1);
49211
+ }
49212
+ function sample(track, t5, loop2 = false) {
49183
49213
  const keys = track.keys;
49184
49214
  if (t5 <= keys[0].t) return keys[0].v;
49185
49215
  const last = keys[keys.length - 1];
@@ -49187,31 +49217,38 @@ function sample(track, t5) {
49187
49217
  let i6 = 0;
49188
49218
  while (keys[i6 + 1].t <= t5) i6++;
49189
49219
  const a5 = keys[i6], b4 = keys[i6 + 1];
49220
+ if (a5.ease === "step") return a5.v;
49190
49221
  if (typeof a5.v === "number" && typeof b4.v === "number") {
49222
+ if (track.interpolation === "pchip") return smoothValue(track, i6, t5, void 0, loop2);
49191
49223
  const u6 = ease(a5.ease)((t5 - a5.t) / (b4.t - a5.t));
49192
49224
  return a5.v + (b4.v - a5.v) * u6;
49193
49225
  }
49194
49226
  if (Array.isArray(a5.v) && Array.isArray(b4.v) && typeof a5.v[0] === "number") {
49227
+ if (track.interpolation === "pchip") return a5.v.map((_4, j5) => smoothValue(track, i6, t5, j5, loop2));
49195
49228
  const u6 = ease(a5.ease)((t5 - a5.t) / (b4.t - a5.t)), av = a5.v, bv = b4.v;
49196
49229
  return av.map((v6, k4) => v6 + ((bv[k4] ?? v6) - v6) * u6);
49197
49230
  }
49198
49231
  return a5.v;
49199
49232
  }
49200
49233
  function poseOf(anim, t5, out2 = /* @__PURE__ */ new Map()) {
49201
- for (const tr of anim.tracks) out2.set(pk(tr.target, tr.id, tr.prop), sample(tr, t5));
49234
+ for (const tr of anim.tracks) out2.set(pk(tr.target, tr.id, tr.prop), sample(tr, t5, anim.loop && tr.keys[0].t === 0 && tr.keys.at(-1).t === anim.duration));
49202
49235
  return out2;
49203
49236
  }
49204
49237
  function blendPoses(from, to, w5, restOf) {
49205
- const out2 = new Map(to);
49206
- for (const [k4, f6] of from) {
49207
- const cur = to.get(k4);
49208
- if (typeof f6 === "number") {
49238
+ if (w5 >= 1) return new Map(to);
49239
+ const out2 = /* @__PURE__ */ new Map();
49240
+ for (const k4 of /* @__PURE__ */ new Set([...from.keys(), ...to.keys()])) {
49241
+ const f6 = from.has(k4) ? from.get(k4) : restOf(k4);
49242
+ const cur = to.has(k4) ? to.get(k4) : restOf(k4);
49243
+ if (typeof f6 === "number" && !Array.isArray(cur)) {
49209
49244
  const target = typeof cur === "number" ? cur : restOf(k4);
49210
49245
  out2.set(k4, f6 + (target - f6) * w5);
49211
49246
  } else if (Array.isArray(f6) && typeof f6[0] === "number") {
49212
49247
  const fv = f6, tv = Array.isArray(cur) && typeof cur[0] === "number" ? cur : fv.map(() => 0);
49213
49248
  out2.set(k4, fv.map((v6, i6) => v6 + ((tv[i6] ?? 0) - v6) * w5));
49214
- } else if (cur === void 0 && w5 < 1) out2.set(k4, f6);
49249
+ } else if (Array.isArray(cur) && typeof cur[0] === "number" && typeof f6 !== "string") {
49250
+ out2.set(k4, cur.map((v6) => v6 * w5));
49251
+ } else out2.set(k4, f6 ?? null);
49215
49252
  }
49216
49253
  return out2;
49217
49254
  }
@@ -49380,10 +49417,43 @@ function aimAt(skel, boneId, tx, ty, mix, compress = false, stretch = false, uni
49380
49417
  updateSubtree(skel, boneId);
49381
49418
  }
49382
49419
  var shortest = (deg) => ((deg + 180) % 360 + 360) % 360 - 180;
49420
+ function applyLongIk(skel, c5, tx, ty, mix) {
49421
+ const initial = c5.bones.map((id) => ({ ...skel.bones.get(id) }));
49422
+ const last = c5.bones.at(-1), len = boneOf(skel.doc, last).length;
49423
+ const end = () => apply(skel.bones.get(last).world, len, 0);
49424
+ const tolerance = c5.tolerance ?? 0.05;
49425
+ const root2 = skel.bones.get(c5.bones[0]).world, tip = end();
49426
+ if (Math.abs((tip[0] - root2.tx) * (ty - root2.ty) - (tip[1] - root2.ty) * (tx - root2.tx)) < 1e-7 && Math.hypot(tx - root2.tx, ty - root2.ty) < Math.hypot(tip[0] - root2.tx, tip[1] - root2.ty) - tolerance) {
49427
+ skel.bones.get(last).rotation += cnum(skel, "ik", c5.id, "bendPositive", c5.bendPositive) > 0.5 ? 5 : -5;
49428
+ updateSubtree(skel, last);
49429
+ }
49430
+ for (let iter = 0; iter < (c5.iterations ?? 100); iter++) {
49431
+ const e5 = end();
49432
+ if (Math.hypot(e5[0] - tx, e5[1] - ty) <= tolerance) break;
49433
+ for (let i6 = c5.bones.length - 1; i6 >= 0; i6--) {
49434
+ const id = c5.bones[i6], b4 = boneOf(skel.doc, id), st = skel.bones.get(id), w5 = st.world, e6 = end();
49435
+ const angle = wrapRad(Math.atan2(ty - w5.ty, tx - w5.tx) - Math.atan2(e6[1] - w5.ty, e6[0] - w5.tx));
49436
+ const cos = Math.cos(angle), sin = Math.sin(angle);
49437
+ setEffectiveWorld(skel, b4, st, { ...w5, a: cos * w5.a - sin * w5.b, b: sin * w5.a + cos * w5.b, c: cos * w5.c - sin * w5.d, d: sin * w5.c + cos * w5.d });
49438
+ }
49439
+ }
49440
+ if (mix < 1) {
49441
+ c5.bones.forEach((id, i6) => {
49442
+ const st = skel.bones.get(id), from = initial[i6];
49443
+ for (const p6 of ["rotation", "shearX", "shearY"]) st[p6] = from[p6] + shortest(st[p6] - from[p6]) * mix;
49444
+ for (const p6 of ["x", "y", "scaleX", "scaleY"]) st[p6] = from[p6] + (st[p6] - from[p6]) * mix;
49445
+ });
49446
+ updateSubtree(skel, c5.bones[0]);
49447
+ }
49448
+ }
49383
49449
  function applyIk(skel, c5) {
49384
49450
  const mix = cnum(skel, "ik", c5.id, "mix", c5.mix);
49385
49451
  if (mix <= 0) return;
49386
49452
  const target = skel.bones.get(c5.target).world;
49453
+ if (c5.bones.length > 2) {
49454
+ applyLongIk(skel, c5, target.tx, target.ty, mix);
49455
+ return;
49456
+ }
49387
49457
  const bendDir = cnum(skel, "ik", c5.id, "bendPositive", c5.bendPositive) > 0.5 ? 1 : -1;
49388
49458
  const compress = cnum(skel, "ik", c5.id, "compress", c5.compress) > 0.5, stretch = cnum(skel, "ik", c5.id, "stretch", c5.stretch) > 0.5;
49389
49459
  let softness = cnum(skel, "ik", c5.id, "softness", c5.softness);
@@ -49518,6 +49588,7 @@ function applyTransformConstraint(skel, c5) {
49518
49588
  const mixRotate = cnum(skel, "transform", c5.id, "mixRotate", c5.mixRotate), mixX = cnum(skel, "transform", c5.id, "mixX", c5.mixX), mixY = cnum(skel, "transform", c5.id, "mixY", c5.mixY);
49519
49589
  const mixScaleX = cnum(skel, "transform", c5.id, "mixScaleX", c5.mixScaleX), mixScaleY = cnum(skel, "transform", c5.id, "mixScaleY", c5.mixScaleY), mixShearY = cnum(skel, "transform", c5.id, "mixShearY", c5.mixShearY);
49520
49590
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
49591
+ c5 = { ...c5, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p6) => [p6, cnum(skel, "transform", c5.id, p6, c5[p6])])) };
49521
49592
  const target = skel.bones.get(c5.target), tw = target.world;
49522
49593
  const offsetRotation = c5.rotation * DEG2, offsetShearY = c5.shearY * DEG2;
49523
49594
  for (const id of c5.bones) {
@@ -49955,6 +50026,116 @@ function paintWeights(skel, att, world, opts) {
49955
50026
  return { ...rest, weights };
49956
50027
  }
49957
50028
 
50029
+ // src/rig-quality.ts
50030
+ var values = (v6) => typeof v6 === "number" ? [v6] : Array.isArray(v6) && v6.every((n4) => typeof n4 === "number") ? v6 : void 0;
50031
+ var delta = (a5, b4) => a5.map((v6, i6) => v6 - b4[i6]);
50032
+ var magnitude = (a5) => Math.max(0, ...a5.map(Math.abs));
50033
+ function motionQuality(doc2, anim, opts = {}) {
50034
+ const out2 = [], D2 = anim.duration;
50035
+ const report = (code, subject, message, time2, level = "warn") => out2.push({ level, code, subject, animation: anim.id, time: time2, message });
50036
+ for (const tr of anim.tracks) {
50037
+ const id = `${tr.target}.${tr.id}.${tr.prop}`, looping = anim.loop && tr.keys[0].t === 0 && tr.keys.at(-1).t === D2;
50038
+ const at = (t5) => sample(tr, t5, looping), numeric = tr.keys.every((k4) => values(k4.v) !== void 0);
50039
+ if (anim.loop) {
50040
+ const first = at(0), last = at(D2), av = values(first), bv = values(last);
50041
+ const diff = av && bv && av.length === bv.length ? magnitude(delta(av, bv)) : first === last || JSON.stringify(first) === JSON.stringify(last) ? 0 : Infinity;
50042
+ if (diff > 1e-3 && typeof first !== "number") report("loop_seam", id, `loop endpoint values differ (${Number.isFinite(diff) ? `max delta ${diff.toFixed(4)}` : "discrete state"}); all channels must close`);
50043
+ if (numeric && diff <= 1e-3 && tr.keys.length > 1) {
50044
+ const h6 = Math.min(D2 / 1e4, (tr.keys[1].t - tr.keys[0].t) / 100, (tr.keys.at(-1).t - tr.keys.at(-2).t) / 100);
50045
+ const va = delta(values(at(h6)), values(at(0))).map((v6) => v6 / h6), vb = delta(values(at(D2)), values(at(D2 - h6))).map((v6) => v6 / h6);
50046
+ const dv = magnitude(delta(va, vb));
50047
+ if (dv > Math.max(0.05, 0.05 * Math.max(magnitude(va), magnitude(vb)))) report("loop_velocity", id, `loop velocity changes by ${dv.toFixed(3)} units/s at the wrap`);
50048
+ }
50049
+ }
50050
+ if (!numeric) continue;
50051
+ for (let i6 = 1; i6 < tr.keys.length - 1; i6++) {
50052
+ const p6 = tr.keys[i6 - 1], k4 = tr.keys[i6], n4 = tr.keys[i6 + 1];
50053
+ if (p6.ease === "step" || k4.ease === "step") continue;
50054
+ const h6 = Math.min(k4.t - p6.t, n4.t - k4.t) / 1e3;
50055
+ const left = delta(values(at(k4.t)), values(at(k4.t - h6))).map((v6) => v6 / h6), right = delta(values(at(k4.t + h6)), values(at(k4.t))).map((v6) => v6 / h6);
50056
+ const before = delta(values(k4.v), values(p6.v)), after = delta(values(n4.v), values(k4.v));
50057
+ const pass = before.some((v6, j5) => v6 * after[j5] > 1e-6);
50058
+ const speed = Math.max(magnitude(before) / (k4.t - p6.t), magnitude(after) / (n4.t - k4.t));
50059
+ 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`, k4.t);
50060
+ const jump = magnitude(delta(left, right));
50061
+ if (jump > Math.max(0.1, speed * 0.1)) report("velocity_jump", id, `velocity jumps ${jump.toFixed(3)} units/s across this key`, k4.t);
50062
+ const acceleration = (a5, b4, c5) => values(at(b4)).map((v6, j5) => (values(at(c5))[j5] - 2 * v6 + values(at(a5))[j5]) / (h6 * h6));
50063
+ const al = acceleration(k4.t - 2 * h6, k4.t - h6, k4.t), ar = acceleration(k4.t, k4.t + h6, k4.t + 2 * h6);
50064
+ const aj = magnitude(delta(al, ar));
50065
+ if (aj > Math.max(1, speed / Math.min(k4.t - p6.t, n4.t - k4.t) * 2)) report("acceleration_jump", id, `acceleration changes by ${aj.toFixed(2)} units/s\xB2 at this key; PCHIP guarantees velocity, not acceleration continuity`, k4.t, "info");
50066
+ }
50067
+ }
50068
+ const steps = Math.max(2, Math.ceil(D2 * 60));
50069
+ const times3 = new Set(Array.from({ length: steps + 1 }, (_4, i6) => i6 * D2 / steps));
50070
+ for (const tr of anim.tracks) for (const k4 of tr.keys) {
50071
+ times3.add(k4.t);
50072
+ if (k4.t > 0) times3.add(Math.max(0, k4.t - 1e-5));
50073
+ }
50074
+ const samples = [...times3].sort((a5, b4) => a5 - b4).map((t5) => ({ t: t5, pose: poseOf(anim, t5) }));
50075
+ for (const bone of opts.secondaryBones ?? []) {
50076
+ if (!doc2.bones.some((b4) => b4.id === bone)) throw new Error(`unknown secondary bone "${bone}"`);
50077
+ const local = samples.map((s6) => evaluate(doc2, s6.pose).bones.get(bone));
50078
+ const curves = ["rotation", "x", "y"].map((prop) => local.map((b4) => b4[prop]));
50079
+ if (curves.every((v6) => Math.max(...v6) - Math.min(...v6) < 1e-3)) report("secondary_frozen", bone, `declared secondary bone has no local motion in this animation`);
50080
+ }
50081
+ const reported = /* @__PURE__ */ new Set();
50082
+ for (const [front, back] of opts.frontOf ?? []) {
50083
+ if (![front, back].every((id) => doc2.slots.some((s6) => s6.id === id))) throw new Error(`unknown frontOf slots: ${front}, ${back}`);
50084
+ for (const { t: t5, pose: pose2 } of samples) {
50085
+ const skel = evaluate(doc2, pose2);
50086
+ if (!skel.attachment.get(front) || !skel.attachment.get(back)) continue;
50087
+ const visible = (id) => {
50088
+ const slot = doc2.slots.find((s6) => s6.id === id);
50089
+ const color = pose2.get(pk("slot", id, "color")) ?? colorToArray(slot.color);
50090
+ return Number(pose2.get(pk("slot", id, "alpha")) ?? slot.alpha) * color[3] > 0;
50091
+ };
50092
+ if (!visible(front) || !visible(back)) continue;
50093
+ const order = pose2.get(pk("drawOrder", "order", "order"));
50094
+ const z3 = (id) => order ? order.indexOf(id) : Number(pose2.get(pk("slot", id, "z")) ?? doc2.slots.find((s6) => s6.id === id).z);
50095
+ const pair = JSON.stringify([front, back]);
50096
+ if (z3(front) <= z3(back) && !reported.has(pair)) {
50097
+ reported.add(pair);
50098
+ report("draw_order_rule", front, `"${front}" must stay explicitly in front of "${back}" according to the brief (equal z is ambiguous)`, t5);
50099
+ }
50100
+ }
50101
+ }
50102
+ for (const tr of anim.tracks.filter((t5) => t5.target === "slot" && t5.prop === "attachment")) for (const key of tr.keys.filter((k4) => k4.t > 0)) {
50103
+ const a5 = evaluate(doc2, poseOf(anim, Math.max(0, key.t - 1e-6))), b4 = evaluate(doc2, poseOf(anim, key.t));
50104
+ const before = a5.attachment.get(tr.id), after = b4.attachment.get(tr.id);
50105
+ if (!before || !after || before.id === after.id) continue;
50106
+ const landmarksA = opts.landmarks?.[before.id], landmarksB = opts.landmarks?.[after.id];
50107
+ if (!landmarksA && !landmarksB) continue;
50108
+ if (!landmarksA || !landmarksB || !Object.keys(landmarksA).some((name) => landmarksB[name])) {
50109
+ report("landmark_unmeasured", tr.id, `swap ${before.id} \u2192 ${after.id} has no shared declared landmarks`, key.t);
50110
+ continue;
50111
+ }
50112
+ const point = (skel, att, uv) => {
50113
+ if (att.type === "region") {
50114
+ const [w5, h6] = regionSize(doc2, att);
50115
+ return apply(regionMatrix(skel, att.slot, att), (uv[0] - 0.5) * w5, (uv[1] - 0.5) * h6);
50116
+ }
50117
+ if (att.type !== "mesh") return;
50118
+ const v6 = attachmentWorldVertices(skel, att.slot, att);
50119
+ for (let i6 = 0; i6 < att.triangles.length; i6 += 3) {
50120
+ const [ia, ib, ic] = att.triangles.slice(i6, i6 + 3), ux2 = att.uvs[ia * 2], uy2 = att.uvs[ia * 2 + 1], bx = att.uvs[ib * 2] - ux2, by = att.uvs[ib * 2 + 1] - uy2, cx = att.uvs[ic * 2] - ux2, cy = att.uvs[ic * 2 + 1] - uy2, det = bx * cy - by * cx;
50121
+ if (Math.abs(det) < 1e-12) continue;
50122
+ const s6 = ((uv[0] - ux2) * cy - (uv[1] - uy2) * cx) / det, r5 = (bx * (uv[1] - uy2) - by * (uv[0] - ux2)) / det;
50123
+ if (s6 >= -1e-6 && r5 >= -1e-6 && s6 + r5 <= 1 + 1e-6) return [v6[ia * 2] * (1 - s6 - r5) + v6[ib * 2] * s6 + v6[ic * 2] * r5, v6[ia * 2 + 1] * (1 - s6 - r5) + v6[ib * 2 + 1] * s6 + v6[ic * 2 + 1] * r5];
50124
+ }
50125
+ };
50126
+ for (const [name, uv] of Object.entries(landmarksA)) if (landmarksB[name]) {
50127
+ const p6 = point(a5, before, uv), q2 = point(b4, after, landmarksB[name]);
50128
+ if (!p6 || !q2) {
50129
+ report("landmark_unmeasured", tr.id, `landmark "${name}" is outside attachment geometry`, key.t);
50130
+ continue;
50131
+ }
50132
+ const distance = Math.hypot(p6[0] - q2[0], p6[1] - q2[1]);
50133
+ if (distance > (opts.landmarkTolerance ?? 2)) report("attachment_landmark_jump", tr.id, `"${name}" moves ${distance.toFixed(2)}px when ${before.id} changes to ${after.id}`, key.t);
50134
+ }
50135
+ }
50136
+ return out2;
50137
+ }
50138
+
49958
50139
  // src/rig-path.ts
49959
50140
  var flatten = (pts) => pts.flatMap((p6) => [p6[0], p6[1]]);
49960
50141
  var anchorCount = (vertexCount2) => Math.floor(vertexCount2 / 3);
@@ -50119,6 +50300,43 @@ function pathFromPoints(skel, slotId, points, closed) {
50119
50300
  }
50120
50301
  return { vertices, closed, lengths: curveLengths(flatten(ctrl.flat()), closed) };
50121
50302
  }
50303
+ function constraintDependencies(doc2) {
50304
+ const parent = new Map(doc2.bones.map((b4) => [b4.id, b4.parent]));
50305
+ const ancestors = (id) => {
50306
+ const out2 = /* @__PURE__ */ new Set();
50307
+ for (let b4 = id; b4 && !out2.has(b4); b4 = parent.get(b4)) out2.add(b4);
50308
+ return out2;
50309
+ };
50310
+ const all = [
50311
+ ...doc2.ik.map((c5) => ({ kind: "ik", ...c5 })),
50312
+ ...doc2.transform.map((c5) => ({ kind: "transform", ...c5 })),
50313
+ ...doc2.path.map((c5) => ({ kind: "path", ...c5 }))
50314
+ ].sort((a5, b4) => a5.order - b4.order);
50315
+ const active = (c5) => MIX_PROPS[c5.kind].some((p6) => c5[p6] !== 0 || doc2.animations.some((a5) => a5.tracks.some((t5) => t5.target === c5.kind && t5.id === c5.id && t5.prop === p6 && t5.keys.some((k4) => typeof k4.v === "number" && k4.v !== 0))));
50316
+ const edges = [];
50317
+ all.forEach((reader, ri) => {
50318
+ if (!active(reader)) return;
50319
+ const reads = /* @__PURE__ */ new Set();
50320
+ const add = (b4) => ancestors(b4).forEach((x4) => reads.add(x4));
50321
+ if (reader.kind === "path") {
50322
+ const slot = doc2.slots.find((s6) => s6.id === reader.target);
50323
+ if (slot) add(slot.bone);
50324
+ for (const a5 of doc2.attachments) if (a5.slot === reader.target && a5.type === "path") {
50325
+ for (const list of a5.weights ?? []) for (const inf of list) if (inf.w !== 0) add(inf.bone);
50326
+ }
50327
+ } else add(reader.target);
50328
+ for (const b4 of reader.bones) {
50329
+ const p6 = parent.get(b4);
50330
+ if (p6 && !reader.bones.includes(p6)) add(p6);
50331
+ }
50332
+ all.forEach((writer, wi) => {
50333
+ if (ri === wi || !active(writer)) return;
50334
+ const bone = writer.bones.find((b4) => reads.has(b4));
50335
+ if (bone) edges.push({ reader: reader.id, readerKind: reader.kind, writer: writer.id, writerKind: writer.kind, bone, late: wi > ri });
50336
+ });
50337
+ });
50338
+ return { all, edges };
50339
+ }
50122
50340
 
50123
50341
  // src/rig-check.ts
50124
50342
  var FOLD_ERROR_RATIO = 0.05;
@@ -50128,15 +50346,22 @@ var isNumeric = (t5) => t5.target !== "slot" || t5.prop !== "attachment";
50128
50346
  var trackId2 = (t5) => `${t5.target}.${t5.id}.${t5.prop}`;
50129
50347
  var fmt = (n4) => String(Math.round(n4 * 1e3) / 1e3);
50130
50348
  var times = (a5, fps) => {
50131
- const n4 = Math.max(1, Math.round(a5.duration * fps)), out2 = [];
50132
- for (let i6 = 0; i6 <= n4; i6++) out2.push(Math.min(a5.duration, i6 / fps));
50349
+ const n4 = Math.max(1, Math.ceil(a5.duration * fps)), out2 = [];
50350
+ for (let i6 = 0; i6 <= n4; i6++) out2.push(a5.duration * i6 / n4);
50133
50351
  return out2;
50134
50352
  };
50135
50353
  function checkRig(doc2, opts = {}) {
50136
50354
  const fps = opts.fps ?? 30, dfps = opts.deformFps ?? 60, overlapMin = opts.overlap ?? 0.6, margin = opts.margin ?? 1;
50355
+ if (![fps, dfps].every((v6) => Number.isFinite(v6) && v6 > 0)) throw new Error("check sampling rates must be positive");
50137
50356
  const anims = opts.animation === void 0 ? doc2.animations : doc2.animations.filter((a5) => a5.id === opts.animation);
50138
50357
  if (opts.animation !== void 0 && !anims.length) throw new Error(`unknown animation "${opts.animation}"`);
50139
50358
  const out2 = [];
50359
+ for (const e5 of constraintDependencies(doc2).edges.filter((e6) => e6.late)) out2.push({
50360
+ level: "warn",
50361
+ code: "constraint_order",
50362
+ subject: e5.reader,
50363
+ message: `constraint "${e5.reader}" reads bone "${e5.bone}" before later constraint "${e5.writer}" moves it; use rig_order_constraints or set explicit order`
50364
+ });
50140
50365
  const W = doc2.meta.width, H3 = doc2.meta.height;
50141
50366
  const overflowOf = (pose2, slotId, skel) => {
50142
50367
  const pts = slotOutline(doc2, pose2, slotId, 4, skel);
@@ -50308,6 +50533,7 @@ function checkRig(doc2, opts = {}) {
50308
50533
  return rest;
50309
50534
  };
50310
50535
  for (const a5 of anims) {
50536
+ out2.push(...motionQuality(doc2, a5, opts));
50311
50537
  for (const t5 of a5.tracks) {
50312
50538
  if (!isNumeric(t5)) continue;
50313
50539
  const vals = t5.keys.map((k4) => k4.v);
@@ -50415,7 +50641,9 @@ function checkRig(doc2, opts = {}) {
50415
50641
  if (!sorted.length) continue;
50416
50642
  const typical = sorted[Math.floor(sorted.length / 2)];
50417
50643
  const worst2 = ds.reduce((m5, x4) => x4.d > m5.d ? x4 : m5, ds[0]);
50418
- if (worst2.d > Math.max(6, 5 * typical)) {
50644
+ const index = ds.indexOf(worst2);
50645
+ const neighbours = Math.max(ds[index - 1]?.d ?? 0, ds[index + 1]?.d ?? 0);
50646
+ if (worst2.d > Math.max(6, 5 * typical, 3 * neighbours)) {
50419
50647
  const stepped = stepTimes.some((st) => st > worst2.t - 1 / dfps - 1e-9 && st <= worst2.t + 1e-9);
50420
50648
  out2.push({
50421
50649
  level: stepped ? "info" : "warn",
@@ -50466,7 +50694,7 @@ function checkRig(doc2, opts = {}) {
50466
50694
  for (const att of doc2.attachments) {
50467
50695
  if (att.type !== "region") continue;
50468
50696
  const asset = doc2.assets.find((a5) => a5.id === att.asset), bbox = asset?.origin?.bbox;
50469
- if (!asset || !bbox || !doc2.slots.some((s6) => s6.id === att.slot)) continue;
50697
+ if (!asset || !bbox || asset.origin?.bboxSpace === "source" || !doc2.slots.some((s6) => s6.id === att.slot)) continue;
50470
50698
  const [cx, cy] = apply(regionMatrix(restWorld, att.slot, att), 0, 0);
50471
50699
  const ex2 = bbox[0] + bbox[2] / 2, ey2 = bbox[1] + bbox[3] / 2, dx = cx - ex2, dy = cy - ey2;
50472
50700
  const dist = Math.hypot(dx, dy);
@@ -50511,7 +50739,8 @@ function checkRig(doc2, opts = {}) {
50511
50739
  for (const att of doc2.attachments) if (att.type !== "path" && shownAttachments.has(att.id)) used.add(att.asset);
50512
50740
  for (const asset of doc2.assets) if (!used.has(asset.id))
50513
50741
  out2.push({ level: "info", code: "unused_asset", subject: asset.id, message: `asset "${asset.id}" is not shown by any attachment at rest or in any animation` });
50514
- for (const s6 of doc2.slots) if (!s6.attachment && !swappedIn.has(s6.id))
50742
+ const maskSlots = new Set(doc2.slots.flatMap((s6) => s6.clip ? [doc2.attachments.find((a5) => a5.id === s6.clip)?.slot] : []));
50743
+ for (const s6 of doc2.slots) if (!s6.attachment && !swappedIn.has(s6.id) && !maskSlots.has(s6.id))
50515
50744
  out2.push({ level: "warn", code: "never_visible", subject: s6.id, message: `slot "${s6.id}" has no attachment and no animation ever shows one` });
50516
50745
  return out2;
50517
50746
  }
@@ -50913,10 +51142,11 @@ function setPathAttachment(doc2, id, slot, geom) {
50913
51142
  function setKeys(doc2, animId2, track, keys, duration, opts = {}) {
50914
51143
  const anim = doc2.animations.find((a5) => a5.id === animId2) ?? { id: animId2, duration: duration ?? 1, loop: false, tracks: [], events: [] };
50915
51144
  const same2 = (t5) => t5.target === track.target && t5.id === track.id && t5.prop === track.prop;
51145
+ const old = anim.tracks.find(same2);
50916
51146
  const given = keys.map((k4) => ({ ...k4, ease: k4.ease ?? "ease_in_out" }));
50917
51147
  const kept = opts.merge ? (anim.tracks.find(same2)?.keys ?? []).filter((k4) => !given.some((g4) => Math.abs(g4.t - k4.t) < 1e-9)) : [];
50918
51148
  const sorted = [...kept, ...given].sort((a5, b4) => a5.t - b4.t);
50919
- const edited = { ...track, keys: sorted };
51149
+ const edited = { ...old, ...track, interpolation: track.interpolation ?? old?.interpolation, keys: sorted };
50920
51150
  const tracks = anim.tracks.some(same2) ? anim.tracks.map((t5) => same2(t5) ? edited : t5) : [...anim.tracks, edited];
50921
51151
  const dur = duration ?? Math.max(anim.duration, ...sorted.map((k4) => k4.t));
50922
51152
  const next = { ...anim, tracks, duration: dur, preset: void 0 };
@@ -51796,7 +52026,7 @@ function KeyDiamond({ track, value }) {
51796
52026
  void commit("rig_remove_keys", { animation: anim.id, ...track, times: [t5] });
51797
52027
  return;
51798
52028
  }
51799
- const v6 = tr ? sample(tr, t5) : value;
52029
+ const v6 = tr ? sample(tr, t5, anim.loop && tr.keys[0].t === 0 && tr.keys.at(-1).t === anim.duration) : value;
51800
52030
  const ease2 = track.prop === "attachment" ? { ease: "step" } : {};
51801
52031
  void commit("rig_set_keys", { animation: anim.id, ...track, merge: true, keys: [{ t: t5, v: v6, ...ease2 }] });
51802
52032
  };
@@ -51846,6 +52076,16 @@ function BoneForm({ d: d5, id }) {
51846
52076
  /* @__PURE__ */ u4(Num, { label: "length", value: b4.length, onCommit: (v6) => setField({ length: v6 }) }),
51847
52077
  /* @__PURE__ */ u4(Sel, { label: "inherit", value: b4.inherit, options: INHERIT.map((v6) => [v6, v6]), onCommit: (v6) => setField({ inherit: v6 }) }),
51848
52078
  /* @__PURE__ */ u4(Txt, { label: "color", value: b4.color ?? "", onCommit: (v6) => setField({ color: v6 }) }),
52079
+ anim && /* @__PURE__ */ u4(S, { children: [
52080
+ /* @__PURE__ */ u4("button", { onClick: () => {
52081
+ const target = prompt("Follow bone at the current time (world pose is preserved)");
52082
+ if (target) void commit("rig_attach_to_bone", { animation: anim.id, bone: id, target, t: time.value });
52083
+ }, children: "attach at contact" }),
52084
+ /* @__PURE__ */ u4("button", { onClick: () => {
52085
+ const driver = prompt("Driver bone for baked secondary rotation", b4.parent ?? "root");
52086
+ if (driver) void commit("rig_bake_secondary", { animation: anim.id, driver, bones: [id] });
52087
+ }, children: "bake secondary motion" })
52088
+ ] }),
51849
52089
  /* @__PURE__ */ u4("h4", { children: [
51850
52090
  "transform",
51851
52091
  anim ? " (posed)" : ""
@@ -51912,6 +52152,15 @@ function SlotForm({ d: d5, id }) {
51912
52152
  }
51913
52153
  ),
51914
52154
  /* @__PURE__ */ u4(Num, { label: "z", value: z3, track: track("z"), onCommit: (v6) => anim ? setKey("z", Math.round(v6)) : setSlot({ z: Math.round(v6) }) }),
52155
+ /* @__PURE__ */ u4(
52156
+ Sel,
52157
+ {
52158
+ label: "clip mask",
52159
+ value: s6.clip ?? "",
52160
+ options: [["", "none"], ...d5.attachments.filter((a5) => a5.type === "path" && a5.closed).map((a5) => [a5.id, a5.id])],
52161
+ onCommit: (v6) => setSlot({ clip: v6 || null })
52162
+ }
52163
+ ),
51915
52164
  /* @__PURE__ */ u4(Sel, { label: "blend", value: s6.blend, options: BLEND.map((v6) => [v6, v6]), onCommit: (v6) => setSlot({ blend: v6 }) }),
51916
52165
  /* @__PURE__ */ u4(Txt, { label: "color", value: s6.color, onCommit: (v6) => setSlot({ color: v6 }) })
51917
52166
  ] });
@@ -52053,7 +52302,15 @@ function WeightsPanel({ d: d5, a: a5 }) {
52053
52302
  /* @__PURE__ */ u4("input", { type: "checkbox", checked: candidates.includes(x4.id), onChange: (e5) => toggle(x4.id, e5.currentTarget.checked) }),
52054
52303
  x4.id
52055
52304
  ] }, x4.id)),
52056
- /* @__PURE__ */ u4("button", { class: "auto-weights", title: "re-skin this mesh to the checked bones by distance", onClick: autoWeights, children: "auto weights" })
52305
+ /* @__PURE__ */ u4("button", { class: "auto-weights", title: "re-skin this mesh to the checked bones by distance", onClick: autoWeights, children: "auto weights" }),
52306
+ /* @__PURE__ */ u4("button", { disabled: candidates.length < 2, onClick: () => void commit("rig_gradient_weights", { id: a5.id, bones: candidates }), children: "chain gradient" }),
52307
+ /* @__PURE__ */ u4("p", { class: "hint", children: [
52308
+ "Gradient order: ",
52309
+ candidates.join(" \u2192 "),
52310
+ ". Uncheck and select bones in chain order."
52311
+ ] }),
52312
+ /* @__PURE__ */ u4("button", { disabled: !a5.weights, onClick: () => void commit("rig_smooth_weights", { id: a5.id, locked: selectedVertices.value }), children: "smooth (keep selected fixed)" }),
52313
+ /* @__PURE__ */ u4("button", { disabled: !selectedVertices.value.length, onClick: () => void commit("rig_pin_weights", { id: a5.id, bone: b4.bone, vertices: selectedVertices.value }), children: "bind selected to brush bone" })
52057
52314
  ] });
52058
52315
  }
52059
52316
  function VertexForm({ d: d5, id, index }) {
@@ -52153,8 +52410,6 @@ function ConstraintHead({ d: d5, kind, c: c5, set }) {
52153
52410
  const unknown = bones.filter((b4) => !d5.bones.some((x4) => x4.id === b4));
52154
52411
  if (!bones.length) setStatus(`${c5.id}: a constraint drives at least one bone`, "warn");
52155
52412
  else if (unknown.length) setStatus(`${c5.id}: no bone "${unknown.join('", "')}" in this rig`, "warn");
52156
- else if (kind === "ik" && bones.length > 2)
52157
- setStatus(`${c5.id}: an IK chain is one or two bones (the format has no longer ones), got ${bones.length}`, "warn");
52158
52413
  else if (bones.join(",") !== c5.bones.join(",")) set({ bones });
52159
52414
  };
52160
52415
  const options = kind === "path" ? d5.slots.filter((s6) => d5.attachments.some((a5) => a5.slot === s6.id && a5.type === "path")).map((s6) => [s6.id, s6.id]) : d5.bones.map((b4) => [b4.id, b4.id]);
@@ -52167,15 +52422,23 @@ function ConstraintHead({ d: d5, kind, c: c5, set }) {
52167
52422
  /* @__PURE__ */ u4(Sel, { label: "target", name: "target", value: c5.target, options, onCommit: (v6) => set({ target: v6 }) })
52168
52423
  ] });
52169
52424
  }
52170
- var Order = ({ value, set }) => /* @__PURE__ */ u4(Num, { label: "order", name: "order", value, onCommit: (v6) => set({ order: Math.round(v6) }) });
52425
+ var Order = ({ value, set }) => /* @__PURE__ */ u4(S, { children: [
52426
+ /* @__PURE__ */ u4(Num, { label: "order", name: "order", value, onCommit: (v6) => set({ order: Math.round(v6) }) }),
52427
+ /* @__PURE__ */ u4("button", { onClick: () => void commit("rig_order_constraints", {}), children: "order by dependencies" })
52428
+ ] });
52171
52429
  function IkForm({ d: d5, c: c5 }) {
52172
52430
  const o5 = constraintOps("ik", c5.id);
52173
52431
  return /* @__PURE__ */ u4(S, { children: [
52174
52432
  /* @__PURE__ */ u4("h4", { children: "ik constraint" }),
52175
52433
  /* @__PURE__ */ u4(ConstraintHead, { d: d5, kind: "ik", c: c5, set: o5.set }),
52176
52434
  /* @__PURE__ */ u4(Num, { label: "mix", name: "mix", step: 0.05, value: o5.value("mix", c5.mix), track: o5.track("mix"), onCommit: (v6) => o5.num("mix", v6) }),
52177
- /* @__PURE__ */ u4(Num, { label: "softness", name: "softness", value: o5.value("softness", c5.softness), track: o5.track("softness"), onCommit: (v6) => o5.num("softness", v6) }),
52178
- IK_FLAGS.map((f6) => /* @__PURE__ */ u4(Chk, { label: f6, name: f6, value: c5[f6], onCommit: (v6) => o5.set({ [f6]: v6 }) }, f6)),
52435
+ c5.bones.length > 2 && /* @__PURE__ */ u4(S, { children: [
52436
+ /* @__PURE__ */ u4(Num, { label: "iterations", value: c5.iterations ?? 100, onCommit: (v6) => o5.set({ iterations: Math.round(v6) }) }),
52437
+ /* @__PURE__ */ u4(Num, { label: "tolerance", value: c5.tolerance ?? 0.05, step: 0.01, onCommit: (v6) => o5.set({ tolerance: v6 }) })
52438
+ ] }),
52439
+ c5.bones.length <= 2 && /* @__PURE__ */ u4(Num, { label: "softness", name: "softness", value: o5.value("softness", c5.softness), track: o5.track("softness"), onCommit: (v6) => o5.num("softness", v6) }),
52440
+ IK_FLAGS.filter((f6) => c5.bones.length <= 2 || f6 === "bendPositive").map((f6) => /* @__PURE__ */ u4(Chk, { label: f6, name: f6, value: c5[f6], onCommit: (v6) => o5.set({ [f6]: v6 }) }, f6)),
52441
+ c5.bones.length > 2 && /* @__PURE__ */ u4("p", { class: "hint", children: "Long chains bend without stretching. Use a path to control the shape between the root and tip." }),
52179
52442
  /* @__PURE__ */ u4(Order, { value: c5.order, set: o5.set })
52180
52443
  ] });
52181
52444
  }
@@ -52186,7 +52449,7 @@ function TransformForm({ d: d5, c: c5 }) {
52186
52449
  /* @__PURE__ */ u4(ConstraintHead, { d: d5, kind: "transform", c: c5, set: o5.set }),
52187
52450
  TRANSFORM_FLAGS.map((f6) => /* @__PURE__ */ u4(Chk, { label: f6, name: f6, value: c5[f6], onCommit: (v6) => o5.set({ [f6]: v6 }) }, f6)),
52188
52451
  /* @__PURE__ */ u4("h4", { children: "offset" }),
52189
- TRANSFORM_OFFSETS.map((p6) => /* @__PURE__ */ u4(Num, { label: p6, name: p6, value: c5[p6], step: p6.startsWith("scale") ? 0.05 : 1, onCommit: (v6) => o5.set({ [p6]: v6 }) }, p6)),
52452
+ TRANSFORM_OFFSETS.map((p6) => /* @__PURE__ */ u4(Num, { label: p6, name: p6, value: o5.value(p6, c5[p6]), track: o5.track(p6), step: p6.startsWith("scale") ? 0.05 : 1, onCommit: (v6) => o5.num(p6, v6) }, p6)),
52190
52453
  /* @__PURE__ */ u4("h4", { children: "mix" }),
52191
52454
  MIX_PROPS.transform.map((p6) => /* @__PURE__ */ u4(Num, { label: p6, name: p6, step: 0.05, value: o5.value(p6, c5[p6]), track: o5.track(p6), onCommit: (v6) => o5.num(p6, v6) }, p6)),
52192
52455
  /* @__PURE__ */ u4(Order, { value: c5.order, set: o5.set })
@@ -52197,13 +52460,13 @@ function PathForm({ d: d5, c: c5 }) {
52197
52460
  return /* @__PURE__ */ u4(S, { children: [
52198
52461
  /* @__PURE__ */ u4("h4", { children: "path constraint" }),
52199
52462
  /* @__PURE__ */ u4(ConstraintHead, { d: d5, kind: "path", c: c5, set: o5.set }),
52200
- PATH_MODES.map(([p6, values]) => /* @__PURE__ */ u4(
52463
+ PATH_MODES.map(([p6, values2]) => /* @__PURE__ */ u4(
52201
52464
  Sel,
52202
52465
  {
52203
52466
  label: p6,
52204
52467
  name: p6,
52205
52468
  value: c5[p6],
52206
- options: values.map((v6) => [v6, v6]),
52469
+ options: values2.map((v6) => [v6, v6]),
52207
52470
  onCommit: (v6) => o5.set({ [p6]: v6 })
52208
52471
  },
52209
52472
  p6
@@ -52508,7 +52771,25 @@ function TrackSliders() {
52508
52771
  h2(() => () => cancelReleases(group), [group]);
52509
52772
  if (!a5 || !ref || !d5) return null;
52510
52773
  const tr = a5.tracks.find((t5) => t5.target === ref.target && t5.id === ref.id && t5.prop === ref.prop);
52511
- if (!tr || tr.keys.some((k4) => typeof k4.v !== "number")) return null;
52774
+ if (!tr || tr.keys.some((k4) => typeof k4.v !== "number" && !(Array.isArray(k4.v) && k4.v.every((v6) => typeof v6 === "number")))) return null;
52775
+ const interpolation = /* @__PURE__ */ u4(
52776
+ Sel,
52777
+ {
52778
+ label: "interpolation",
52779
+ value: tr.interpolation ?? "segment",
52780
+ options: [["segment", "per-key easing"], ["pchip", "smooth through keys"]],
52781
+ onCommit: (v6) => void commit("rig_set_keys", { animation: a5.id, ...ref, interpolation: v6, keys: tr.keys })
52782
+ }
52783
+ );
52784
+ if (tr.keys.some((k4) => typeof k4.v !== "number")) return /* @__PURE__ */ u4("div", { class: "track-adjust", children: [
52785
+ /* @__PURE__ */ u4("h4", { children: [
52786
+ "track \xB7 ",
52787
+ ref.id,
52788
+ ".",
52789
+ ref.prop
52790
+ ] }),
52791
+ interpolation
52792
+ ] });
52512
52793
  const cur = adjust.value.key === key ? adjust.value : { key, ...NEUTRAL };
52513
52794
  const vs = tr.keys.map((k4) => k4.v);
52514
52795
  const R2 = Math.max(1, Math.max(...vs) - Math.min(...vs));
@@ -52543,6 +52824,7 @@ function TrackSliders() {
52543
52824
  ".",
52544
52825
  ref.prop
52545
52826
  ] }),
52827
+ interpolation,
52546
52828
  Object.keys(NEUTRAL).map((k4) => /* @__PURE__ */ u4(
52547
52829
  Slider,
52548
52830
  {
@@ -52559,7 +52841,7 @@ function TrackSliders() {
52559
52841
  ))
52560
52842
  ] });
52561
52843
  }
52562
- var EASES = ["linear", "step", "ease_in", "ease_out", "ease_in_out", "back_out", "elastic_out", "bezier"];
52844
+ var EASES = ["linear", "step", "ease_in", "ease_out", "ease_in_out", "minimum_jerk", "back_out", "elastic_out", "bezier"];
52563
52845
  var BEZ_DEFAULT = [0.25, 0.1, 0.25, 1];
52564
52846
  var EW = 100;
52565
52847
  var EPAD = 15;
@@ -52619,6 +52901,26 @@ function KeyEase() {
52619
52901
  const tr = a5.tracks.find((k4) => k4.target === track.target && k4.id === track.id && k4.prop === track.prop);
52620
52902
  const key = tr?.keys.find((k4) => Math.abs(k4.t - times3[0]) < 1e-6);
52621
52903
  if (!key) return null;
52904
+ if (tr?.interpolation === "pchip") return /* @__PURE__ */ u4(S, { children: [
52905
+ /* @__PURE__ */ u4("h4", { children: [
52906
+ "key ",
52907
+ fmt2(key.t),
52908
+ "s \xB7 ",
52909
+ track.id,
52910
+ ".",
52911
+ track.prop
52912
+ ] }),
52913
+ /* @__PURE__ */ u4("p", { children: "Velocity is matched through neighbouring keys. Select per-key easing above to edit segment curves." }),
52914
+ /* @__PURE__ */ u4(
52915
+ Sel,
52916
+ {
52917
+ label: "segment",
52918
+ value: key.ease === "step" ? "step" : "smooth",
52919
+ options: [["smooth", "smooth"], ["step", "hold until next key"]],
52920
+ onCommit: (v6) => void commit("rig_set_keys", { animation: a5.id, ...track, merge: true, keys: [{ t: key.t, v: key.v, ease: v6 === "step" ? "step" : "linear" }] })
52921
+ }
52922
+ )
52923
+ ] });
52622
52924
  const bez = typeof key.ease === "string" ? BEZ_DEFAULT : key.ease.bezier;
52623
52925
  const set = (ease2) => void commit("rig_set_keys", {
52624
52926
  animation: a5.id,
@@ -53117,7 +53419,7 @@ var BoneGizmo = class {
53117
53419
  begin(handle, p6) {
53118
53420
  const skel = this.ctx.skel(), doc2 = this.ctx.shown(), st = skel.bones.get(this.boneId);
53119
53421
  if (!st) return;
53120
- const values = { x: st.x, y: st.y, rotation: st.rotation, scaleX: st.scaleX, scaleY: st.scaleY };
53422
+ const values2 = { x: st.x, y: st.y, rotation: st.rotation, scaleX: st.scaleX, scaleY: st.scaleY };
53121
53423
  const parent = parentWorld(skel, doc2, this.boneId);
53122
53424
  const [lx, ly] = parent ? toLocal(parent, p6[0], p6[1]) : p6;
53123
53425
  this.patch = void 0;
@@ -53127,11 +53429,11 @@ var BoneGizmo = class {
53127
53429
  joint: [st.world.tx, st.world.ty],
53128
53430
  doc: doc2,
53129
53431
  skel,
53130
- values,
53432
+ values: values2,
53131
53433
  time: this.ctx.time(),
53132
53434
  // parent-local, so the scale ratio ignores the parent's scale; the axis is whichever grip was grabbed (x or y)
53133
- axis: unit(lx - values.x, ly - values.y),
53134
- along: Math.hypot(lx - values.x, ly - values.y)
53435
+ axis: unit(lx - values2.x, ly - values2.y),
53436
+ along: Math.hypot(lx - values2.x, ly - values2.y)
53135
53437
  };
53136
53438
  }
53137
53439
  drag(p6, mods) {
@@ -53337,20 +53639,20 @@ var VertexGizmo = class {
53337
53639
  const anim = doc2.animations.find((a5) => a5.id === this.ctx.animId());
53338
53640
  const tr = anim?.tracks.find((x4) => x4.target === "deform" && x4.id === this.attachmentId && x4.prop === "vertices");
53339
53641
  if (!tr || !tr.keys.length) return void 0;
53340
- const v6 = sample(tr, t5);
53642
+ const v6 = sample(tr, t5, !!anim?.loop && tr.keys[0].t === 0 && tr.keys.at(-1).t === anim.duration);
53341
53643
  return Array.isArray(v6) && v6.length === deformLength(att) && typeof v6[0] === "number" ? v6.slice() : void 0;
53342
53644
  }
53343
53645
  drag(p6, _mods) {
53344
53646
  const s6 = this.start;
53345
53647
  if (!s6) return this.ctx.shown();
53346
- const delta = [p6[0] - s6.p[0], p6[1] - s6.p[1]];
53347
- this.delta = delta;
53648
+ const delta2 = [p6[0] - s6.p[0], p6[1] - s6.p[1]];
53649
+ this.delta = delta2;
53348
53650
  if (this.ctx.mode() !== "animate") {
53349
- const patch = this.shift(s6, this.localDeltas(s6, delta));
53651
+ const patch = this.shift(s6, this.localDeltas(s6, delta2));
53350
53652
  this.patch = patch;
53351
53653
  return s6.att.type === "path" ? setPathAttachment(s6.doc, this.attachmentId, s6.att.slot, { ...patch, closed: s6.att.closed }).doc : setMesh(s6.doc, this.attachmentId, patch).doc;
53352
53654
  }
53353
- this.offsets = this.localDeltas(s6, delta, s6.base?.slice()).map(round6);
53655
+ this.offsets = this.localDeltas(s6, delta2, s6.base?.slice()).map(round6);
53354
53656
  const anim = this.ctx.animId();
53355
53657
  if (!anim) return s6.doc;
53356
53658
  return setKeys(
@@ -53368,9 +53670,9 @@ var VertexGizmo = class {
53368
53670
  * rather than handed over, so no future in-place writer downstream can spoil the next move's baseline.
53369
53671
  * Rebuilt from that base every move, so a gesture never accumulates.
53370
53672
  */
53371
- localDeltas(s6, delta, base) {
53673
+ localDeltas(s6, delta2, base) {
53372
53674
  let out2 = base;
53373
- for (const i6 of s6.indices) out2 = deformOffsetsFor(s6.skel, s6.att, out2, i6, delta);
53675
+ for (const i6 of s6.indices) out2 = deformOffsetsFor(s6.skel, s6.att, out2, i6, delta2);
53374
53676
  return out2 ?? [];
53375
53677
  }
53376
53678
  /**
@@ -53400,12 +53702,12 @@ var VertexGizmo = class {
53400
53702
  return { vertices: s6.att.vertices.map((v6, k4) => moved.has(k4 >> 1) ? round6(v6 + off[k4]) : v6) };
53401
53703
  }
53402
53704
  commit() {
53403
- const s6 = this.start, delta = this.delta, patch = this.patch, offsets = this.offsets;
53705
+ const s6 = this.start, delta2 = this.delta, patch = this.patch, offsets = this.offsets;
53404
53706
  this.start = void 0;
53405
53707
  this.delta = void 0;
53406
53708
  this.patch = void 0;
53407
53709
  this.offsets = void 0;
53408
- if (!s6 || !delta || Math.hypot(delta[0], delta[1]) <= 1e-6) return void 0;
53710
+ if (!s6 || !delta2 || Math.hypot(delta2[0], delta2[1]) <= 1e-6) return void 0;
53409
53711
  if (this.ctx.mode() !== "animate") {
53410
53712
  if (!patch) return void 0;
53411
53713
  return s6.att.type === "path" ? { tool: "rig_set_path_attachment", args: { id: this.attachmentId, slot: s6.att.slot, ...patch, closed: s6.att.closed } } : { tool: "rig_set_mesh", args: { id: this.attachmentId, ...patch } };
@@ -57351,6 +57653,7 @@ var import_gifenc = __toESM(require_gifenc(), 1);
57351
57653
  var AnimationState = class {
57352
57654
  constructor(sink, opts = {}) {
57353
57655
  this.sink = sink;
57656
+ this.opts = opts;
57354
57657
  this.defaultMix = opts.mix ?? 0;
57355
57658
  }
57356
57659
  onEvent;
@@ -57375,11 +57678,18 @@ var AnimationState = class {
57375
57678
  }
57376
57679
  channel(id, loop2, priority = 0) {
57377
57680
  const anim = this.sink.doc.animations.find((a5) => a5.id === id);
57378
- if (!anim) throw new Error(`unknown animation "${id}"`);
57681
+ if (!anim) {
57682
+ const message = `unknown animation "${id}"; available: ${this.sink.doc.animations.map((a5) => a5.id).join(", ")}`;
57683
+ if (this.opts.missingAnimation === "throw") throw new Error(message);
57684
+ (this.opts.onWarning ?? console.warn)(message);
57685
+ return void 0;
57686
+ }
57379
57687
  return { anim, time: 0, loop: loop2 ?? anim.loop, done: false, priority, fresh: true };
57380
57688
  }
57381
57689
  setBase(id, opts = {}) {
57382
- this.base = id === void 0 ? void 0 : this.channel(id);
57690
+ const next = id === void 0 ? void 0 : this.channel(id);
57691
+ if (id !== void 0 && !next) return;
57692
+ this.base = next;
57383
57693
  if (!this.main || this.main.done) {
57384
57694
  this.main = void 0;
57385
57695
  this.beginMix(opts.mix);
@@ -57388,6 +57698,16 @@ var AnimationState = class {
57388
57698
  }
57389
57699
  play(id, opts = {}) {
57390
57700
  const ch = this.channel(id, opts.loop, opts.priority ?? 0);
57701
+ if (!ch) {
57702
+ this.main?.settle?.("interrupted");
57703
+ for (const queued of this.queue) queued.settle?.("dropped");
57704
+ this.main = void 0;
57705
+ this.queue = [];
57706
+ this.beginMix(opts.mix);
57707
+ this.update(0);
57708
+ opts.onComplete?.("dropped");
57709
+ return Promise.resolve("dropped");
57710
+ }
57391
57711
  const promise2 = new Promise((resolve) => {
57392
57712
  ch.settle = (r5) => {
57393
57713
  ch.settle = void 0;
@@ -57408,7 +57728,10 @@ var AnimationState = class {
57408
57728
  /** Independent loop on top of the shown pose; `undefined` removes it. */
57409
57729
  setOverlay(name, id) {
57410
57730
  if (id === void 0) this.overlays.delete(name);
57411
- else this.overlays.set(name, this.channel(id));
57731
+ else {
57732
+ const ch = this.channel(id);
57733
+ if (ch) this.overlays.set(name, ch);
57734
+ }
57412
57735
  }
57413
57736
  /** Clear everything and show the setup pose. Pending plays resolve as "stopped". */
57414
57737
  stop() {
@@ -57493,6 +57816,7 @@ function hit(t5, prev, cur, d5, loop2) {
57493
57816
  // src/rig-runtime.ts
57494
57817
  var DEG4 = Math.PI / 180;
57495
57818
  var MESH_GRID = 9;
57819
+ var GHOST_ALPHA = 0.5;
57496
57820
  async function createTextures(doc2, loadSource) {
57497
57821
  const bySrc = /* @__PURE__ */ new Map(), out2 = /* @__PURE__ */ new Map();
57498
57822
  for (const a5 of doc2.assets) {
@@ -57532,7 +57856,7 @@ function atlasUV(asset, u6, v6, sourceW, sourceH) {
57532
57856
  var rgbOf = (c5) => Math.round(c5[0] * 255) << 16 | Math.round(c5[1] * 255) << 8 | Math.round(c5[2] * 255);
57533
57857
  var sameNumbers = (a5, b4) => a5 === b4 || a5.length === b4.length && a5.every((v6, i6) => v6 === b4[i6]);
57534
57858
  var RigPlayer = class {
57535
- /** `mix`: default crossfade seconds for state transitions (setBase, completion → base); play() passes its own */
57859
+ /** Default crossfade is 0.18 seconds; explicit mix: 0 requests a hard cut. */
57536
57860
  constructor(doc2, textures, opts = {}) {
57537
57861
  this.doc = doc2;
57538
57862
  parse(doc2);
@@ -57571,7 +57895,7 @@ var RigPlayer = class {
57571
57895
  this.items.push(item);
57572
57896
  this.view.addChild(item.node);
57573
57897
  }
57574
- this.state = new AnimationState(this, { mix: opts.mix ?? 0 });
57898
+ this.state = new AnimationState(this, { ...opts, mix: opts.mix ?? 0.18 });
57575
57899
  this.setPose(/* @__PURE__ */ new Map());
57576
57900
  }
57577
57901
  view = new Container();
@@ -57582,6 +57906,9 @@ var RigPlayer = class {
57582
57906
  pages = /* @__PURE__ */ new Map();
57583
57907
  /** slots the editor hides; honoured by setPose */
57584
57908
  hidden = /* @__PURE__ */ new Set();
57909
+ /** slots the editor has selected; setPose draws them at GHOST_ALPHA at least */
57910
+ ghosts = /* @__PURE__ */ new Set();
57911
+ clipGraphics = /* @__PURE__ */ new Map();
57585
57912
  /** whole atlas page as a plain texture (identity uv mapping) for skinned meshes */
57586
57913
  pageTexture(tex) {
57587
57914
  let page = this.pages.get(tex.source);
@@ -57593,7 +57920,7 @@ var RigPlayer = class {
57593
57920
  this.state.onEvent = opts.onEvent;
57594
57921
  return this.state.play(animId2, {
57595
57922
  loop: opts.loop,
57596
- mix: opts.mix ?? 0,
57923
+ mix: opts.mix,
57597
57924
  ifBusy: "interrupt",
57598
57925
  onComplete: (r5) => {
57599
57926
  if (r5 === "completed") opts.onComplete?.();
@@ -57622,6 +57949,10 @@ var RigPlayer = class {
57622
57949
  if (visible) this.hidden.delete(slotId);
57623
57950
  else this.hidden.add(slotId);
57624
57951
  }
57952
+ /** Editor-only: draw these slots at GHOST_ALPHA at least, whatever the pose says (an empty list clears it). */
57953
+ setGhostSlots(slotIds) {
57954
+ this.ghosts = new Set(slotIds);
57955
+ }
57625
57956
  /** Stop playback and immediately restore the complete setup pose. */
57626
57957
  stop() {
57627
57958
  this.state.stop();
@@ -57633,12 +57964,32 @@ var RigPlayer = class {
57633
57964
  /** Apply a pose (rest where silent) to the display objects. */
57634
57965
  setPose(pose2) {
57635
57966
  const skel = this.skel = evaluate(this.doc, pose2);
57967
+ const clips = new Set(this.doc.slots.flatMap((s6) => s6.clip ? [s6.clip] : []));
57968
+ for (const [id, g4] of this.clipGraphics) if (!clips.has(id)) {
57969
+ g4.destroy();
57970
+ this.clipGraphics.delete(id);
57971
+ }
57972
+ for (const id of clips) {
57973
+ const att = this.doc.attachments.find((a5) => a5.id === id);
57974
+ if (att?.type !== "path" || !att.closed) continue;
57975
+ let g4 = this.clipGraphics.get(id);
57976
+ if (!g4) {
57977
+ g4 = new Graphics();
57978
+ this.clipGraphics.set(id, g4);
57979
+ this.view.addChild(g4);
57980
+ }
57981
+ const vertices = attachmentWorldVertices(skel, att.slot, att);
57982
+ const n4 = Math.max(32, vertices.length * 4);
57983
+ const points = samplePath(vertices, true, att.lengths, true, Array.from({ length: n4 }, (_4, i6) => i6 / n4), true);
57984
+ g4.clear().poly(points.flatMap((p6) => [p6.x, p6.y]), true).fill(16777215);
57985
+ }
57636
57986
  const m5 = new Matrix();
57637
57987
  const orderV = pose2.get(pk("drawOrder", "order", "order"));
57638
57988
  const order = /* @__PURE__ */ new Map();
57639
57989
  if (Array.isArray(orderV)) orderV.forEach((id, i6) => order.set(id, i6));
57640
57990
  for (const it of this.items) {
57641
57991
  const s6 = it.slot, shown2 = skel.attachment.get(s6.id);
57992
+ it.node.mask = s6.clip ? this.clipGraphics.get(s6.clip) ?? null : null;
57642
57993
  if (shown2 !== it.att || this.hidden.has(s6.id)) {
57643
57994
  it.node.visible = false;
57644
57995
  continue;
@@ -57646,7 +57997,8 @@ var RigPlayer = class {
57646
57997
  it.node.visible = true;
57647
57998
  const colorV = pose2.get(pk("slot", s6.id, "color"));
57648
57999
  const color = Array.isArray(colorV) && typeof colorV[0] === "number" ? colorV : colorToArray(s6.color);
57649
- it.node.alpha = numOf(pose2, pk("slot", s6.id, "alpha"), s6.alpha) * color[3];
58000
+ const alpha = numOf(pose2, pk("slot", s6.id, "alpha"), s6.alpha) * color[3];
58001
+ it.node.alpha = this.ghosts.has(s6.id) ? Math.max(alpha, GHOST_ALPHA) : alpha;
57650
58002
  it.node.tint = rgbOf(color);
57651
58003
  it.node.zIndex = order.size ? order.get(s6.id) ?? 0 : numOf(pose2, pk("slot", s6.id, "z"), s6.z);
57652
58004
  if (it.skinned) {
@@ -57979,6 +58331,7 @@ var Stage = class {
57979
58331
  }));
57980
58332
  this.dispose.push(j3(() => {
57981
58333
  hiddenSlots.value;
58334
+ selection.value;
57982
58335
  f3(() => this.applyPose());
57983
58336
  }));
57984
58337
  this.dispose.push(j3(() => {
@@ -58088,7 +58441,7 @@ var Stage = class {
58088
58441
  for (const l6 of this.labelPool.values()) l6.destroy();
58089
58442
  this.labelPool.clear();
58090
58443
  this.labels.removeChildren();
58091
- this.player = new RigPlayer(d5, this.textures);
58444
+ this.player = new RigPlayer(d5, this.textures, { mix: 0 });
58092
58445
  this.structKey = structureKeyOf(d5);
58093
58446
  this.player.view.visible = !this.atlasOn;
58094
58447
  this.camera.addChildAt(this.player.view, 1);
@@ -58113,6 +58466,7 @@ var Stage = class {
58113
58466
  if (!p6 || !d5) return;
58114
58467
  const hidden = hiddenSlots.value;
58115
58468
  for (const s6 of d5.slots) p6.setSlotVisible(s6.id, !hidden.has(s6.id));
58469
+ p6.setGhostSlots(playing.peek() ? [] : this.selectedSlots(d5));
58116
58470
  const anim = currentAnim.value;
58117
58471
  if (mode.value === "animate" && anim) {
58118
58472
  void p6.play(anim.id, { loop: loop.value });
@@ -58126,6 +58480,21 @@ var Stage = class {
58126
58480
  }
58127
58481
  this.drawOverlay();
58128
58482
  }
58483
+ /**
58484
+ * The slots the selection is about — a slot, the one an attachment (or a vertex of it) hangs in, or every slot on
58485
+ * a selected bone. The player draws them half-visible at least, so a part faded out at rest (runes that only light
58486
+ * up in `win`) is not an invisible box of grips. Not while playing: the animation's own fade is what is watched.
58487
+ */
58488
+ selectedSlots(d5) {
58489
+ const sel = selection.peek();
58490
+ if (sel?.kind === "slot") return [sel.id];
58491
+ if (sel?.kind === "attachment" || sel?.kind === "vertex") {
58492
+ const a5 = d5.attachments.find((a6) => a6.id === sel.id);
58493
+ return a5 ? [a5.slot] : [];
58494
+ }
58495
+ if (sel?.kind === "bone") return d5.slots.filter((s6) => s6.bone === sel.id).map((s6) => s6.id);
58496
+ return [];
58497
+ }
58129
58498
  // ---------- reference underlay ----------
58130
58499
  reference;
58131
58500
  referenceSrc;
@@ -58204,6 +58573,25 @@ var Stage = class {
58204
58573
  * is on screen. The overlay (bones, labels, gizmos) is switched off for the duration, so nothing of the editor
58205
58574
  * ends up in it; the paper stays, which is what makes every pixel opaque — GIF has no partial transparency.
58206
58575
  */
58576
+ async exportPng() {
58577
+ if (!this.player || this.exporting) return;
58578
+ const p6 = this.player, d5 = shown.peek();
58579
+ if (!d5) return;
58580
+ this.exporting = true;
58581
+ const wasPlaying = playing.peek();
58582
+ playing.value = false;
58583
+ p6.setGhostSlots([]);
58584
+ p6.setPose(pose.peek());
58585
+ try {
58586
+ return await this.app.renderer.extract.base64({ target: p6.view, frame: new Rectangle(0, 0, d5.meta.width, d5.meta.height), clearColor: [0, 0, 0, 0] });
58587
+ } finally {
58588
+ this.exporting = false;
58589
+ if (!this.destroyed) {
58590
+ playing.value = wasPlaying;
58591
+ this.applyPose();
58592
+ }
58593
+ }
58594
+ }
58207
58595
  async exportGif(fps = FPS) {
58208
58596
  const d5 = shown.peek(), anim = currentAnim.peek(), p6 = this.player;
58209
58597
  if (!p6 || !d5 || !anim || mode.peek() !== "animate") {
@@ -58215,12 +58603,13 @@ var Stage = class {
58215
58603
  const wasPlaying = playing.peek();
58216
58604
  playing.value = false;
58217
58605
  this.overlay.visible = false;
58606
+ p6.setGhostSlots([]);
58218
58607
  try {
58219
58608
  const w5 = d5.meta.width, h6 = d5.meta.height, n4 = Math.max(1, Math.round(anim.duration * fps));
58220
58609
  const frame = new Rectangle(0, 0, w5, h6), gif = (0, import_gifenc.GIFEncoder)();
58221
58610
  for (let i6 = 0; i6 < n4; i6++) {
58222
58611
  if (this.destroyed) return;
58223
- void p6.play(anim.id, { loop: false });
58612
+ void p6.play(anim.id, { loop: false, mix: 0 });
58224
58613
  p6.tick(i6 / fps);
58225
58614
  const { pixels } = this.app.renderer.extract.pixels({ target: this.camera, frame });
58226
58615
  const palette = (0, import_gifenc.quantize)(pixels, 256);
@@ -59380,7 +59769,7 @@ var Timeline = class {
59380
59769
  selectedTrack.value = ref;
59381
59770
  selectedKeys.value = [t5];
59382
59771
  });
59383
- void commit("rig_set_keys", { animation: id, ...ref, keys: [{ t: t5, v: sample(tr, t5), ease: prev?.ease }], merge: true });
59772
+ void commit("rig_set_keys", { animation: id, ...ref, keys: [{ t: t5, v: sample(tr, t5, anim.loop && tr.keys[0].t === 0 && tr.keys.at(-1).t === anim.duration), ease: prev?.ease }], merge: true });
59384
59773
  }
59385
59774
  bindPointer() {
59386
59775
  const c5 = this.canvas;
@@ -59546,6 +59935,30 @@ async function exportGif() {
59546
59935
  a5.click();
59547
59936
  setTimeout(() => URL.revokeObjectURL(url), 1e4);
59548
59937
  }
59938
+ async function exportMp4() {
59939
+ const animation = animId.peek();
59940
+ if (!animation) {
59941
+ setStatus("Select an animation first", "warn");
59942
+ return;
59943
+ }
59944
+ const outFile = prompt("Save MP4 to (path relative to this rig, or absolute)", `../previews/${shown.peek()?.meta.name ?? "rig"}-${animation}.mp4`);
59945
+ if (!outFile) return;
59946
+ setStatus("Rendering MP4 at 60 fps\u2026");
59947
+ try {
59948
+ const r5 = await callTool(rigPath, "rig_export_video", { animation, outFile, fps: 60 });
59949
+ setStatus(r5.text, r5.isError ? "warn" : "info");
59950
+ } catch (e5) {
59951
+ setStatus(String(e5), "warn");
59952
+ }
59953
+ }
59954
+ async function exportPng() {
59955
+ const url = await stage?.exportPng();
59956
+ if (!url) return;
59957
+ const a5 = document.createElement("a");
59958
+ a5.href = url;
59959
+ a5.download = `${shown.peek()?.meta.name ?? "rig"}.png`;
59960
+ a5.click();
59961
+ }
59549
59962
  function setMode(m5) {
59550
59963
  if (m5 === "animate" && !shown.value?.animations.length) {
59551
59964
  setStatus("no animations \u2014 create one in the properties panel", "warn");
@@ -59772,6 +60185,8 @@ function TopBar() {
59772
60185
  children: "gif"
59773
60186
  }
59774
60187
  ),
60188
+ /* @__PURE__ */ u4("button", { class: "mp4", onClick: click(() => void exportMp4()), children: "mp4 \xB7 60 fps" }),
60189
+ /* @__PURE__ */ u4("button", { class: "png", title: "transparent PNG of the current pose", onClick: click(() => void exportPng()), children: "png" }),
59775
60190
  /* @__PURE__ */ u4("button", { class: "fit", title: "fit the document to the viewport (F)", onClick: click(() => stage?.fit()), children: "fit" }),
59776
60191
  /* @__PURE__ */ u4(
59777
60192
  "button",