@energy8platform/golem 0.2.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +28 -2
  2. package/dist/editor.css +50 -0
  3. package/dist/editor.js +1772 -1037
  4. package/dist/lib/cli.js +1575 -366
  5. package/dist/lib/cli.js.map +4 -4
  6. package/dist/lib/editor/controls-panel.d.ts +2 -0
  7. package/dist/lib/editor/stage.d.ts +1 -0
  8. package/dist/lib/editor/store.d.ts +212 -33
  9. package/dist/lib/editor-entry.js +1575 -366
  10. package/dist/lib/editor-entry.js.map +4 -4
  11. package/dist/lib/harness.js +328 -31
  12. package/dist/lib/interactive-bones.d.ts +37 -0
  13. package/dist/lib/interactive-editor.d.ts +80 -0
  14. package/dist/lib/interactive-editor.js +744 -0
  15. package/dist/lib/interactive-editor.js.map +7 -0
  16. package/dist/lib/preview/export-video.d.ts +12 -0
  17. package/dist/lib/preview/harness.d.ts +2 -0
  18. package/dist/lib/preview/render-preview.d.ts +6 -0
  19. package/dist/lib/rig-anim.d.ts +6 -3
  20. package/dist/lib/rig-check.d.ts +2 -1
  21. package/dist/lib/rig-constraints.d.ts +63 -0
  22. package/dist/lib/rig-control-qa.d.ts +319 -0
  23. package/dist/lib/rig-controls.d.ts +13 -0
  24. package/dist/lib/rig-format.d.ts +1469 -331
  25. package/dist/lib/rig-gates.d.ts +209 -0
  26. package/dist/lib/rig-mesh.d.ts +1 -0
  27. package/dist/lib/rig-motion.d.ts +22 -0
  28. package/dist/lib/rig-qa.d.ts +35 -0
  29. package/dist/lib/rig-quality.d.ts +12 -0
  30. package/dist/lib/rig-runtime.d.ts +43 -3
  31. package/dist/lib/rig-state.d.ts +3 -0
  32. package/dist/lib/rig-tools.d.ts +5 -1
  33. package/dist/lib/runtime.d.ts +3 -1
  34. package/dist/lib/runtime.js +306 -28
  35. package/dist/lib/runtime.js.map +4 -4
  36. package/dist/lib/tools.js +1557 -348
  37. package/dist/lib/tools.js.map +4 -4
  38. package/package.json +8 -2
@@ -1,5 +1,5 @@
1
1
  // src/rig-runtime.ts
2
- import { Container, Sprite, Mesh, MeshGeometry, MeshPlane, Texture, Matrix, Rectangle } from "pixi.js";
2
+ import { Container, Sprite, Mesh, MeshGeometry, MeshPlane, Texture, Matrix, Rectangle, Graphics } from "pixi.js";
3
3
 
4
4
  // src/rig-format.ts
5
5
  import { z } from "zod";
