@energy8platform/golem 0.3.0 → 0.6.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.
@@ -50,17 +50,17 @@ export declare const GateConfig: z.ZodObject<{
50
50
  }, "strip", z.ZodTypeAny, {
51
51
  b: string;
52
52
  a: string;
53
- during?: [number, number] | undefined;
54
53
  min?: number | undefined;
55
54
  max?: number | undefined;
55
+ during?: [number, number] | undefined;
56
56
  minRatio?: number | undefined;
57
57
  maxRatio?: number | undefined;
58
58
  }, {
59
59
  b: string;
60
60
  a: string;
61
- during?: [number, number] | undefined;
62
61
  min?: number | undefined;
63
62
  max?: number | undefined;
63
+ during?: [number, number] | undefined;
64
64
  minRatio?: number | undefined;
65
65
  maxRatio?: number | undefined;
66
66
  }>, "many">>;
@@ -84,9 +84,9 @@ export declare const GateConfig: z.ZodObject<{
84
84
  bone: string;
85
85
  prop: "x" | "y" | "rotation";
86
86
  whenSlotVisible: string;
87
- space?: "local" | "world" | undefined;
88
87
  min?: number | undefined;
89
88
  max?: number | undefined;
89
+ space?: "local" | "world" | undefined;
90
90
  required?: boolean | undefined;
91
91
  }>, "many">>;
