@rig-c/play 1.14.1 → 1.16.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/README.md CHANGED
@@ -556,6 +556,104 @@ active (`null`), track evaluation runs with zero extra work and produces
556
556
  byte-identical poses. An unknown set name throws by name; on a build without
557
557
  `sets.json`, `sets()` is empty (`[]`) and `setSet` throws.
558
558
 
559
+ **Several sets at once** (#168): sets that touch disjoint slots can be active
560
+ together — a mouth viseme driven by audio beside an arm gesture driven by
561
+ input, without building every combination as its own set:
562
+
563
+ ```ts
564
+ player.setSets(['viseme_o', 'gesture_chin']); // exactly these, together
565
+ player.removeSet('viseme_o'); // the gesture stays
566
+ player.addSet('viseme_a'); // beside the active ones; already active = no change
567
+ player.activeSets; // ['viseme_a', 'gesture_chin'] in sets.json order
568
+ player.set; // null: several are active (one active → its name)
569
+ player.setSets([]); // restore base / track attachments
570
+ ```
571
+
572
+ A set *touches* every slot it covers, every slot it `hides` and the
573
+ `<base>_still` companion it hides. Two active sets touching the same slot are
574
+ refused by name — `sets "viseme_o" and "talk_point" both touch slot "mouth"` —
575
+ and nothing changes; precedence is never guessed. Disjoint sets compose as the
576
+ union of each alone, whatever the order they were named or added in, and every
577
+ rule above (applied to the held pose, after track evaluation, uncovered slots
578
+ untouched) holds for the composition. `setSet(name)` keeps meaning *exactly this
579
+ one* (`setSets([name])`) and `setSet(null)` is `setSets([])`; an unknown or
580
+ repeated name throws by name. `slotIdentity()` reports the list as `sets`
581
+ beside `set`.
582
+
583
+ **Drawn bones** (#170): a set from `@rig-c/parts` that replaces a limb
584
+ (a *gesture* subset set) carries `drawn` — the world rotation, in degrees
585
+ (CCW, Y up), each of its bones was painted at — and its attachments are
586
+ placed in the frame of the bones posed there:
587
+
588
+ ```json
589
+ "wave": {
590
+ "slots": { "arm_upper": "arm_upper_wave", "arm_lower": "arm_lower_wave" },
591
+ "drawn": { "shoulder": 50, "elbow": 95 }
592
+ }
593
+ ```
594
+
595
+ While such a set is active its bones are posed at those angles, the way
596
+ `rig-parts check --set` poses them: in document order, each drawn bone's
597
+ local rotation is the drawn angle less its world rotation at local 0 under
598
+ its already-posed parents (no constraint applied), computed once when the set
599
+ is activated. On a track that local rotation replaces the clip's rotation
600
+ of that bone on every pose, before `adjust` (so `adjust` still has the last
601
+ word); every other bone keeps playing. Activating or clearing a drawn set
602
+ poses the held pose again at the same time. Two active sets drawing the same
603
+ bone are refused by name, as two sets touching one slot are. A `drawn` naming
604
+ a bone the document does not have is refused when `sets.json` is loaded.
605
+
606
+ **Physics under a drawn bone** (#173). A physics chain hung from a bone a set
607
+ re-poses — a sleeve under a raised arm — reads the jump to the drawn angle as
608
+ motion: the constraint remembers where its bone was and pointed, so the
609
+ switch kicks it (on the test rig `drape`, a 110° raise throws the sleeve's
610
+ tip about 297 units off the arm in the switch itself, against an idle
611
+ frame-to-frame range of 1.5). Every set call takes an option for the chains
612
+ under the bones the switch re-poses:
613
+
614
+ ```ts
615
+ player.setSets(['raise'], { physics: 'carry' }); // also setSet, addSet, removeSet
616
+ ```
617
+
618
+ - `'carry'` moves each affected constraint's remembered state by the rigid
619
+ turn of its deepest re-posed ancestor, so the chain keeps its pose relative
620
+ to the arm and goes on swinging from there (the switch itself moves the
621
+ sleeve 0 relative to the arm; gravity then re-settles it in the turned pose,
622
+ which is physics and not the switch).
623
+ - `'reset'` restarts each affected constraint at the new pose, as a physics
624
+ `reset` key does: the chain snaps straight along its bones (17 units on the
625
+ same raise) and settles from there.
626
+ - Absent, nothing changes: today's behaviour, frame for frame. This stays the
627
+ default.
628
+
629
+ `'carry'` is recommended: on `drape` it moves the sleeve 0 relative to the arm
630
+ at the switch and 0 at the deactivation, where `'reset'` moves it 17 units at
631
+ the switch and 118 at the deactivation (no option: 297 and 298).
632
+
633
+ A constraint is affected when its bone is a bone whose drawn rotation the call
634
+ adds, removes or changes, or a descendant of one; every other chain is not
635
+ touched. The option does nothing before `play` (the setup pose carries no
636
+ physics state) and nothing when no physics hangs under a re-posed bone. Three limits:
637
+
638
+ - Carry moves the positions and direction the constraint remembers; a
639
+ constraint with `x`/`y` keeps its translation offsets along the world axes,
640
+ unturned.
641
+ - A drawn bone nested under a carried chain reads its new world from the
642
+ chain's state before the carry.
643
+ - A later `seek` replays from the start with the sets then active, as it
644
+ always has, so it does not reproduce a carried switch.
645
+
646
+ `sets.json` keys are read strictly. Besides `slots`, `hides` and `drawn`, a
647
+ set may carry `drawnLength`, `registration` and `threshold`, which parts
648
+ writes and which place nothing under `check --set`: play reads past them by
649
+ name and gives them no meaning. Any other key, on a set or on the document
650
+ (which takes `spec`, `sets` and `threshold`), is refused by name rather than
651
+ ignored.
652
+
653
+ To switch one driver's set while another stays — a new viseme while a gesture
654
+ holds — name the new list in one call (`setSets(['viseme_a', 'gesture_chin'])`)
655
+ or `removeSet` then `addSet`; the first re-poses once, the second twice.
656
+
559
657
  ### Slot identity snapshot (`slotIdentity()`)
560
658
 
561
659
  To inspect which attachment key, owning skin and visibility state each slot
@@ -566,7 +664,8 @@ holds on the current frame — for a parts explorer or review overlay — withou
566
664
  const snap = player.slotIdentity();
567
665
  // {
568
666
  // generation: number, // matches player.generation and player.posedFrame().generation
569
- // set: string | null, // active set name (player.set)
667
+ // set: string | null, // the one active set (player.set); null when none or several
668
+ // sets: readonly string[], // every active set (player.activeSets), sets.json order
570
669
  // skin: string | null, // active skin name (player.skin ?? null)
571
670
  // slots: readonly [ // every slot in posed draw order (z = 0 .. slots.length - 1)
572
671
  // {
@@ -589,7 +688,7 @@ ones included) appears in `snap.slots`:
589
688
  and the skin that owns it (`skin`).
590
689
  - Non-drawn slots report `attachment: null`, `skin: null`, and why they are
591
690
  absent from `posedFrame().order`:
592
- - `'set-hide'` — hidden by the active attachment set (`set.hides` or a
691
+ - `'set-hide'` — hidden by an active attachment set (`set.hides` or a
593
692
  `<base>_still` companion of a slot the set covers or hides).
594
693
  - `'timeline-null'` — cleared by an active attachment timeline key (`null`).
595
694
  - `'no-attachment'` — no attachment in the setup pose or under the active skin
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { type CanvasBackend, CanvasRenderer, type CanvasViewport } from './Canva
2
2
  export { DomRenderer, type MeshBackend, type MeshSourceRect } from './DomRenderer.js';
3
3
  export { type PageLayout, type PageRegion, type RegionEncoding, type RegionImage, revokeRegions, type UnpackRegionsOptions, unpackRegions } from './DomTexture.js';
4
4
  export { type LoadModelOptions, loadModel, type ModelAssets, type ModelDocument, readSetupMesh, type RegionLoading, type RigSet, type SetupMesh, type SetupMeshTarget } from './loadModel.js';
5
- export { type AdjustableBones, type BoneAdjust, type BoneAdjustContext, type BoneLocals, type BoneWorld, type PhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead, type PlayOptions, type PoseBounds, type QueueOptions, type RenderMode, type RigComplete, type RigEnvironment, type RigEvent, RigPlayer, type RigPlayerOptions, type SlotFilter, type SlotIdentity, type SlotIdentityReason, type SlotIdentitySnapshot, type TrackEntry } from './player.js';
5
+ export { type AdjustableBones, type BoneAdjust, type BoneAdjustContext, type BoneLocals, type BoneWorld, type PhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead, type PlayOptions, type PoseBounds, type QueueOptions, type RenderMode, type RigComplete, type RigEnvironment, type RigEvent, RigPlayer, type RigPlayerOptions, type SetActivationOptions, type SlotFilter, type SlotIdentity, type SlotIdentityReason, type SlotIdentitySnapshot, type TrackEntry } from './player.js';
6
6
  export type { BlendMode, PosedClip, PosedFrame, PosedMesh, PosedRegion, PosedSlot, } from './posedFrame.js';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnK,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9L,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,KAAK,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACpd,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnK,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9L,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,KAAK,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC/e,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -8579,6 +8579,9 @@ function readSetupMesh(assets, target) {
8579
8579
  return readSetupMesh$1(underNoSkin(assets.document.doc), target);
8580
8580
  }
8581
8581
  const SETS_SPECS = ["spine-parts-sets/1", "rig-parts-sets/1"];
8582
+ const SET_KEYS_NOT_POSED = ["drawnLength", "registration", "threshold"];
8583
+ const SET_KEYS = ["slots", "hides", "drawn", ...SET_KEYS_NOT_POSED];
8584
+ const SETS_DOCUMENT_KEYS = ["spec", "sets", "threshold"];
8582
8585
  function isRecord(value) {
8583
8586
  return typeof value === "object" && value !== null && !Array.isArray(value);
8584
8587
  }
@@ -8597,10 +8600,16 @@ function readSetsText(text, where, doc) {
8597
8600
  `${where}: spec is ${JSON.stringify(raw.spec)}, not "${SETS_SPECS[0]}" or "${SETS_SPECS[1]}"`
8598
8601
  );
8599
8602
  }
8603
+ for (const key of Object.keys(raw)) {
8604
+ if (!SETS_DOCUMENT_KEYS.includes(key)) {
8605
+ throw new Error(`${where}: unknown key ${JSON.stringify(key)} (known: ${SETS_DOCUMENT_KEYS.join(", ")})`);
8606
+ }
8607
+ }
8600
8608
  if (!isRecord(raw.sets) && !Array.isArray(raw.sets)) {
8601
8609
  throw new Error(`${where}: "sets" must be an object or an array`);
8602
8610
  }
8603
8611
  const knownSlots = new Set(doc.slots.map((slot) => slot.name));
8612
+ const knownBones = new Set(doc.bones.map((bone2) => bone2.name));
8604
8613
  const attachmentsBySlot = /* @__PURE__ */ new Map();
8605
8614
  for (const skin of doc.skins) {
8606
8615
  for (const [slotName, table] of Object.entries(skin.attachments)) {
@@ -8636,6 +8645,12 @@ function readSetsText(text, where, doc) {
8636
8645
  throw new Error(`${where}: duplicate set "${name}"`);
8637
8646
  }
8638
8647
  seenNames.add(name);
8648
+ for (const key of Object.keys(item)) {
8649
+ if (Array.isArray(raw.sets) && key === "name") continue;
8650
+ if (!SET_KEYS.includes(key)) {
8651
+ throw new Error(`${where}: set "${name}" has unknown key ${JSON.stringify(key)} (known: ${SET_KEYS.join(", ")})`);
8652
+ }
8653
+ }
8639
8654
  if (!isRecord(item.slots)) {
8640
8655
  throw new Error(`${where}: set "${name}" must have a "slots" object`);
8641
8656
  }
@@ -8670,11 +8685,28 @@ function readSetsText(text, where, doc) {
8670
8685
  if (!hides.includes(hidden)) hides.push(hidden);
8671
8686
  }
8672
8687
  }
8688
+ let drawn;
8689
+ if (item.drawn !== void 0) {
8690
+ if (!isRecord(item.drawn)) {
8691
+ throw new Error(`${where}: set "${name}" "drawn" must be an object of bone name → degrees`);
8692
+ }
8693
+ drawn = {};
8694
+ for (const [boneName, degrees] of Object.entries(item.drawn)) {
8695
+ if (!knownBones.has(boneName)) {
8696
+ throw new Error(`${where}: set "${name}" draws unknown bone "${boneName}"`);
8697
+ }
8698
+ if (typeof degrees !== "number" || !Number.isFinite(degrees)) {
8699
+ throw new Error(`${where}: set "${name}" drawn bone "${boneName}" must be a finite number of degrees`);
8700
+ }
8701
+ drawn[boneName] = degrees;
8702
+ }
8703
+ }
8673
8704
  out.push(
8674
8705
  Object.freeze({
8675
8706
  name,
8676
8707
  slots: Object.freeze(slots),
8677
- hides: Object.freeze(hides)
8708
+ hides: Object.freeze(hides),
8709
+ ...drawn === void 0 || Object.keys(drawn).length === 0 ? {} : { drawn: Object.freeze(drawn) }
8678
8710
  })
8679
8711
  );
8680
8712
  }
@@ -8930,6 +8962,28 @@ function frameOf(source, pose, color = [1, 1, 1, 1], draws = () => true) {
8930
8962
  return { order };
8931
8963
  }
8932
8964
  const DEFAULT_ENVIRONMENT = Object.freeze({ wind: Object.freeze([1, 0]), gravity: Object.freeze([0, 1]) });
8965
+ function setPhysicsOption(call, options) {
8966
+ if (options === void 0) return void 0;
8967
+ if (options === null || typeof options !== "object") {
8968
+ throw new Error(`${call}: options must be an object like { physics: 'carry' }, got ${JSON.stringify(options)}`);
8969
+ }
8970
+ const physics = options.physics;
8971
+ if (physics === void 0 || physics === "carry" || physics === "reset") return physics;
8972
+ throw new Error(`${call}: options.physics must be 'carry' or 'reset', got ${JSON.stringify(physics)}`);
8973
+ }
8974
+ function drawnChanged(before, after) {
8975
+ const changed = /* @__PURE__ */ new Set();
8976
+ if (before !== null) {
8977
+ for (const [bone2, rotation] of before) if (after?.get(bone2) !== rotation) changed.add(bone2);
8978
+ }
8979
+ if (after !== null) {
8980
+ for (const [bone2, rotation] of after) if (before?.get(bone2) !== rotation) changed.add(bone2);
8981
+ }
8982
+ return changed;
8983
+ }
8984
+ function physicsContextOf(stepper) {
8985
+ return "first" in stepper ? stepper.track.ctx : stepper.path.ctx;
8986
+ }
8933
8987
  function predicateOf(names) {
8934
8988
  const allowed = new Set(names);
8935
8989
  return (slot) => allowed.has(slot);
@@ -8942,6 +8996,47 @@ function hiddenBySet(slotName, set) {
8942
8996
  }
8943
8997
  return false;
8944
8998
  }
8999
+ function touchedBySet(view, set) {
9000
+ const touched = /* @__PURE__ */ new Set();
9001
+ for (const slot of view.slots) {
9002
+ if (slot.name in set.slots || hiddenBySet(slot.name, set)) touched.add(slot.name);
9003
+ }
9004
+ return touched;
9005
+ }
9006
+ function normaliseDegrees(deg) {
9007
+ let v = (deg % 360 + 360) % 360;
9008
+ if (v > 180) v -= 360;
9009
+ return v;
9010
+ }
9011
+ function drawnLocals(view, drawn) {
9012
+ const names = new Set(view.bones.map((b) => b.name));
9013
+ const realParent = (parent) => parent !== void 0 && parent.endsWith("_d") && names.has(parent.slice(0, -2)) ? parent.slice(0, -2) : parent;
9014
+ const bones = view.bones.map((b) => ({ ...b, parent: realParent(b.parent) }));
9015
+ const locals = /* @__PURE__ */ new Map();
9016
+ for (let i = 0; i < bones.length; i++) {
9017
+ const target = drawn[bones[i].name];
9018
+ if (target === void 0) continue;
9019
+ bones[i] = { ...bones[i], rotation: 0 };
9020
+ const pose = poseRawSetup({ ...view, bones, slots: [], constraints: [] });
9021
+ const world = pose.bones.find((b) => b.name === bones[i].name);
9022
+ if (world === void 0) throw new Error(`drawn bone ${JSON.stringify(bones[i].name)} was not posed`);
9023
+ const rotation = normaliseDegrees(target - normaliseDegrees(Math.atan2(world.c, world.a) * 180 / Math.PI));
9024
+ bones[i] = { ...bones[i], rotation };
9025
+ locals.set(bones[i].name, rotation);
9026
+ }
9027
+ return locals;
9028
+ }
9029
+ function composeSets(sets) {
9030
+ if (sets.length === 0) return null;
9031
+ if (sets.length === 1) return sets[0];
9032
+ const drawn = sets.filter((s) => s.drawn !== void 0);
9033
+ return {
9034
+ name: sets.map((s) => s.name).join("+"),
9035
+ slots: Object.assign({}, ...sets.map((s) => s.slots)),
9036
+ hides: sets.flatMap((s) => s.hides),
9037
+ ...drawn.length === 0 ? {} : { drawn: Object.assign({}, ...drawn.map((s) => s.drawn)) }
9038
+ };
9039
+ }
8945
9040
  function applySetToPose(view, raw, set) {
8946
9041
  const coveredNames = new Set(Object.keys(set.slots));
8947
9042
  const overrideSlots = view.slots.flatMap((slot) => {
@@ -9182,7 +9277,14 @@ class RigPlayer {
9182
9277
  cachedIdentityGen = -1;
9183
9278
  setList;
9184
9279
  setsByName;
9280
+ /** The active sets in declaration order (#168); `activeSet` is their composition. */
9281
+ activeList = [];
9185
9282
  activeSet = null;
9283
+ touchedSets = /* @__PURE__ */ new Map();
9284
+ /** The active sets' drawn bones as local rotations, applied before `adjust` on every pose (#170); null when none draws. */
9285
+ drawnPose = null;
9286
+ /** Test-only (#173's planted control): `carry` maps every state through the identity instead of the anchor's turn. Not API. */
9287
+ carryPlant = false;
9186
9288
  adjustFn = null;
9187
9289
  /** Current preview patches, reapplied through the public core API on replay; no edit history. */
9188
9290
  physicsPatches = /* @__PURE__ */ new Map();
@@ -9193,6 +9295,7 @@ class RigPlayer {
9193
9295
  draws = () => true;
9194
9296
  /** Handed to every track the player opens; reads the current `adjust` so setting it later needs no reopen. */
9195
9297
  adjustHook = (locals, ctx) => {
9298
+ if (this.drawnPose !== null) for (const [bone2, rotation] of this.drawnPose) locals.bone(bone2).rotation = rotation;
9196
9299
  if (this.adjustFn !== null) this.adjustFn(locals, ctx);
9197
9300
  };
9198
9301
  listeners = {
@@ -9726,8 +9829,7 @@ class RigPlayer {
9726
9829
  this.reopen();
9727
9830
  this.stepTo(time);
9728
9831
  } else if (this.rawPose !== null) {
9729
- this.setup = poseRawSetup(this.view);
9730
- this.setRawPose(this.setup);
9832
+ this.setRawPose(this.drawnSetup());
9731
9833
  } else {
9732
9834
  this.poseGeneration++;
9733
9835
  this.cachedIdentity = null;
@@ -9741,9 +9843,17 @@ class RigPlayer {
9741
9843
  sets() {
9742
9844
  return this.setList;
9743
9845
  }
9744
- /** The currently active attachment set's name, or `null` when the base attachments are active. */
9846
+ /**
9847
+ * The active attachment set's name when exactly one is active; `null` when
9848
+ * the base attachments are active or when several sets are (read
9849
+ * `activeSets` for the list).
9850
+ */
9745
9851
  get set() {
9746
- return this.activeSet?.name ?? null;
9852
+ return this.activeList.length === 1 ? this.activeList[0].name : null;
9853
+ }
9854
+ /** Every active attachment set's name, in `sets.json` declaration order (`[]` when none). */
9855
+ get activeSets() {
9856
+ return this.activeList.map((s) => s.name);
9747
9857
  }
9748
9858
  /**
9749
9859
  * Switches the active attachment set (#134): every slot the set covers takes
@@ -9757,34 +9867,258 @@ class RigPlayer {
9757
9867
  * frames untouched; when no set is active (`null`), track evaluation runs
9758
9868
  * with zero overhead and produces byte-identical poses.
9759
9869
  *
9870
+ * A set with `drawn` (#170) also poses its bones at those world angles, as
9871
+ * `@rig-c/parts check --set` does, replacing a clip's rotation of them.
9872
+ *
9873
+ * `setSet(name)` means exactly this one set: any other active sets are
9874
+ * cleared (`setSets([name])`), and `setSet(null)` is `setSets([])`.
9875
+ *
9760
9876
  * A name not in `sets.json` throws by name; a build without `sets.json` has
9761
9877
  * `sets()` empty and `setSet` throws.
9762
9878
  */
9763
- setSet(name) {
9879
+ setSet(name, options) {
9764
9880
  if (this.setList.length === 0) {
9765
9881
  throw new Error(`cannot setSet(${JSON.stringify(name)}): this model has no sets.json`);
9766
9882
  }
9767
- if (name === null) {
9768
- this.activeSet = null;
9769
- this.clearSeekCache();
9770
- const base2 = this.rawPose ?? (this.setup ?? (this.setup = poseRawSetup(this.view)));
9771
- this.rawPose = base2;
9772
- this.pose = base2;
9773
- this.poseGeneration++;
9774
- this.cachedIdentity = null;
9775
- return;
9883
+ const physics = setPhysicsOption("setSet", options);
9884
+ this.activate(name === null ? [] : [this.namedSet(name)], physics);
9885
+ }
9886
+ /**
9887
+ * Makes exactly `names` the active attachment sets (#168), together: each
9888
+ * set covers and hides its slots as it does alone, provided no two of them
9889
+ * touch the same slot (cover it, hide it, or hide it as a `<base>_still`
9890
+ * companion). Composition does not depend on the order of `names`. An
9891
+ * unknown name, a repeated name or two sets touching one slot throw by name
9892
+ * and leave the active sets as they were; `[]` restores the base.
9893
+ *
9894
+ * `options.physics` (#173) decides what a switch that re-poses `drawn` bones
9895
+ * does to the physics constraints hung under them — on a bone whose drawn
9896
+ * rotation this call adds, removes or changes, or on a descendant of one:
9897
+ * `'carry'` moves each one's remembered state (where its bone was and
9898
+ * pointed) by the rigid turn of its deepest re-posed ancestor, so the chain
9899
+ * keeps its pose relative to the bone; `'reset'` restarts each one at the
9900
+ * new pose, as a physics `reset` key does. Absent, the constraints read the
9901
+ * jump as motion and kick, as they always have. Anything else throws before
9902
+ * anything changes. Nothing to do before `play` or when no physics hangs
9903
+ * under a re-posed bone. A later `seek` replays from the start with the sets
9904
+ * then active and does not reproduce a carried switch. `setSet`, `addSet`
9905
+ * and `removeSet` take the same option.
9906
+ */
9907
+ setSets(names, options) {
9908
+ if (this.setList.length === 0) {
9909
+ throw new Error(`cannot setSets(${JSON.stringify(names)}): this model has no sets.json`);
9776
9910
  }
9911
+ const physics = setPhysicsOption("setSets", options);
9912
+ const sets = names.map((name) => this.namedSet(name));
9913
+ for (let i = 0; i < sets.length; i++) {
9914
+ for (let j = 0; j < i; j++) {
9915
+ if (sets[i] === sets[j]) throw new Error(`set ${JSON.stringify(sets[i].name)} is named twice in setSets`);
9916
+ }
9917
+ }
9918
+ this.activate(sets, physics);
9919
+ }
9920
+ /**
9921
+ * Activates `name` beside the sets already active (#168): a set already
9922
+ * active is left as it is; one touching a slot an active set touches throws
9923
+ * by name (the slot and both sets) and changes nothing.
9924
+ */
9925
+ addSet(name, options) {
9926
+ if (this.setList.length === 0) {
9927
+ throw new Error(`cannot addSet(${JSON.stringify(name)}): this model has no sets.json`);
9928
+ }
9929
+ const physics = setPhysicsOption("addSet", options);
9930
+ const found = this.namedSet(name);
9931
+ if (this.activeList.includes(found)) return;
9932
+ this.activate([...this.activeList, found], physics);
9933
+ }
9934
+ /**
9935
+ * Deactivates `name`, keeping every other active set (#168). An unknown name
9936
+ * throws by name; a set that is not active is left as it is.
9937
+ */
9938
+ removeSet(name, options) {
9939
+ if (this.setList.length === 0) {
9940
+ throw new Error(`cannot removeSet(${JSON.stringify(name)}): this model has no sets.json`);
9941
+ }
9942
+ const physics = setPhysicsOption("removeSet", options);
9943
+ const found = this.namedSet(name);
9944
+ if (!this.activeList.includes(found)) return;
9945
+ this.activate(this.activeList.filter((s) => s !== found), physics);
9946
+ }
9947
+ /** The declared set named `name`, or a throw naming it and the available sets. */
9948
+ namedSet(name) {
9777
9949
  const found = this.setsByName.get(name);
9778
9950
  if (found === void 0) {
9779
9951
  throw new Error(
9780
9952
  `no set named ${JSON.stringify(name)} in sets.json (available: ${this.setList.map((s) => s.name).join(", ")})`
9781
9953
  );
9782
9954
  }
9783
- this.activeSet = found;
9955
+ return found;
9956
+ }
9957
+ /**
9958
+ * Poses again after the drawn bones changed (#170): through the tracks at
9959
+ * the same time when the player has played, else the setup pose with the
9960
+ * drawn bones at their rotations.
9961
+ */
9962
+ reposeDrawn() {
9963
+ const stepper = this.tracks ?? this.track;
9964
+ if (stepper !== null && this.pose !== null) {
9965
+ this.setRawPose(stepper.step(0));
9966
+ return;
9967
+ }
9968
+ this.setRawPose(this.drawnSetup());
9969
+ }
9970
+ /** The setup pose with the active sets' drawn bones at their rotations (#170); the plain setup pose when none draws. */
9971
+ drawnSetup() {
9972
+ const setup = this.setup ?? (this.setup = poseRawSetup(this.view));
9973
+ if (this.drawnPose === null) return setup;
9974
+ return poseRawSetup({
9975
+ ...this.view,
9976
+ bones: this.view.bones.map((b) => {
9977
+ const rotation = this.drawnPose?.get(b.name);
9978
+ return rotation === void 0 ? b : { ...b, rotation };
9979
+ })
9980
+ });
9981
+ }
9982
+ /** The slots `set` touches, computed once per set. */
9983
+ touched(set) {
9984
+ let touched = this.touchedSets.get(set.name);
9985
+ if (touched === void 0) {
9986
+ touched = touchedBySet(this.view, set);
9987
+ this.touchedSets.set(set.name, touched);
9988
+ }
9989
+ return touched;
9990
+ }
9991
+ /**
9992
+ * The physics under the bones a set switch re-poses (#173), before the
9993
+ * repose step: the drawn rotations went from `before` to `after` (already
9994
+ * `drawnPose`), and every physics constraint whose bone is one of the
9995
+ * changed bones or a descendant of one is carried or reset. Nothing to do
9996
+ * before `play` (the setup pose carries no physics state) or when no
9997
+ * constraint hangs under a changed bone.
9998
+ */
9999
+ handOverPhysics(before, after, mode) {
10000
+ const stepper = this.tracks ?? this.track;
10001
+ if (stepper === null || this.pose === null || this.rawPose === null) return;
10002
+ const changed = drawnChanged(before, after);
10003
+ if (changed.size === 0) return;
10004
+ const parentOf = new Map(this.view.bones.map((b) => [b.name, b.parent]));
10005
+ const anchorOf = (bone2) => {
10006
+ for (let b = bone2; b !== void 0; b = parentOf.get(b)) if (changed.has(b)) return b;
10007
+ return void 0;
10008
+ };
10009
+ const ctx = physicsContextOf(stepper);
10010
+ const affected = [];
10011
+ for (const c of this.view.constraints) {
10012
+ if (c.kind !== "physics") continue;
10013
+ const bone2 = c.record?.bone;
10014
+ const anchor = bone2 === void 0 ? void 0 : anchorOf(bone2);
10015
+ if (anchor === void 0) continue;
10016
+ const state = ctx.states.get(c.name);
10017
+ if (state === void 0 || state.pending) continue;
10018
+ affected.push({ name: c.name, anchor });
10019
+ }
10020
+ if (affected.length === 0) return;
10021
+ if (mode === "reset") {
10022
+ for (const { name } of affected) {
10023
+ const s = ctx.states.get(name);
10024
+ s.remaining = 0;
10025
+ s.lastTime = ctx.time;
10026
+ s.pending = true;
10027
+ s.xOffset = s.xLag = s.xVelocity = 0;
10028
+ s.yOffset = s.yLag = s.yVelocity = 0;
10029
+ s.rotateOffset = s.rotateLag = s.rotateVelocity = 0;
10030
+ s.scaleOffset = s.scaleLag = s.scaleVelocity = 0;
10031
+ }
10032
+ return;
10033
+ }
10034
+ const anchors = new Set(affected.map((a) => a.anchor));
10035
+ const worlds = (pose) => {
10036
+ const out = /* @__PURE__ */ new Map();
10037
+ for (const b of pose.bones) if (anchors.has(b.name)) out.set(b.name, b);
10038
+ return out;
10039
+ };
10040
+ const old = worlds(this.rawPose);
10041
+ const snapshot = captureStepperState(stepper);
10042
+ const now = worlds(stepper.step(0));
10043
+ restoreStepperState(stepper, snapshot);
10044
+ const maps = /* @__PURE__ */ new Map();
10045
+ for (const anchor of anchors) {
10046
+ const o = old.get(anchor);
10047
+ const n = now.get(anchor);
10048
+ if (o === void 0 || n === void 0) continue;
10049
+ if (this.carryPlant) {
10050
+ maps.set(anchor, [1, 0, 0, 1, 0, 0]);
10051
+ continue;
10052
+ }
10053
+ const det = o.a * o.d - o.b * o.c;
10054
+ if (!Number.isFinite(det) || det === 0) continue;
10055
+ const ia = o.d / det;
10056
+ const ib = -o.b / det;
10057
+ const ic = -o.c / det;
10058
+ const id = o.a / det;
10059
+ const a = n.a * ia + n.b * ic;
10060
+ const b = n.a * ib + n.b * id;
10061
+ const c = n.c * ia + n.d * ic;
10062
+ const d = n.c * ib + n.d * id;
10063
+ maps.set(anchor, [a, b, c, d, n.worldX - (a * o.worldX + b * o.worldY), n.worldY - (c * o.worldX + d * o.worldY)]);
10064
+ }
10065
+ const states = physicsContextOf(stepper).states;
10066
+ for (const { name, anchor } of affected) {
10067
+ const m = maps.get(anchor);
10068
+ const s = states.get(name);
10069
+ if (m === void 0 || s === void 0) continue;
10070
+ const [a, b, c, d, x, y] = m;
10071
+ const ux = s.ux;
10072
+ s.ux = a * ux + b * s.uy + x;
10073
+ s.uy = c * ux + d * s.uy + y;
10074
+ const cx = s.cx;
10075
+ s.cx = a * cx + b * s.cy + x;
10076
+ s.cy = c * cx + d * s.cy + y;
10077
+ const tx = s.tx;
10078
+ s.tx = a * tx + b * s.ty;
10079
+ s.ty = c * tx + d * s.ty;
10080
+ }
10081
+ }
10082
+ /**
10083
+ * Makes `sets` (distinct) the active sets: refuses any two touching one slot
10084
+ * before changing anything, then applies their composition to the held pose.
10085
+ */
10086
+ activate(sets, physics) {
10087
+ for (let i = 0; i < sets.length; i++) {
10088
+ for (let j = 0; j < i; j++) {
10089
+ const other = this.touched(sets[j]);
10090
+ for (const slot of this.touched(sets[i])) {
10091
+ if (other.has(slot)) {
10092
+ throw new Error(
10093
+ `sets ${JSON.stringify(sets[j].name)} and ${JSON.stringify(sets[i].name)} both touch slot ${JSON.stringify(slot)}; active sets must cover and hide disjoint slots`
10094
+ );
10095
+ }
10096
+ }
10097
+ const otherDrawn = sets[j].drawn;
10098
+ for (const bone2 of Object.keys(sets[i].drawn ?? {})) {
10099
+ if (otherDrawn !== void 0 && bone2 in otherDrawn) {
10100
+ throw new Error(
10101
+ `sets ${JSON.stringify(sets[j].name)} and ${JSON.stringify(sets[i].name)} both pose bone ${JSON.stringify(bone2)}; active sets must draw disjoint bones`
10102
+ );
10103
+ }
10104
+ }
10105
+ }
10106
+ }
10107
+ const drawnBefore = this.activeSet?.drawn;
10108
+ this.activeList = this.setList.filter((s) => sets.includes(s));
10109
+ this.activeSet = composeSets(this.activeList);
9784
10110
  this.clearSeekCache();
10111
+ const drawn = this.activeSet?.drawn;
10112
+ if (drawn !== drawnBefore && !(drawn === void 0 && drawnBefore === void 0)) {
10113
+ const posedBefore = this.drawnPose;
10114
+ this.drawnPose = drawn === void 0 ? null : drawnLocals(this.view, drawn);
10115
+ if (physics !== void 0) this.handOverPhysics(posedBefore, this.drawnPose, physics);
10116
+ this.reposeDrawn();
10117
+ return;
10118
+ }
9785
10119
  const base = this.rawPose ?? (this.setup ?? (this.setup = poseRawSetup(this.view)));
9786
10120
  this.rawPose = base;
9787
- this.pose = applySetToPose(this.view, base, found);
10121
+ this.pose = this.activeSet === null ? base : applySetToPose(this.view, base, this.activeSet);
9788
10122
  this.poseGeneration++;
9789
10123
  this.cachedIdentity = null;
9790
10124
  }
@@ -10020,7 +10354,8 @@ class RigPlayer {
10020
10354
  }
10021
10355
  const snapshot = Object.freeze({
10022
10356
  generation: this.poseGeneration,
10023
- set: this.activeSet?.name ?? null,
10357
+ set: this.set,
10358
+ sets: Object.freeze(this.activeSets),
10024
10359
  skin: this.skinName ?? null,
10025
10360
  slots: Object.freeze(slots)
10026
10361
  });