@@ -48,7 +48,7 @@ function gridPositions(offsets, w, h, pivot, n, out = new Float32Array(2 * n * n
48
48
  var ID_RE = /^[a-z][a-z0-9_]*$/;
49
49
  var Id = z.string().regex(ID_RE, "snake_case id");
50
50
  var Ease = z.union([
51
- z.enum(["linear", "step", "ease_in", "ease_out", "ease_in_out", "back_out", "elastic_out"]),
51
+ z.enum(["linear", "step", "ease_in", "ease_out", "ease_in_out", "minimum_jerk", "back_out", "elastic_out"]),
52
52
  // cubic-bezier control points, same semantics as CSS
53
53
  z.object({ bezier: z.tuple([z.number(), z.number(), z.number(), z.number()]) })
54
54
  ]);
@@ -80,6 +80,7 @@ var Asset = z.object({
80
80
  /** where this layer came from — provenance for agents */
81
81
  origin: z.object({
82
82
  sourceImage: z.string().optional(),
83
+ bboxSpace: z.enum(["source", "document"]).optional(),
83
84
  /** bbox in source image: [x, y, w, h] */
84
85
  bbox: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(),
85
86
  inpainted: z.boolean().default(false),
@@ -102,6 +103,8 @@ var Bone = z.object({
102
103
  var Slot = z.object({
103
104
  id: Id,
104
105
  bone: Id,
106
+ /** Closed path attachment used as a moving clipping mask. */
107
+ clip: Id.optional(),
105
108
  /** active attachment id; undefined = hidden */
106
109
  attachment: Id.optional(),
107
110
  alpha: z.number().min(0).max(1).default(1),
@@ -164,6 +167,8 @@ var IkConstraint = z.object({
164
167
  order: z.number().int().default(0),
165
168
  bones: z.array(Id).min(1),
166
169
  target: Id,
170
+ iterations: z.number().int().min(1).max(1e3).optional(),
171
+ tolerance: z.number().positive().optional(),
167
172
  mix: z.number().default(1),
168
173
  softness: z.number().default(0),
169
174
  bendPositive: z.boolean().default(true),
@@ -211,7 +216,7 @@ var BoneProp = z.enum(["x", "y", "rotation", "scaleX", "scaleY", "shearX", "shea
211
216
  var SlotProp = z.enum(["alpha", "attachment", "z", "color"]);
212
217
  var DeformProp = z.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");
213
218
  var IkProp = z.enum(["mix", "softness", "bendPositive", "compress", "stretch"]);
214
- var TransformProp = z.enum(["mixRotate", "mixX", "mixY", "mixScaleX", "mixScaleY", "mixShearY"]);
219
+ var TransformProp = z.enum(["mixRotate", "mixX", "mixY", "mixScaleX", "mixScaleY", "mixShearY", "x", "y", "rotation", "scaleX", "scaleY", "shearY"]);
215
220
  var PathProp = z.enum(["position", "spacing", "mixRotate", "mixX", "mixY"]);
216
221
  var KeyValue = z.union([z.number(), z.string(), z.null(), z.array(z.number()), z.array(z.string())]);
217
222
  var Key = z.object({
@@ -220,14 +225,16 @@ var Key = z.object({
220
225
  /** easing INTO the next key */
221
226
  ease: Ease.default("ease_in_out")
222
227
  });
228
+ var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
223
229
  var Track = z.discriminatedUnion("target", [
224
- z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, keys: z.array(Key).min(1) }),
225
- z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, keys: z.array(Key).min(1) }),
226
- z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, keys: z.array(Key).min(1) }),
227
- z.object({ target: z.literal("ik"), id: Id, prop: IkProp, keys: z.array(Key).min(1) }),
228
- z.object({ target: z.literal("transform"), id: Id, prop: TransformProp, keys: z.array(Key).min(1) }),
229
- z.object({ target: z.literal("path"), id: Id, prop: PathProp, keys: z.array(Key).min(1) }),
230
- z.object({ target: z.literal("drawOrder"), id: Id.default("order"), prop: z.literal("order").default("order"), keys: z.array(Key).min(1) })
230
+ z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
231
+ z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
232
+ z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
233
+ z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
234
+ z.object({ target: z.literal("ik"), id: Id, prop: IkProp, ...Interpolation, keys: z.array(Key).min(1) }),
235
+ z.object({ target: z.literal("transform"), id: Id, prop: TransformProp, ...Interpolation, keys: z.array(Key).min(1) }),
236
+ z.object({ target: z.literal("path"), id: Id, prop: PathProp, ...Interpolation, keys: z.array(Key).min(1) }),
237
+ z.object({ target: z.literal("drawOrder"), id: Id.default("order"), prop: z.literal("order").default("order"), ...Interpolation, keys: z.array(Key).min(1) })
231
238
  ]);
232
239
  var Event = z.object({
233
240
  t: z.number().nonnegative(),
@@ -243,6 +250,25 @@ var Animation = z.object({
243
250
  /** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
244
251
  preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
245
252
  });
253
+ var RigControl = z.object({
254
+ id: Id,
255
+ label: z.string().optional(),
256
+ min: z.number().finite(),
257
+ max: z.number().finite(),
258
+ default: z.number().finite()
259
+ }).strict();
260
+ var ControlValues = z.array(z.union([z.number().finite(), z.array(z.number().finite()).min(1)])).min(2);
261
+ var ControlChannel = z.discriminatedUnion("target", [
262
+ z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
263
+ z.object({ target: z.literal("slot"), id: Id, prop: z.literal("alpha"), values: ControlValues }).strict(),
264
+ z.object({ target: z.literal("deform"), id: Id, prop: z.literal("vertices"), values: ControlValues }).strict()
265
+ ]);
266
+ var ControlBinding = z.object({
267
+ id: Id,
268
+ axes: z.array(z.object({ control: Id, keys: z.array(z.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
269
+ /** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
270
+ channels: z.array(ControlChannel).min(1)
271
+ }).strict();
246
272
  var Reference = z.object({
247
273
  src: z.string(),
248
274
  opacity: z.number().min(0).max(1).default(0.5),
@@ -268,7 +294,9 @@ var RigSchema = z.object({
268
294
  ik: z.array(IkConstraint).default([]),
269
295
  transform: z.array(TransformConstraint).default([]),
270
296
  path: z.array(PathConstraint).default([]),
271
- animations: z.array(Animation)
297
+ animations: z.array(Animation),
298
+ controls: z.array(RigControl).optional(),
299
+ controlBindings: z.array(ControlBinding).optional()
272
300
  });
273
301
  var RigDocument = z.preprocess((input) => {
274
302
  const v = input?.version;
@@ -343,6 +371,8 @@ function validate(doc) {
343
371
  dup(doc.ik, "ik");
344
372
  dup(doc.transform, "transform");
345
373
  dup(doc.path, "path");
374
+ dup(doc.controls ?? [], "control");
375
+ dup(doc.controlBindings ?? [], "control binding");
346
376
  const bones = new Set(doc.bones.map((b) => b.id));
347
377
  const assets = new Map(doc.assets.map((a) => [a.id, a]));
348
378
  const slots = new Set(doc.slots.map((s) => s.id));
@@ -369,6 +399,10 @@ function validate(doc) {
369
399
  if (a.frame && !a.trim && !a.rotate && (a.frame[2] !== a.width || a.frame[3] !== a.height)) errs.push(`asset "${a.id}": frame size must equal width\xD7height (or declare trim/rotate)`);
370
400
  }
371
401
  for (const s of doc.slots) {
402
+ if (s.clip) {
403
+ const mask = attachments.get(s.clip);
404
+ if (mask?.type !== "path" || !mask.closed) errs.push(`slot "${s.id}": clip must name a closed path attachment`);
405
+ }
372
406
  if (!bones.has(s.bone)) errs.push(`slot "${s.id}": unknown bone "${s.bone}"`);
373
407
  if (s.attachment) {
374
408
  const a = attachments.get(s.attachment);
@@ -400,7 +434,8 @@ function validate(doc) {
400
434
  }
401
435
  }
402
436
  for (const c of doc.ik) {
403
- if (c.bones.length > 2) errs.push(`ik "${c.id}": must have 1 or 2 bones`);
437
+ for (let i = 1; i < c.bones.length; i++) if (doc.bones.find((b) => b.id === c.bones[i])?.parent !== c.bones[i - 1])
438
+ errs.push(`ik "${c.id}": bones must form a contiguous parent-to-child chain`);
404
439
  for (const b of c.bones) if (!bones.has(b)) errs.push(`ik "${c.id}": unknown bone "${b}"`);
405
440
  if (!bones.has(c.target)) errs.push(`ik "${c.id}": unknown target bone "${c.target}"`);
406
441
  }
@@ -412,13 +447,47 @@ function validate(doc) {
412
447
  for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
413
448
  if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
414
449
  }
450
+ const controls = new Map((doc.controls ?? []).map((c) => [c.id, c]));
451
+ const driven = /* @__PURE__ */ new Set();
452
+ for (const c of controls.values()) if (!(c.min < c.max && c.default >= c.min && c.default <= c.max))
453
+ errs.push(`control "${c.id}": require min < max and default within range`);
454
+ for (const b of doc.controlBindings ?? []) {
455
+ const prefix = `control binding "${b.id}"`;
456
+ if (new Set(b.axes.map((a) => a.control)).size !== b.axes.length) errs.push(`${prefix}: axes must use different controls`);
457
+ for (const axis of b.axes) {
458
+ const c = controls.get(axis.control);
459
+ if (!c) errs.push(`${prefix}: unknown control "${axis.control}"`);
460
+ else if (axis.keys[0] !== c.min || axis.keys.at(-1) !== c.max) errs.push(`${prefix}: keys must span the control's full range`);
461
+ if (axis.keys.some((v, i) => i > 0 && v <= axis.keys[i - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
462
+ }
463
+ const count = b.axes.reduce((n, a) => n * a.keys.length, 1);
464
+ for (const ch of b.channels) {
465
+ const key = `${ch.target}.${ch.id}.${ch.prop}`;
466
+ if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
467
+ driven.add(key);
468
+ if (ch.values.length !== count) errs.push(`${prefix}: ${key} needs ${count} grid values`);
469
+ if (ch.target === "deform") {
470
+ const att = attachments.get(ch.id);
471
+ if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
472
+ else {
473
+ const n = att.weights ? att.weights.reduce((s, a) => s + a.length * 2, 0) : att.vertices.length;
474
+ if (ch.values.some((v) => !Array.isArray(v) || v.length !== n)) errs.push(`${prefix}: ${key} needs arrays of ${n} offsets`);
475
+ }
476
+ } else {
477
+ if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
478
+ if (ch.values.some((v) => typeof v !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
479
+ if (ch.target === "slot" && ch.values.some((v) => typeof v === "number" && (v < 0 || v > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
480
+ }
481
+ }
482
+ }
415
483
  for (const a of doc.animations) {
416
484
  const tracks = /* @__PURE__ */ new Set();
417
485
  for (const t of a.tracks) {
418
486
  const tid = trackId(t);
419
487
  if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
420
488
  tracks.add(tid);
421
- const known = t.target === "bone" ? bones.has(t.id) : t.target === "slot" ? slots.has(t.id) : t.target === "deform" ? attachments.has(t.id) : t.target === "ik" ? iks.has(t.id) : t.target === "transform" ? transforms.has(t.id) : t.target === "path" ? paths.has(t.id) : true;
489
+ if (driven.has(tid)) errs.push(`animation "${a.id}": ${tid} is controlled by a binding; animate its control instead`);
490
+ const known = t.target === "bone" ? bones.has(t.id) : t.target === "slot" ? slots.has(t.id) : t.target === "deform" ? attachments.has(t.id) : t.target === "ik" ? iks.has(t.id) : t.target === "transform" ? transforms.has(t.id) : t.target === "path" ? paths.has(t.id) : t.target === "control" ? controls.has(t.id) : true;
422
491
  if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
423
492
  const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
424
493
  const att = t.target === "deform" ? attachments.get(t.id) : void 0;
@@ -426,6 +495,10 @@ function validate(doc) {
426
495
  for (const k of t.keys) {
427
496
  if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
428
497
  if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
498
+ if (t.target === "control") {
499
+ const c = controls.get(t.id);
500
+ if (typeof k.v !== "number" || !Number.isFinite(k.v) || c && (k.v < c.min || k.v > c.max)) errs.push(`animation "${a.id}" ${tid}: control key outside its finite range`);
501
+ }
429
502
  if (t.target === "slot" && t.prop === "attachment") {
430
503
  if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
431
504
  else if (typeof k.v === "string") {
@@ -464,6 +537,52 @@ function parse(input) {
464
537
  return doc;
465
538
  }
466
539
 
540
+ // src/rig-controls.ts
541
+ function controlPose(doc, values) {
542
+ const out = /* @__PURE__ */ new Map();
543
+ for (const [id, value] of Object.entries(values)) {
544
+ const c = doc.controls?.find((c2) => c2.id === id);
545
+ if (!c) throw new Error(`unknown control "${id}"`);
546
+ if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
547
+ out.set(`control:${id}:value`, Math.max(c.min, Math.min(c.max, value)));
548
+ }
549
+ return out;
550
+ }
551
+ function bracket(keys, value) {
552
+ let i = 0;
553
+ while (i < keys.length - 2 && value > keys[i + 1]) i++;
554
+ return [i, Math.max(0, Math.min(1, (value - keys[i]) / (keys[i + 1] - keys[i])))];
555
+ }
556
+ function sampleBinding(binding, values) {
557
+ const [x, u] = bracket(binding.axes[0].keys, values[0]);
558
+ const [y, v] = binding.axes[1] ? bracket(binding.axes[1].keys, values[1]) : [0, 0];
559
+ const width = binding.axes[0].keys.length, a = y * width + x;
560
+ const mix = (a2, b, u2) => a2 + (b - a2) * u2;
561
+ return binding.channels.map((ch) => {
562
+ const read = (i, j) => j === void 0 ? ch.values[i] : ch.values[i][j];
563
+ const at = (j) => {
564
+ const top = mix(read(a, j), read(a + 1, j), u);
565
+ return binding.axes[1] ? mix(top, mix(read(a + width, j), read(a + width + 1, j), u), v) : top;
566
+ };
567
+ return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j) => at(j)) : at();
568
+ });
569
+ }
570
+ function resolveControls(doc, input) {
571
+ if (!doc.controlBindings?.length) return input;
572
+ const out = new Map(input), controls = new Map(doc.controls?.map((c) => [c.id, c]));
573
+ for (const b of doc.controlBindings) {
574
+ const values = b.axes.map((axis) => {
575
+ const c = controls.get(axis.control);
576
+ const value = input.get(`control:${c.id}:value`) ?? c.default;
577
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c.id}" must be finite`);
578
+ return Math.max(c.min, Math.min(c.max, value));
579
+ });
580
+ const sampled = sampleBinding(b, values);
581
+ b.channels.forEach((ch, i) => out.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled[i]));
582
+ }
583
+ return out;
584
+ }
585
+
467
586
  // src/rig-anim.ts
468
587
  var DEG2 = Math.PI / 180;
469
588
  var named = {
@@ -472,6 +591,7 @@ var named = {
472
591
  ease_in: (t) => t * t,
473
592
  ease_out: (t) => 1 - (1 - t) * (1 - t),
474
593
  ease_in_out: (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2,
594
+ minimum_jerk: (t) => t * t * t * (10 + t * (-15 + 6 * t)),
475
595
  back_out: (t) => 1 + 2.70158 * (t - 1) ** 3 + 1.70158 * (t - 1) ** 2,
476
596
  elastic_out: (t) => t === 0 ? 0 : t === 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1
477
597
  };
@@ -503,7 +623,25 @@ function ease(spec) {
503
623
  return f;
504
624
  }
505
625
  var pk = (target, id, prop) => `${target}:${id}:${prop}`;
506
- function sample(track, t) {
626
+ function smoothValue(track, i, t, component, loop) {
627
+ const k = track.keys, n = k.length;
628
+ const v = (j) => component === void 0 ? k[j].v : k[j].v[component];
629
+ const h = (j) => k[j + 1].t - k[j].t;
630
+ const d = (j) => (v(j + 1) - v(j)) / h(j);
631
+ const harmonic = (a, b, h0, h1) => a * b <= 0 ? 0 : 3 * (h0 + h1) / ((2 * h1 + h0) / a + (h1 + 2 * h0) / b);
632
+ const tangent = (j) => {
633
+ if (j === 0 || j === n - 1) {
634
+ if (loop && n > 2 && v(0) === v(n - 1) && k[0].ease !== "step" && k[n - 2].ease !== "step")
635
+ return harmonic(d(n - 2), d(0), h(n - 2), h(0));
636
+ return 0;
637
+ }
638
+ if (k[j - 1].ease === "step" || k[j].ease === "step") return 0;
639
+ return harmonic(d(j - 1), d(j), h(j - 1), h(j));
640
+ };
641
+ const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;
642
+ 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);
643
+ }
644
+ function sample(track, t, loop = false) {
507
645
  const keys = track.keys;
508
646
  if (t <= keys[0].t) return keys[0].v;
509
647
  const last = keys[keys.length - 1];
@@ -511,36 +649,44 @@ function sample(track, t) {
511
649
  let i = 0;
512
650
  while (keys[i + 1].t <= t) i++;
513
651
  const a = keys[i], b = keys[i + 1];
652
+ if (a.ease === "step") return a.v;
514
653
  if (typeof a.v === "number" && typeof b.v === "number") {
654
+ if (track.interpolation === "pchip") return smoothValue(track, i, t, void 0, loop);
515
655
  const u = ease(a.ease)((t - a.t) / (b.t - a.t));
516
656
  return a.v + (b.v - a.v) * u;
517
657
  }
518
658
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
659
+ if (track.interpolation === "pchip") return a.v.map((_, j) => smoothValue(track, i, t, j, loop));
519
660
  const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
520
661
  return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
521
662
  }
522
663
  return a.v;
523
664
  }
524
665
  function poseOf(anim, t, out = /* @__PURE__ */ new Map()) {
525
- for (const tr of anim.tracks) out.set(pk(tr.target, tr.id, tr.prop), sample(tr, t));
666
+ for (const tr of anim.tracks) out.set(pk(tr.target, tr.id, tr.prop), sample(tr, t, anim.loop && tr.keys[0].t === 0 && tr.keys.at(-1).t === anim.duration));
526
667
  return out;
527
668
  }
528
669
  function blendPoses(from, to, w, restOf) {
529
- const out = new Map(to);
530
- for (const [k, f] of from) {
531
- const cur = to.get(k);
532
- if (typeof f === "number") {
670
+ if (w >= 1) return new Map(to);
671
+ const out = /* @__PURE__ */ new Map();
672
+ for (const k of /* @__PURE__ */ new Set([...from.keys(), ...to.keys()])) {
673
+ const f = from.has(k) ? from.get(k) : restOf(k);
674
+ const cur = to.has(k) ? to.get(k) : restOf(k);
675
+ if (typeof f === "number" && !Array.isArray(cur)) {
533
676
  const target = typeof cur === "number" ? cur : restOf(k);
534
677
  out.set(k, f + (target - f) * w);
535
678
  } else if (Array.isArray(f) && typeof f[0] === "number") {
536
679
  const fv = f, tv = Array.isArray(cur) && typeof cur[0] === "number" ? cur : fv.map(() => 0);
537
680
  out.set(k, fv.map((v, i) => v + ((tv[i] ?? 0) - v) * w));
538
- } else if (cur === void 0 && w < 1) out.set(k, f);
681
+ } else if (Array.isArray(cur) && typeof cur[0] === "number" && typeof f !== "string") {
682
+ out.set(k, cur.map((v) => v * w));
683
+ } else out.set(k, f ?? null);
539
684
  }
540
685
  return out;
541
686
  }
542
687
  function restValue(doc, key) {
543
688
  const [target, id, prop] = key.split(":");
689
+ if (target === "control") return doc.controls?.find((c) => c.id === id)?.default ?? 0;
544
690
  if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
545
691
  if (target === "slot") {
546
692
  const s = doc.slots.find((s2) => s2.id === id);
@@ -643,7 +789,9 @@ function updateSubtree(skel, root) {
643
789
  for (const k of kids.get(id) ?? []) stack.push(k.id);
644
790
  }
645
791
  }
646
- function evaluate(doc, pose) {
792
+ function evaluate(doc, pose, modifiers) {
793
+ pose = resolveControls(doc, pose);
794
+ if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
647
795
  const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
648
796
  for (const b of boneOrder(doc)) {
649
797
  const s = localOf(doc, pose, b);
@@ -704,10 +852,43 @@ function aimAt(skel, boneId, tx, ty, mix, compress = false, stretch = false, uni
704
852
  updateSubtree(skel, boneId);
705
853
  }
706
854
  var shortest = (deg) => ((deg + 180) % 360 + 360) % 360 - 180;
855
+ function applyLongIk(skel, c, tx, ty, mix) {
856
+ const initial = c.bones.map((id) => ({ ...skel.bones.get(id) }));
857
+ const last = c.bones.at(-1), len = boneOf(skel.doc, last).length;
858
+ const end = () => apply(skel.bones.get(last).world, len, 0);
859
+ const tolerance = c.tolerance ?? 0.05;
860
+ const root = skel.bones.get(c.bones[0]).world, tip = end();
861
+ if (Math.abs((tip[0] - root.tx) * (ty - root.ty) - (tip[1] - root.ty) * (tx - root.tx)) < 1e-7 && Math.hypot(tx - root.tx, ty - root.ty) < Math.hypot(tip[0] - root.tx, tip[1] - root.ty) - tolerance) {
862
+ skel.bones.get(last).rotation += cnum(skel, "ik", c.id, "bendPositive", c.bendPositive) > 0.5 ? 5 : -5;
863
+ updateSubtree(skel, last);
864
+ }
865
+ for (let iter = 0; iter < (c.iterations ?? 100); iter++) {
866
+ const e = end();
867
+ if (Math.hypot(e[0] - tx, e[1] - ty) <= tolerance) break;
868
+ for (let i = c.bones.length - 1; i >= 0; i--) {
869
+ const id = c.bones[i], b = boneOf(skel.doc, id), st = skel.bones.get(id), w = st.world, e2 = end();
870
+ const angle = wrapRad(Math.atan2(ty - w.ty, tx - w.tx) - Math.atan2(e2[1] - w.ty, e2[0] - w.tx));
871
+ const cos = Math.cos(angle), sin = Math.sin(angle);
872
+ setEffectiveWorld(skel, b, st, { ...w, a: cos * w.a - sin * w.b, b: sin * w.a + cos * w.b, c: cos * w.c - sin * w.d, d: sin * w.c + cos * w.d });
873
+ }
874
+ }
875
+ if (mix < 1) {
876
+ c.bones.forEach((id, i) => {
877
+ const st = skel.bones.get(id), from = initial[i];
878
+ for (const p of ["rotation", "shearX", "shearY"]) st[p] = from[p] + shortest(st[p] - from[p]) * mix;
879
+ for (const p of ["x", "y", "scaleX", "scaleY"]) st[p] = from[p] + (st[p] - from[p]) * mix;
880
+ });
881
+ updateSubtree(skel, c.bones[0]);
882
+ }
883
+ }
707
884
  function applyIk(skel, c) {
708
885
  const mix = cnum(skel, "ik", c.id, "mix", c.mix);
709
886
  if (mix <= 0) return;
710
887
  const target = skel.bones.get(c.target).world;
888
+ if (c.bones.length > 2) {
889
+ applyLongIk(skel, c, target.tx, target.ty, mix);
890
+ return;
891
+ }
711
892
  const bendDir = cnum(skel, "ik", c.id, "bendPositive", c.bendPositive) > 0.5 ? 1 : -1;
712
893
  const compress = cnum(skel, "ik", c.id, "compress", c.compress) > 0.5, stretch = cnum(skel, "ik", c.id, "stretch", c.stretch) > 0.5;
713
894
  let softness = cnum(skel, "ik", c.id, "softness", c.softness);
@@ -842,6 +1023,7 @@ function applyTransformConstraint(skel, c) {
842
1023
  const mixRotate = cnum(skel, "transform", c.id, "mixRotate", c.mixRotate), mixX = cnum(skel, "transform", c.id, "mixX", c.mixX), mixY = cnum(skel, "transform", c.id, "mixY", c.mixY);
843
1024
  const mixScaleX = cnum(skel, "transform", c.id, "mixScaleX", c.mixScaleX), mixScaleY = cnum(skel, "transform", c.id, "mixScaleY", c.mixScaleY), mixShearY = cnum(skel, "transform", c.id, "mixShearY", c.mixShearY);
844
1025
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
1026
+ c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
845
1027
  const target = skel.bones.get(c.target), tw = target.world;
846
1028
  const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
847
1029
  for (const id of c.bones) {
@@ -1169,6 +1351,7 @@ function regionSize(doc, att) {
1169
1351
  var AnimationState = class {
1170
1352
  constructor(sink, opts = {}) {
1171
1353
  this.sink = sink;
1354
+ this.opts = opts;
1172
1355
  this.defaultMix = opts.mix ?? 0;
1173
1356
  }
1174
1357
  onEvent;
@@ -1193,11 +1376,18 @@ var AnimationState = class {
1193
1376
  }
1194
1377
  channel(id, loop, priority = 0) {
1195
1378
  const anim = this.sink.doc.animations.find((a) => a.id === id);
1196
- if (!anim) throw new Error(`unknown animation "${id}"`);
1379
+ if (!anim) {
1380
+ const message = `unknown animation "${id}"; available: ${this.sink.doc.animations.map((a) => a.id).join(", ")}`;
1381
+ if (this.opts.missingAnimation === "throw") throw new Error(message);
1382
+ (this.opts.onWarning ?? console.warn)(message);
1383
+ return void 0;
1384
+ }
1197
1385
  return { anim, time: 0, loop: loop ?? anim.loop, done: false, priority, fresh: true };
1198
1386
  }
1199
1387
  setBase(id, opts = {}) {
1200
- this.base = id === void 0 ? void 0 : this.channel(id);
1388
+ const next = id === void 0 ? void 0 : this.channel(id);
1389
+ if (id !== void 0 && !next) return;
1390
+ this.base = next;
1201
1391
  if (!this.main || this.main.done) {
1202
1392
  this.main = void 0;
1203
1393
  this.beginMix(opts.mix);
@@ -1206,6 +1396,16 @@ var AnimationState = class {
1206
1396
  }
1207
1397
  play(id, opts = {}) {
1208
1398
  const ch = this.channel(id, opts.loop, opts.priority ?? 0);
1399
+ if (!ch) {
1400
+ this.main?.settle?.("interrupted");
1401
+ for (const queued of this.queue) queued.settle?.("dropped");
1402
+ this.main = void 0;
1403
+ this.queue = [];
1404
+ this.beginMix(opts.mix);
1405
+ this.update(0);
1406
+ opts.onComplete?.("dropped");
1407
+ return Promise.resolve("dropped");
1408
+ }
1209
1409
  const promise = new Promise((resolve) => {
1210
1410
  ch.settle = (r) => {
1211
1411
  ch.settle = void 0;
@@ -1226,7 +1426,10 @@ var AnimationState = class {
1226
1426
  /** Independent loop on top of the shown pose; `undefined` removes it. */
1227
1427
  setOverlay(name, id) {
1228
1428
  if (id === void 0) this.overlays.delete(name);
1229
- else this.overlays.set(name, this.channel(id));
1429
+ else {
1430
+ const ch = this.channel(id);
1431
+ if (ch) this.overlays.set(name, ch);
1432
+ }
1230
1433
  }
1231
1434
  /** Clear everything and show the setup pose. Pending plays resolve as "stopped". */
1232
1435
  stop() {
@@ -1351,7 +1554,7 @@ function atlasUV(asset, u, v, sourceW, sourceH) {
1351
1554
  var rgbOf = (c) => Math.round(c[0] * 255) << 16 | Math.round(c[1] * 255) << 8 | Math.round(c[2] * 255);
1352
1555
  var sameNumbers = (a, b) => a === b || a.length === b.length && a.every((v, i) => v === b[i]);
1353
1556
  var RigPlayer = class {
1354
- /** `mix`: default crossfade seconds for state transitions (setBase, completion → base); play() passes its own */
1557
+ /** Default crossfade is 0.18 seconds; explicit mix: 0 requests a hard cut. */
1355
1558
  constructor(doc, textures, opts = {}) {
1356
1559
  this.doc = doc;
1357
1560
  parse(doc);
@@ -1390,12 +1593,26 @@ var RigPlayer = class {
1390
1593
  this.items.push(item);
1391
1594
  this.view.addChild(item.node);
1392
1595
  }
1393
- this.state = new AnimationState(this, { mix: opts.mix ?? 0 });
1596
+ this.state = new AnimationState(this, { ...opts, mix: opts.mix ?? 0.18 });
1394
1597
  this.setPose(/* @__PURE__ */ new Map());
1395
1598
  }
1599
+ inputPose = /* @__PURE__ */ new Map();
1600
+ controlOverrides = /* @__PURE__ */ new Map();
1601
+ /** Manual inputs override animated controls until resetControls; values are clamped to declared ranges. */
1602
+ setControls(values) {
1603
+ this.controlOverrides = controlPose(this.doc, values);
1604
+ this.setPose(this.inputPose);
1605
+ }
1606
+ resetControls() {
1607
+ this.controlOverrides.clear();
1608
+ this.setPose(this.inputPose);
1609
+ }
1396
1610
  view = new Container();
1397
1611
  state;
1398
1612
  items = [];
1613
+ beforePose = /* @__PURE__ */ new Set();
1614
+ afterPose = /* @__PURE__ */ new Set();
1615
+ clipPoints = /* @__PURE__ */ new Map();
1399
1616
  skel;
1400
1617
  textures;
1401
1618
  pages = /* @__PURE__ */ new Map();
@@ -1403,6 +1620,36 @@ var RigPlayer = class {
1403
1620
  hidden = /* @__PURE__ */ new Set();
1404
1621
  /** slots the editor has selected; setPose draws them at GHOST_ALPHA at least */
1405
1622
  ghosts = /* @__PURE__ */ new Set();
1623
+ clipGraphics = /* @__PURE__ */ new Map();
1624
+ /** Read-only render handles for in-page tools. Nodes remain owned by the player. */
1625
+ get renderItems() {
1626
+ return this.items;
1627
+ }
1628
+ /** Current document-space clipping polygons, keyed by path attachment id. */
1629
+ get clipPolygons() {
1630
+ return this.clipPoints;
1631
+ }
1632
+ /**
1633
+ * Modify resolved control outputs before skeleton evaluation, constraints and skinning.
1634
+ * Callbacks run in registration order, each with its own map. Change control inputs via setControls.
1635
+ */
1636
+ onBeforePose(callback) {
1637
+ this.beforePose.add(callback);
1638
+ return () => {
1639
+ this.beforePose.delete(callback);
1640
+ };
1641
+ }
1642
+ /** Subscribe after the base pose has been rebuilt. Return value removes the callback. */
1643
+ onAfterPose(callback) {
1644
+ this.afterPose.add(callback);
1645
+ return () => {
1646
+ this.afterPose.delete(callback);
1647
+ };
1648
+ }
1649
+ /** Rebuild the current pose without advancing playback; useful for reversible overlays. */
1650
+ refreshPose() {
1651
+ this.setPose(this.inputPose);
1652
+ }
1406
1653
  /** whole atlas page as a plain texture (identity uv mapping) for skinned meshes */
1407
1654
  pageTexture(tex) {
1408
1655
  let page = this.pages.get(tex.source);
@@ -1414,7 +1661,7 @@ var RigPlayer = class {
1414
1661
  this.state.onEvent = opts.onEvent;
1415
1662
  return this.state.play(animId, {
1416
1663
  loop: opts.loop,
1417
- mix: opts.mix ?? 0,
1664
+ mix: opts.mix,
1418
1665
  ifBusy: "interrupt",
1419
1666
  onComplete: (r) => {
1420
1667
  if (r === "completed") opts.onComplete?.();
@@ -1457,13 +1704,39 @@ var RigPlayer = class {
1457
1704
  }
1458
1705
  /** Apply a pose (rest where silent) to the display objects. */
1459
1706
  setPose(pose) {
1460
- const skel = this.skel = evaluate(this.doc, pose);
1707
+ this.inputPose = new Map(pose);
1708
+ if (this.controlOverrides.size) pose = new Map([...pose, ...this.controlOverrides]);
1709
+ this.clipPoints.clear();
1710
+ const skel = this.skel = evaluate(this.doc, pose, this.beforePose);
1711
+ pose = skel.pose;
1712
+ const clips = new Set(this.doc.slots.flatMap((s) => s.clip ? [s.clip] : []));
1713
+ for (const [id, g] of this.clipGraphics) if (!clips.has(id)) {
1714
+ g.destroy();
1715
+ this.clipGraphics.delete(id);
1716
+ }
1717
+ for (const id of clips) {
1718
+ const att = this.doc.attachments.find((a) => a.id === id);
1719
+ if (att?.type !== "path" || !att.closed) continue;
1720
+ let g = this.clipGraphics.get(id);
1721
+ if (!g) {
1722
+ g = new Graphics();
1723
+ this.clipGraphics.set(id, g);
1724
+ this.view.addChild(g);
1725
+ }
1726
+ const vertices = attachmentWorldVertices(skel, att.slot, att);
1727
+ const n = Math.max(32, vertices.length * 4);
1728
+ const points = samplePath(vertices, true, att.lengths, true, Array.from({ length: n }, (_, i) => i / n), true);
1729
+ const polygon = points.flatMap((p) => [p.x, p.y]);
1730
+ this.clipPoints.set(id, polygon);
1731
+ g.clear().poly(polygon, true).fill(16777215);
1732
+ }
1461
1733
  const m = new Matrix();
1462
1734
  const orderV = pose.get(pk("drawOrder", "order", "order"));
1463
1735
  const order = /* @__PURE__ */ new Map();
1464
1736
  if (Array.isArray(orderV)) orderV.forEach((id, i) => order.set(id, i));
1465
1737
  for (const it of this.items) {
1466
1738
  const s = it.slot, shown = skel.attachment.get(s.id);
1739
+ it.node.mask = s.clip ? this.clipGraphics.get(s.clip) ?? null : null;
1467
1740
  if (shown !== it.att || this.hidden.has(s.id)) {
1468
1741
  it.node.visible = false;
1469
1742
  continue;
@@ -1476,6 +1749,7 @@ var RigPlayer = class {
1476
1749
  it.node.tint = rgbOf(color);
1477
1750
  it.node.zIndex = order.size ? order.get(s.id) ?? 0 : numOf(pose, pk("slot", s.id, "z"), s.z);
1478
1751
  if (it.skinned) {
1752
+ it.node.setFromMatrix(m.identity());
1479
1753
  const v = attachmentWorldVertices(skel, s.id, it.att);
1480
1754
  const buf = it.skinned.mesh.geometry.getBuffer("aPosition");
1481
1755
  buf.data.set(v);
@@ -1508,6 +1782,7 @@ var RigPlayer = class {
1508
1782
  it.node.scale.y *= h / (this.textures.get(att.asset)?.height ?? h);
1509
1783
  }
1510
1784
  }
1785
+ for (const callback of this.afterPose) callback(this);
1511
1786
  }
1512
1787
  /** World-space bone positions — used by editor gizmos and renderPreview overlays */
1513
1788
  bonePositions() {
@@ -1584,9 +1859,12 @@ export {
1584
1859
  RigPlayer,
1585
1860
  RigSymbol,
1586
1861
  atlasUV,
1862
+ controlPose,
1587
1863
  createTextures,
1588
1864
  migrateV1,
1589
1865
  parse,
1866
+ resolveControls,
1867
+ sampleBinding,
1590
1868
  validate
1591
1869
  };
1592
1870
  //# sourceMappingURL=runtime.js.map