92
92
  transitions: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -130,9 +130,9 @@ export declare const GateConfig: z.ZodObject<{
130
130
  distances: {
131
131
  b: string;
132
132
  a: string;
133
- during?: [number, number] | undefined;
134
133
  min?: number | undefined;
135
134
  max?: number | undefined;
135
+ during?: [number, number] | undefined;
136
136
  minRatio?: number | undefined;
137
137
  maxRatio?: number | undefined;
138
138
  }[];
@@ -172,9 +172,9 @@ export declare const GateConfig: z.ZodObject<{
172
172
  distances?: {
173
173
  b: string;
174
174
  a: string;
175
- during?: [number, number] | undefined;
176
175
  min?: number | undefined;
177
176
  max?: number | undefined;
177
+ during?: [number, number] | undefined;
178
178
  minRatio?: number | undefined;
179
179
  maxRatio?: number | undefined;
180
180
  }[] | undefined;
@@ -182,9 +182,9 @@ export declare const GateConfig: z.ZodObject<{
182
182
  bone: string;
183
183
  prop: "x" | "y" | "rotation";
184
184
  whenSlotVisible: string;
185
- space?: "local" | "world" | undefined;
186
185
  min?: number | undefined;
187
186
  max?: number | undefined;
187
+ space?: "local" | "world" | undefined;
188
188
  required?: boolean | undefined;
189
189
  }[] | undefined;
190
190
  transitions?: {
@@ -8,8 +8,8 @@
8
8
  * - play/tick/stop are a thin wrapper over an AnimationState (see rig-state.ts) kept for simple consumers
9
9
  * (harness, viewer). Games should drive `state` directly for queues, priorities and overlays.
10
10
  */
11
- import { Container, Texture } from "pixi.js";
12
- import { type RigDocument, type Asset } from "./rig-format";
11
+ import { Container, Sprite, Mesh, MeshPlane, Texture } from "pixi.js";
12
+ import { type RigDocument, type Slot, type Attachment, type Asset } from "./rig-format";
13
13
  import { type Pose, type Affine } from "./rig-anim";
14
14
  import { AnimationState, type PlayResult } from "./rig-state";
15
15
  /** vertices per axis of a lattice-deformed region's render mesh (8 segments) */
@@ -23,6 +23,22 @@ export interface PlayOptions {
23
23
  onComplete?: () => void;
24
24
  onEvent?: (name: string, payload?: Record<string, unknown>) => void;
25
25
  }
26
+ export interface RigRenderItem {
27
+ att: Attachment;
28
+ slot: Slot;
29
+ node: Sprite | MeshPlane | Mesh;
30
+ /** lattice-deformed region */
31
+ lattice?: {
32
+ plane: MeshPlane;
33
+ offsets: Float64Array;
34
+ applied: Float64Array;
35
+ };
36
+ /** skinned mesh */
37
+ skinned?: {
38
+ mesh: Mesh;
39
+ positions: Float32Array;
40
+ };
41
+ }
26
42
  /** Build the asset-id → Texture map, loading each `src` once and cutting atlas frames (with rotation and trim). */
27
43
  export declare function createTextures(doc: RigDocument, loadSource: (src: string) => Promise<Texture>): Promise<Map<string, Texture>>;
28
44
  /**
@@ -32,9 +48,17 @@ export declare function createTextures(doc: RigDocument, loadSource: (src: strin
32
48
  export declare function atlasUV(asset: Asset, u: number, v: number, sourceW: number, sourceH: number): [number, number];
33
49
  export declare class RigPlayer {
34
50
  doc: RigDocument;
51
+ private inputPose;
52
+ private controlOverrides;
53
+ /** Manual inputs override animated controls until resetControls; values are clamped to declared ranges. */
54
+ setControls(values: Record<string, number>): void;
55
+ resetControls(): void;
35
56
  readonly view: Container<import("pixi.js").ContainerChild>;
36
57
  readonly state: AnimationState;
37
58
  private items;
59
+ private beforePose;
60
+ private afterPose;
61
+ private clipPoints;
38
62
  private skel;
39
63
  private textures;
40
64
  private pages;
@@ -49,6 +73,19 @@ export declare class RigPlayer {
49
73
  missingAnimation?: "warn" | "throw";
50
74
  onWarning?: (message: string) => void;
51
75
  });
76
+ /** Read-only render handles for in-page tools. Nodes remain owned by the player. */
77
+ get renderItems(): readonly Readonly<RigRenderItem>[];
78
+ /** Current document-space clipping polygons, keyed by path attachment id. */
79
+ get clipPolygons(): ReadonlyMap<string, readonly number[]>;
80
+ /**
81
+ * Modify resolved control outputs before skeleton evaluation, constraints and skinning.
82
+ * Callbacks run in registration order, each with its own map. Change control inputs via setControls.
83
+ */
84
+ onBeforePose(callback: (pose: Pose) => Pose): () => void;
85
+ /** Subscribe after the base pose has been rebuilt. Return value removes the callback. */
86
+ onAfterPose(callback: (player: RigPlayer) => void): () => void;
87
+ /** Rebuild the current pose without advancing playback; useful for reversible overlays. */
88
+ refreshPose(): void;
52
89
  /** whole atlas page as a plain texture (identity uv mapping) for skinned meshes */
53
90
  private pageTexture;
54
91
  /** Replace whatever is playing (no queueing). Resolves when the animation completes or is replaced. */
@@ -2,8 +2,10 @@
2
2
  * `@energy8platform/golem/runtime` — the browser half: play a rig document in PixiJS v8.
3
3
  * Depends on pixi.js (peer) and zod only; no node, no tools, no editor.
4
4
  */
5
- export { RigPlayer, createTextures, atlasUV, MESH_GRID, type PlayOptions } from "./rig-runtime";
5
+ export { RigPlayer, createTextures, atlasUV, MESH_GRID, type PlayOptions, type RigRenderItem } from "./rig-runtime";
6
6
  export { RigSymbol, type RigSymbolOptions, type SymbolSize, type SymbolViewLike, type TickerLike } from "./rig-symbol";
7
7
  export { AnimationState, type PlayResult, type PoseSink } from "./rig-state";
8
8
  export { parse, validate, migrateV1, RigDocument as RigDocumentSchema } from "./rig-format";
9
9
  export type { RigDocument, Bone, Slot, Asset, Attachment, Animation, Track, Key } from "./rig-format";
10
+ export type { RigControl, ControlBinding } from "./rig-format";
11
+ export { controlPose, resolveControls, sampleBinding } from "./rig-controls";
@@ -159,7 +159,9 @@ var PathAttachment = z.object({
159
159
  /** curve lengths for constant-speed positioning */
160
160
  lengths: z.array(z.number()).default([]),
161
161
  closed: z.boolean().default(false),
162
- constantSpeed: z.boolean().default(true)
162
+ constantSpeed: z.boolean().default(true),
163
+ /** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
164
+ clipping: z.object({ end: Id.optional() }).optional()
163
165
  });
164
166
  var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
165
167
  var IkConstraint = z.object({
@@ -223,10 +225,13 @@ var Key = z.object({
223
225
  t: z.number().nonnegative(),
224
226
  v: KeyValue,
225
227
  /** easing INTO the next key */
226
- ease: Ease.default("ease_in_out")
228
+ ease: Ease.default("ease_in_out"),
229
+ /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
230
+ componentEase: z.array(Ease).min(1).optional()
227
231
  });
228
232
  var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
229
233
  var Track = z.discriminatedUnion("target", [
234
+ z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
230
235
  z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
231
236
  z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
232
237
  z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
@@ -249,6 +254,25 @@ var Animation = z.object({
249
254
  /** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
250
255
  preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
251
256
  });
257
+ var RigControl = z.object({
258
+ id: Id,
259
+ label: z.string().optional(),
260
+ min: z.number().finite(),
261
+ max: z.number().finite(),
262
+ default: z.number().finite()
263
+ }).strict();
264
+ var ControlValues = z.array(z.union([z.number().finite(), z.array(z.number().finite()).min(1)])).min(2);
265
+ var ControlChannel = z.discriminatedUnion("target", [
266
+ z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
267
+ z.object({ target: z.literal("slot"), id: Id, prop: z.literal("alpha"), values: ControlValues }).strict(),
268
+ z.object({ target: z.literal("deform"), id: Id, prop: z.literal("vertices"), values: ControlValues }).strict()
269
+ ]);
270
+ var ControlBinding = z.object({
271
+ id: Id,
272
+ axes: z.array(z.object({ control: Id, keys: z.array(z.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
273
+ /** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
274
+ channels: z.array(ControlChannel).min(1)
275
+ }).strict();
252
276
  var Reference = z.object({
253
277
  src: z.string(),
254
278
  opacity: z.number().min(0).max(1).default(0.5),
@@ -274,7 +298,9 @@ var RigSchema = z.object({
274
298
  ik: z.array(IkConstraint).default([]),
275
299
  transform: z.array(TransformConstraint).default([]),
276
300
  path: z.array(PathConstraint).default([]),
277
- animations: z.array(Animation)
301
+ animations: z.array(Animation),
302
+ controls: z.array(RigControl).optional(),
303
+ controlBindings: z.array(ControlBinding).optional()
278
304
  });
279
305
  var RigDocument = z.preprocess((input) => {
280
306
  const v = input?.version;
@@ -349,6 +375,8 @@ function validate(doc) {
349
375
  dup(doc.ik, "ik");
350
376
  dup(doc.transform, "transform");
351
377
  dup(doc.path, "path");
378
+ dup(doc.controls ?? [], "control");
379
+ dup(doc.controlBindings ?? [], "control binding");
352
380
  const bones = new Set(doc.bones.map((b) => b.id));
353
381
  const assets = new Map(doc.assets.map((a) => [a.id, a]));
354
382
  const slots = new Set(doc.slots.map((s) => s.id));
@@ -386,6 +414,10 @@ function validate(doc) {
386
414
  else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
387
415
  }
388
416
  }
417
+ for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
418
+ if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
419
+ if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
420
+ }
389
421
  const vertexCount = (a) => {
390
422
  if (a.type === "region") return 4;
391
423
  if (a.type === "mesh") return a.uvs.length / 2;
@@ -423,20 +455,59 @@ function validate(doc) {
423
455
  for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
424
456
  if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
425
457
  }
458
+ const controls = new Map((doc.controls ?? []).map((c) => [c.id, c]));
459
+ const driven = /* @__PURE__ */ new Set();
460
+ for (const c of controls.values()) if (!(c.min < c.max && c.default >= c.min && c.default <= c.max))
461
+ errs.push(`control "${c.id}": require min < max and default within range`);
462
+ for (const b of doc.controlBindings ?? []) {
463
+ const prefix = `control binding "${b.id}"`;
464
+ if (new Set(b.axes.map((a) => a.control)).size !== b.axes.length) errs.push(`${prefix}: axes must use different controls`);
465
+ for (const axis of b.axes) {
466
+ const c = controls.get(axis.control);
467
+ if (!c) errs.push(`${prefix}: unknown control "${axis.control}"`);
468
+ else if (axis.keys[0] !== c.min || axis.keys.at(-1) !== c.max) errs.push(`${prefix}: keys must span the control's full range`);
469
+ if (axis.keys.some((v, i) => i > 0 && v <= axis.keys[i - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
470
+ }
471
+ const count = b.axes.reduce((n, a) => n * a.keys.length, 1);
472
+ for (const ch of b.channels) {
473
+ const key = `${ch.target}.${ch.id}.${ch.prop}`;
474
+ if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
475
+ driven.add(key);
476
+ if (ch.values.length !== count) errs.push(`${prefix}: ${key} needs ${count} grid values`);
477
+ if (ch.target === "deform") {
478
+ const att = attachments.get(ch.id);
479
+ if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
480
+ else {
481
+ const n = att.weights ? att.weights.reduce((s, a) => s + a.length * 2, 0) : att.vertices.length;
482
+ if (ch.values.some((v) => !Array.isArray(v) || v.length !== n)) errs.push(`${prefix}: ${key} needs arrays of ${n} offsets`);
483
+ }
484
+ } else {
485
+ if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
486
+ if (ch.values.some((v) => typeof v !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
487
+ if (ch.target === "slot" && ch.values.some((v) => typeof v === "number" && (v < 0 || v > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
488
+ }
489
+ }
490
+ }
426
491
  for (const a of doc.animations) {
427
492
  const tracks = /* @__PURE__ */ new Set();
428
493
  for (const t of a.tracks) {
429
494
  const tid = trackId(t);
430
495
  if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
431
496
  tracks.add(tid);
432
- 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;
497
+ if (driven.has(tid)) errs.push(`animation "${a.id}": ${tid} is controlled by a binding; animate its control instead`);
498
+ 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;
433
499
  if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
434
500
  const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
435
501
  const att = t.target === "deform" ? attachments.get(t.id) : void 0;
436
502
  let prev = -1;
437
503
  for (const k of t.keys) {
504
+ if (k.componentEase && (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number") || k.componentEase.length !== k.v.length)) errs.push(`animation "${a.id}" ${tid}: componentEase must match the numeric-array value length`);
438
505
  if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
439
506
  if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
507
+ if (t.target === "control") {
508
+ const c = controls.get(t.id);
509
+ 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`);
510
+ }
440
511
  if (t.target === "slot" && t.prop === "attachment") {
441
512
  if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
442
513
  else if (typeof k.v === "string") {
@@ -475,6 +546,52 @@ function parse(input) {
475
546
  return doc;
476
547
  }
477
548
 
549
+ // src/rig-controls.ts
550
+ function controlPose(doc, values) {
551
+ const out = /* @__PURE__ */ new Map();
552
+ for (const [id, value] of Object.entries(values)) {
553
+ const c = doc.controls?.find((c2) => c2.id === id);
554
+ if (!c) throw new Error(`unknown control "${id}"`);
555
+ if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
556
+ out.set(`control:${id}:value`, Math.max(c.min, Math.min(c.max, value)));
557
+ }
558
+ return out;
559
+ }
560
+ function bracket(keys, value) {
561
+ let i = 0;
562
+ while (i < keys.length - 2 && value > keys[i + 1]) i++;
563
+ return [i, Math.max(0, Math.min(1, (value - keys[i]) / (keys[i + 1] - keys[i])))];
564
+ }
565
+ function sampleBinding(binding, values) {
566
+ const [x, u] = bracket(binding.axes[0].keys, values[0]);
567
+ const [y, v] = binding.axes[1] ? bracket(binding.axes[1].keys, values[1]) : [0, 0];
568
+ const width = binding.axes[0].keys.length, a = y * width + x;
569
+ const mix = (a2, b, u2) => a2 + (b - a2) * u2;
570
+ return binding.channels.map((ch) => {
571
+ const read = (i, j) => j === void 0 ? ch.values[i] : ch.values[i][j];
572
+ const at = (j) => {
573
+ const top = mix(read(a, j), read(a + 1, j), u);
574
+ return binding.axes[1] ? mix(top, mix(read(a + width, j), read(a + width + 1, j), u), v) : top;
575
+ };
576
+ return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j) => at(j)) : at();
577
+ });
578
+ }
579
+ function resolveControls(doc, input) {
580
+ if (!doc.controlBindings?.length) return input;
581
+ const out = new Map(input), controls = new Map(doc.controls?.map((c) => [c.id, c]));
582
+ for (const b of doc.controlBindings) {
583
+ const values = b.axes.map((axis) => {
584
+ const c = controls.get(axis.control);
585
+ const value = input.get(`control:${c.id}:value`) ?? c.default;
586
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c.id}" must be finite`);
587
+ return Math.max(c.min, Math.min(c.max, value));
588
+ });
589
+ const sampled = sampleBinding(b, values);
590
+ b.channels.forEach((ch, i) => out.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled[i]));
591
+ }
592
+ return out;
593
+ }
594
+
478
595
  // src/rig-anim.ts
479
596
  var DEG2 = Math.PI / 180;
480
597
  var named = {
@@ -549,8 +666,8 @@ function sample(track, t, loop = false) {
549
666
  }
550
667
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
551
668
  if (track.interpolation === "pchip") return a.v.map((_, j) => smoothValue(track, i, t, j, loop));
552
- const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
553
- return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
669
+ const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
670
+ return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
554
671
  }
555
672
  return a.v;
556
673
  }
@@ -578,6 +695,7 @@ function blendPoses(from, to, w, restOf) {
578
695
  }
579
696
  function restValue(doc, key) {
580
697
  const [target, id, prop] = key.split(":");
698
+ if (target === "control") return doc.controls?.find((c) => c.id === id)?.default ?? 0;
581
699
  if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
582
700
  if (target === "slot") {
583
701
  const s = doc.slots.find((s2) => s2.id === id);
@@ -637,6 +755,13 @@ function boneWorldOf(bone, s, parent) {
637
755
  if (!parent) return local;
638
756
  if (bone.inherit === "normal") return mul(parent, local);
639
757
  const [tx, ty] = apply(parent, s.x, s.y);
758
+ if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
759
+ const r = s.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
760
+ const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
761
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
762
+ const basis2 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
763
+ return { ...mul(basis2, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
764
+ }
640
765
  return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
641
766
  }
642
767
  var orderCache = /* @__PURE__ */ new WeakMap();
@@ -669,18 +794,20 @@ var localOf = (doc, pose, b) => {
669
794
  shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
670
795
  };
671
796
  };
672
- function updateSubtree(skel, root) {
797
+ function updateSubtree(skel, root, includeRoot = true) {
673
798
  const doc = skel.doc;
674
799
  const kids = /* @__PURE__ */ new Map();
675
800
  for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
676
- const stack = [root];
801
+ const stack = includeRoot ? [root] : (kids.get(root) ?? []).map((b) => b.id);
677
802
  while (stack.length) {
678
803
  const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
679
804
  st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
680
805
  for (const k of kids.get(id) ?? []) stack.push(k.id);
681
806
  }
682
807
  }
683
- function evaluate(doc, pose) {
808
+ function evaluate(doc, pose, modifiers) {
809
+ pose = resolveControls(doc, pose);
810
+ if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
684
811
  const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
685
812
  for (const b of boneOrder(doc)) {
686
813
  const s = localOf(doc, pose, b);
@@ -705,6 +832,23 @@ function evaluate(doc, pose) {
705
832
  }
706
833
  return skel;
707
834
  }
835
+ function slotClips(skel) {
836
+ const keyed = skel.pose.get(pk("drawOrder", "order", "order"));
837
+ const order = Array.isArray(keyed) ? keyed : [...skel.doc.slots].sort((a, b) => a.z - b.z).map((s) => s.id);
838
+ const masks = /* @__PURE__ */ new Map();
839
+ let active;
840
+ for (const id of order) {
841
+ const att = skel.attachment.get(id);
842
+ if (!active && att?.type === "path" && att.clipping) {
843
+ active = att;
844
+ continue;
845
+ }
846
+ if (active) masks.set(id, active.id);
847
+ if (active?.clipping?.end === id) active = void 0;
848
+ }
849
+ for (const s of skel.doc.slots) if (s.clip) masks.set(s.id, s.clip);
850
+ return masks;
851
+ }
708
852
  var cnum = (skel, kind, id, prop, rest) => num(skel.pose, pk(kind, id, prop), typeof rest === "boolean" ? rest ? 1 : 0 : rest);
709
853
  var IDENTITY = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
710
854
  var wrapRad = (r) => r > Math.PI ? r - 2 * Math.PI : r < -Math.PI ? r + 2 * Math.PI : r;
@@ -914,7 +1058,8 @@ function applyTransformConstraint(skel, c) {
914
1058
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
915
1059
  c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
916
1060
  const target = skel.bones.get(c.target), tw = target.world;
917
- const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
1061
+ const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
1062
+ const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
918
1063
  for (const id of c.bones) {
919
1064
  const b = boneOf(skel.doc, id), st = skel.bones.get(id);
920
1065
  if (c.local) {
@@ -1076,6 +1221,7 @@ function applyPathConstraint(skel, c) {
1076
1221
  const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
1077
1222
  const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
1078
1223
  const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
1224
+ const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
1079
1225
  const spacesCount = tangents ? bones.length : bones.length + 1;
1080
1226
  const spaces = new Array(spacesCount).fill(0), worldLengths = [];
1081
1227
  let sum = 0;
@@ -1110,9 +1256,10 @@ function applyPathConstraint(skel, c) {
1110
1256
  if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
1111
1257
  }
1112
1258
  let boneX = samples[0].x, boneY = samples[0].y;
1259
+ const solvedWorlds = /* @__PURE__ */ new Map();
1113
1260
  for (let i = 0; i < bones.length; i++) {
1114
1261
  const { b, st } = bones[i];
1115
- let w = { ...st.world };
1262
+ let w = { ...inputWorlds[i] };
1116
1263
  w.tx += (boneX - w.tx) * mixX;
1117
1264
  w.ty += (boneY - w.ty) * mixY;
1118
1265
  const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
@@ -1142,7 +1289,11 @@ function applyPathConstraint(skel, c) {
1142
1289
  const cos = Math.cos(r), sin = Math.sin(r);
1143
1290
  w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
1144
1291
  }
1145
- setEffectiveWorld(skel, b, st, w);
1292
+ solvedWorlds.set(b.id, w);
1293
+ }
1294
+ for (const b of boneOrder(skel.doc)) {
1295
+ const world = solvedWorlds.get(b.id);
1296
+ if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
1146
1297
  }
1147
1298
  }
1148
1299
  function measuredPathLength(points, closed, lengths, constantSpeed) {
@@ -1171,9 +1322,14 @@ function inheritedParentLinear(bone, parent) {
1171
1322
  case "onlyTranslation":
1172
1323
  return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
1173
1324
  case "noRotationOrReflection": {
1174
- const lenX = Math.hypot(parent.a, parent.b) || 1;
1325
+ const lenX = Math.hypot(parent.a, parent.b);
1175
1326
  const det = parent.a * parent.d - parent.b * parent.c;
1176
- return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };
1327
+ if (lenX * lenX <= 1e-4) {
1328
+ const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
1329
+ return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
1330
+ }
1331
+ const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
1332
+ return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };
1177
1333
  }
1178
1334
  case "noScale":
1179
1335
  case "noScaleOrReflection": {
@@ -1199,7 +1355,16 @@ function setEffectiveWorld(skel, bone, st, world) {
1199
1355
  st.scaleY = Math.hypot(lin.c, lin.d);
1200
1356
  st.shearX = 0;
1201
1357
  st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
1202
- updateSubtree(skel, bone.id);
1358
+ if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
1359
+ const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
1360
+ st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
1361
+ st.scaleX = Math.hypot(world.a, world.b);
1362
+ st.scaleY = Math.hypot(world.c, world.d);
1363
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
1364
+ st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
1365
+ }
1366
+ st.world = world;
1367
+ updateSubtree(skel, bone.id, false);
1203
1368
  }
1204
1369
  function attachmentWorldVertices(skel, slotId, att) {
1205
1370
  const deform = skel.pose.get(pk("deform", att.id, "vertices"));
@@ -1485,9 +1650,23 @@ var RigPlayer = class {
1485
1650
  this.state = new AnimationState(this, { ...opts, mix: opts.mix ?? 0.18 });
1486
1651
  this.setPose(/* @__PURE__ */ new Map());
1487
1652
  }
1653
+ inputPose = /* @__PURE__ */ new Map();
1654
+ controlOverrides = /* @__PURE__ */ new Map();
1655
+ /** Manual inputs override animated controls until resetControls; values are clamped to declared ranges. */
1656
+ setControls(values) {
1657
+ this.controlOverrides = controlPose(this.doc, values);
1658
+ this.setPose(this.inputPose);
1659
+ }
1660
+ resetControls() {
1661
+ this.controlOverrides.clear();
1662
+ this.setPose(this.inputPose);
1663
+ }
1488
1664
  view = new Container();
1489
1665
  state;
1490
1666
  items = [];
1667
+ beforePose = /* @__PURE__ */ new Set();
1668
+ afterPose = /* @__PURE__ */ new Set();
1669
+ clipPoints = /* @__PURE__ */ new Map();
1491
1670
  skel;
1492
1671
  textures;
1493
1672
  pages = /* @__PURE__ */ new Map();
@@ -1496,6 +1675,35 @@ var RigPlayer = class {
1496
1675
  /** slots the editor has selected; setPose draws them at GHOST_ALPHA at least */
1497
1676
  ghosts = /* @__PURE__ */ new Set();
1498
1677
  clipGraphics = /* @__PURE__ */ new Map();
1678
+ /** Read-only render handles for in-page tools. Nodes remain owned by the player. */
1679
+ get renderItems() {
1680
+ return this.items;
1681
+ }
1682
+ /** Current document-space clipping polygons, keyed by path attachment id. */
1683
+ get clipPolygons() {
1684
+ return this.clipPoints;
1685
+ }
1686
+ /**
1687
+ * Modify resolved control outputs before skeleton evaluation, constraints and skinning.
1688
+ * Callbacks run in registration order, each with its own map. Change control inputs via setControls.
1689
+ */
1690
+ onBeforePose(callback) {
1691
+ this.beforePose.add(callback);
1692
+ return () => {
1693
+ this.beforePose.delete(callback);
1694
+ };
1695
+ }
1696
+ /** Subscribe after the base pose has been rebuilt. Return value removes the callback. */
1697
+ onAfterPose(callback) {
1698
+ this.afterPose.add(callback);
1699
+ return () => {
1700
+ this.afterPose.delete(callback);
1701
+ };
1702
+ }
1703
+ /** Rebuild the current pose without advancing playback; useful for reversible overlays. */
1704
+ refreshPose() {
1705
+ this.setPose(this.inputPose);
1706
+ }
1499
1707
  /** whole atlas page as a plain texture (identity uv mapping) for skinned meshes */
1500
1708
  pageTexture(tex) {
1501
1709
  let page = this.pages.get(tex.source);
@@ -1550,8 +1758,12 @@ var RigPlayer = class {
1550
1758
  }
1551
1759
  /** Apply a pose (rest where silent) to the display objects. */
1552
1760
  setPose(pose) {
1553
- const skel = this.skel = evaluate(this.doc, pose);
1554
- const clips = new Set(this.doc.slots.flatMap((s) => s.clip ? [s.clip] : []));
1761
+ this.inputPose = new Map(pose);
1762
+ if (this.controlOverrides.size) pose = new Map([...pose, ...this.controlOverrides]);
1763
+ this.clipPoints.clear();
1764
+ const skel = this.skel = evaluate(this.doc, pose, this.beforePose);
1765
+ pose = skel.pose;
1766
+ const masks = slotClips(skel), clips = new Set(masks.values());
1555
1767
  for (const [id, g] of this.clipGraphics) if (!clips.has(id)) {
1556
1768
  g.destroy();
1557
1769
  this.clipGraphics.delete(id);
@@ -1568,7 +1780,9 @@ var RigPlayer = class {
1568
1780
  const vertices = attachmentWorldVertices(skel, att.slot, att);
1569
1781
  const n = Math.max(32, vertices.length * 4);
1570
1782
  const points = samplePath(vertices, true, att.lengths, true, Array.from({ length: n }, (_, i) => i / n), true);
1571
- g.clear().poly(points.flatMap((p) => [p.x, p.y]), true).fill(16777215);
1783
+ const polygon = points.flatMap((p) => [p.x, p.y]);
1784
+ this.clipPoints.set(id, polygon);
1785
+ g.clear().poly(polygon, true).fill(16777215);
1572
1786
  }
1573
1787
  const m = new Matrix();
1574
1788
  const orderV = pose.get(pk("drawOrder", "order", "order"));
@@ -1576,7 +1790,8 @@ var RigPlayer = class {
1576
1790
  if (Array.isArray(orderV)) orderV.forEach((id, i) => order.set(id, i));
1577
1791
  for (const it of this.items) {
1578
1792
  const s = it.slot, shown = skel.attachment.get(s.id);
1579
- it.node.mask = s.clip ? this.clipGraphics.get(s.clip) ?? null : null;
1793
+ const mask = masks.get(s.id);
1794
+ it.node.mask = mask ? this.clipGraphics.get(mask) ?? null : null;
1580
1795
  if (shown !== it.att || this.hidden.has(s.id)) {
1581
1796
  it.node.visible = false;
1582
1797
  continue;
@@ -1589,6 +1804,7 @@ var RigPlayer = class {
1589
1804
  it.node.tint = rgbOf(color);
1590
1805
  it.node.zIndex = order.size ? order.get(s.id) ?? 0 : numOf(pose, pk("slot", s.id, "z"), s.z);
1591
1806
  if (it.skinned) {
1807
+ it.node.setFromMatrix(m.identity());
1592
1808
  const v = attachmentWorldVertices(skel, s.id, it.att);
1593
1809
  const buf = it.skinned.mesh.geometry.getBuffer("aPosition");
1594
1810
  buf.data.set(v);
@@ -1621,6 +1837,7 @@ var RigPlayer = class {
1621
1837
  it.node.scale.y *= h / (this.textures.get(att.asset)?.height ?? h);
1622
1838
  }
1623
1839
  }
1840
+ for (const callback of this.afterPose) callback(this);
1624
1841
  }
1625
1842
  /** World-space bone positions — used by editor gizmos and renderPreview overlays */
1626
1843
  bonePositions() {
@@ -1697,9 +1914,12 @@ export {
1697
1914
  RigPlayer,
1698
1915
  RigSymbol,
1699
1916
  atlasUV,
1917
+ controlPose,
1700
1918
  createTextures,
1701
1919
  migrateV1,
1702
1920
  parse,
1921
+ resolveControls,
1922
+ sampleBinding,
1703
1923
  validate
1704
1924
  };
1705
1925
  //# sourceMappingURL=runtime.js.map