@rig-c/play 1.15.0 → 1.17.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
@@ -603,6 +603,103 @@ poses the held pose again at the same time. Two active sets drawing the same
603
603
  bone are refused by name, as two sets touching one slot are. A `drawn` naming
604
604
  a bone the document does not have is refused when `sets.json` is loaded.
605
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
+ **Easing a drawn set in and out** (#174). A drawn set snaps its bones to the
647
+ drawn angles in one frame. `ease` turns them there over a duration instead,
648
+ and swaps the attachments halfway, the way an alternate drawing takes over
649
+ at a middle angle:
650
+
651
+ ```ts
652
+ player.setSets(['wave_r'], { ease: 0.5 }); // also setSet, addSet, removeSet
653
+ player.setSets([], { ease: 0.5 }); // and back
654
+ ```
655
+
656
+ - **The curve.** Each bone whose drawn rotation the call adds, removes or
657
+ changes turns from what it shows to its new local rotation along the
658
+ shorter way: `start + shortestΔ(start, end) · smoothstep(u)`, with
659
+ `u = elapsed / ease`. Going on, the start is the clip's value at the time
660
+ (it moves with the clip) and the end the drawn local; going off, the start
661
+ is the drawn local and the end the clip's value. So the progress from start
662
+ to end never leaves `[0, 1]` and never turns back, while the clip plays
663
+ underneath. On `@rig-c/parts`' public wind example with its gesture set
664
+ (`wave_r`, 0.5 s), it is 0 to 1 and non-decreasing on every frame, on and
665
+ off; on a clip that holds the arm still, the sleeve's world angle goes
666
+ −119.32° → −110° and back with no overshoot.
667
+ - **The swap.** The held pose draws the attachments it showed until the frame
668
+ whose `u` is nearest ½, then the set's (a tie goes to the later frame; the
669
+ next step is predicted from the current one, so a constant `dt` lands on
670
+ the nearest frame). On the wind example at `frame(1/60)`, 0.5 s swaps on
671
+ frame 15 and 0.25 s on frame 8. `activeSets`, `set` and `slotIdentity().sets`
672
+ name the call's sets from the call on; `slotIdentity().slots` reports what
673
+ the pose draws, so before the swap it shows the previous attachments.
674
+ - **The end.** On the frame the elapsed time reaches `ease`, the player is in
675
+ exactly the instant switch's state: from then on every bone matches an
676
+ instant switch made at the same frame (0° measured) and the slot identity
677
+ too.
678
+ - **Time.** The elapsed time is the player's step (`dt × timeScale`): a
679
+ paused player, or a step of 0, holds the ease where it is. Before `play`
680
+ there is nothing to step and the switch is instant; so is a call that
681
+ re-poses no bone. A `seek` or a skin change lands an ease in flight at
682
+ once.
683
+ - **A call during an ease** starts a new one from the angles shown (no jump:
684
+ reversing a 0.5 s ease at 0.3 makes no frame-to-frame step larger than the
685
+ ease's own), and the attachment swap starts again from what is drawn. A
686
+ call without `ease` during an ease lands it at once.
687
+ - `ease: 0`, or no `ease`, is the instant switch, frame for frame. Anything
688
+ but a finite number at or above 0 is refused by name before anything
689
+ changes.
690
+
691
+ With physics: `ease` and `physics: 'carry'` carry the chains under the eased
692
+ bones on every eased frame — before each step, the turn the frame's blend
693
+ makes is read as #173's switch is (a trial pose at the previous time) and
694
+ the chains are moved by it — so the ease's own turn is not read as motion.
695
+ `physics: 'reset'` with `ease` restarts them once, at the call. On `drape`
696
+ (0.5 s raise, sleeve tips in the arm's frame): the ease alone moves the
697
+ sleeve up to 30 units a frame relative to the arm (the chain reads the turn
698
+ as motion), ease + carry at most 3.9 on the `idle` clip (its idle ranges are
699
+ 1.5 hanging and 4.2 raised) and 2.3 on `shift` (0.85 and 0.07), where
700
+ gravity re-settles the chain in the turning arm's frame (with gravity at 0,
701
+ 0.83 against 0.84).
702
+
606
703
  `sets.json` keys are read strictly. Besides `slots`, `hides` and `drawn`, a
607
704
  set may carry `drawnLength`, `registration` and `threshold`, which parts
608
705
  writes and which place nothing under `check --set`: play reads past them by
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
@@ -8962,6 +8962,42 @@ function frameOf(source, pose, color = [1, 1, 1, 1], draws = () => true) {
8962
8962
  return { order };
8963
8963
  }
8964
8964
  const DEFAULT_ENVIRONMENT = Object.freeze({ wind: Object.freeze([1, 0]), gravity: Object.freeze([0, 1]) });
8965
+ function setEaseOption(call, options) {
8966
+ const ease = options?.ease;
8967
+ if (ease === void 0) return 0;
8968
+ if (typeof ease !== "number" || !Number.isFinite(ease) || ease < 0) {
8969
+ throw new Error(`${call}: options.ease must be a finite number of seconds at or above 0, got ${typeof ease === "number" ? String(ease) : JSON.stringify(ease)}`);
8970
+ }
8971
+ return ease;
8972
+ }
8973
+ function easeProgress(ease) {
8974
+ return Math.min(Math.max(ease.elapsed / ease.duration, 0), 1);
8975
+ }
8976
+ function smoothstep(u) {
8977
+ return u * u * (3 - 2 * u);
8978
+ }
8979
+ function setPhysicsOption(call, options) {
8980
+ if (options === void 0) return void 0;
8981
+ if (options === null || typeof options !== "object") {
8982
+ throw new Error(`${call}: options must be an object like { physics: 'carry' }, got ${JSON.stringify(options)}`);
8983
+ }
8984
+ const physics = options.physics;
8985
+ if (physics === void 0 || physics === "carry" || physics === "reset") return physics;
8986
+ throw new Error(`${call}: options.physics must be 'carry' or 'reset', got ${JSON.stringify(physics)}`);
8987
+ }
8988
+ function drawnChanged(before, after) {
8989
+ const changed = /* @__PURE__ */ new Set();
8990
+ if (before !== null) {
8991
+ for (const [bone2, rotation] of before) if (after?.get(bone2) !== rotation) changed.add(bone2);
8992
+ }
8993
+ if (after !== null) {
8994
+ for (const [bone2, rotation] of after) if (before?.get(bone2) !== rotation) changed.add(bone2);
8995
+ }
8996
+ return changed;
8997
+ }
8998
+ function physicsContextOf(stepper) {
8999
+ return "first" in stepper ? stepper.track.ctx : stepper.path.ctx;
9000
+ }
8965
9001
  function predicateOf(names) {
8966
9002
  const allowed = new Set(names);
8967
9003
  return (slot) => allowed.has(slot);
@@ -9261,6 +9297,14 @@ class RigPlayer {
9261
9297
  touchedSets = /* @__PURE__ */ new Map();
9262
9298
  /** The active sets' drawn bones as local rotations, applied before `adjust` on every pose (#170); null when none draws. */
9263
9299
  drawnPose = null;
9300
+ /** Test-only (#173's planted control): `carry` maps every state through the identity instead of the anchor's turn. Not API. */
9301
+ carryPlant = false;
9302
+ /** An eased set switch in flight (#174), or null. */
9303
+ easing = null;
9304
+ /** The last positive step `advance` took: what predicts the next one when an ease starts. */
9305
+ lastStep = 0;
9306
+ /** Test-only (#174's planted controls): swap the attachments at u = 0, or ease by w = 1.2u (1 at u = 1). Not API. */
9307
+ easePlant = null;
9264
9308
  adjustFn = null;
9265
9309
  /** Current preview patches, reapplied through the public core API on replay; no edit history. */
9266
9310
  physicsPatches = /* @__PURE__ */ new Map();
@@ -9271,7 +9315,8 @@ class RigPlayer {
9271
9315
  draws = () => true;
9272
9316
  /** Handed to every track the player opens; reads the current `adjust` so setting it later needs no reopen. */
9273
9317
  adjustHook = (locals, ctx) => {
9274
- if (this.drawnPose !== null) for (const [bone2, rotation] of this.drawnPose) locals.bone(bone2).rotation = rotation;
9318
+ if (this.easing !== null) this.applyEase(this.easing, locals);
9319
+ else if (this.drawnPose !== null) for (const [bone2, rotation] of this.drawnPose) locals.bone(bone2).rotation = rotation;
9275
9320
  if (this.adjustFn !== null) this.adjustFn(locals, ctx);
9276
9321
  };
9277
9322
  listeners = {
@@ -9480,11 +9525,12 @@ class RigPlayer {
9480
9525
  if (time > 0) this.stepTo(time);
9481
9526
  return tracks;
9482
9527
  }
9483
- /** Stores `raw` and applies `activeSet` when one is active. Zero work when `activeSet === null`. */
9528
+ /** Stores `raw` and applies the shown set (`activeSet`, or before an ease's swap the composition it eases from). Zero work when none. */
9484
9529
  setRawPose(raw) {
9485
9530
  this.rawPose = raw;
9486
9531
  this.heldParameters = null;
9487
- this.pose = this.activeSet === null ? raw : applySetToPose(this.view, raw, this.activeSet);
9532
+ const set = this.shownSet();
9533
+ this.pose = set === null ? raw : applySetToPose(this.view, raw, set);
9488
9534
  this.poseGeneration++;
9489
9535
  this.cachedIdentity = null;
9490
9536
  }
@@ -9602,6 +9648,7 @@ class RigPlayer {
9602
9648
  if (this.animation === null) throw new Error("seek before play: no animation is open");
9603
9649
  if (!(time >= 0) || !Number.isFinite(time)) throw new Error(`seek time must be a finite time at or above 0, got ${String(time)}`);
9604
9650
  if (this.beyondBase) throw new Error("seek is defined for a single animation on track 0; after a mix, another track, a queue or a clear the player cannot replay to a time — step it forward with frame/advance instead");
9651
+ this.easing = null;
9605
9652
  const cache = this.ensureSeekCache();
9606
9653
  const stepper = this.tracks ?? this.track;
9607
9654
  const exactHit = cache.exact.get(time);
@@ -9757,6 +9804,8 @@ class RigPlayer {
9757
9804
  if (this.pose === null) return;
9758
9805
  const step = dt * this.timeScale;
9759
9806
  if (this.paused || !(step > 0)) return;
9807
+ this.lastStep = step;
9808
+ if (this.easing !== null) this.advanceEase(this.easing, step);
9760
9809
  const tracks = this.tracks;
9761
9810
  if (tracks !== null) {
9762
9811
  const before2 = [];
@@ -9799,6 +9848,7 @@ class RigPlayer {
9799
9848
  this.source = frameSource(view, this.read.parsed);
9800
9849
  this.skinName = name;
9801
9850
  this.setup = null;
9851
+ this.easing = null;
9802
9852
  this.clearSeekCache();
9803
9853
  if (this.animation !== null && (this.track !== null || this.tracks !== null)) {
9804
9854
  const time = this.time;
@@ -9852,11 +9902,13 @@ class RigPlayer {
9852
9902
  * A name not in `sets.json` throws by name; a build without `sets.json` has
9853
9903
  * `sets()` empty and `setSet` throws.
9854
9904
  */
9855
- setSet(name) {
9905
+ setSet(name, options) {
9856
9906
  if (this.setList.length === 0) {
9857
9907
  throw new Error(`cannot setSet(${JSON.stringify(name)}): this model has no sets.json`);
9858
9908
  }
9859
- this.activate(name === null ? [] : [this.namedSet(name)]);
9909
+ const physics = setPhysicsOption("setSet", options);
9910
+ const ease = setEaseOption("setSet", options);
9911
+ this.activate(name === null ? [] : [this.namedSet(name)], physics, ease);
9860
9912
  }
9861
9913
  /**
9862
9914
  * Makes exactly `names` the active attachment sets (#168), together: each
@@ -9865,43 +9917,84 @@ class RigPlayer {
9865
9917
  * companion). Composition does not depend on the order of `names`. An
9866
9918
  * unknown name, a repeated name or two sets touching one slot throw by name
9867
9919
  * and leave the active sets as they were; `[]` restores the base.
9920
+ *
9921
+ * `options.physics` (#173) decides what a switch that re-poses `drawn` bones
9922
+ * does to the physics constraints hung under them — on a bone whose drawn
9923
+ * rotation this call adds, removes or changes, or on a descendant of one:
9924
+ * `'carry'` moves each one's remembered state (where its bone was and
9925
+ * pointed) by the rigid turn of its deepest re-posed ancestor, so the chain
9926
+ * keeps its pose relative to the bone; `'reset'` restarts each one at the
9927
+ * new pose, as a physics `reset` key does. Absent, the constraints read the
9928
+ * jump as motion and kick, as they always have. Anything else throws before
9929
+ * anything changes. Nothing to do before `play` or when no physics hangs
9930
+ * under a re-posed bone. A later `seek` replays from the start with the sets
9931
+ * then active and does not reproduce a carried switch. `setSet`, `addSet`
9932
+ * and `removeSet` take the same option.
9933
+ *
9934
+ * `options.ease` (#174, seconds, a finite number at or above 0) eases the
9935
+ * bones the call re-poses instead of snapping them. Each bone whose drawn
9936
+ * rotation the call adds, removes or changes turns from what it shows to
9937
+ * its new local rotation along the shorter way, as
9938
+ * `start + shortestΔ(start, end) · smoothstep(elapsed / ease)`: going on,
9939
+ * from the clip's value at the time to the drawn local; going off, from the
9940
+ * drawn local to the clip's value. The attachments the pose draws stay the
9941
+ * previous ones until the frame whose `elapsed / ease` is nearest ½ (the
9942
+ * next step predicted from the current one; a tie goes to the later frame),
9943
+ * then become the call's; `activeSets` and `slotIdentity().sets` name the
9944
+ * call's sets at once, while `slotIdentity().slots` follows what is drawn.
9945
+ * On the frame the elapsed time reaches `ease` the player is in exactly the
9946
+ * instant switch's state. The elapsed time is the player's step
9947
+ * (`dt × timeScale`), so a paused player or a zero step holds the ease;
9948
+ * before `play`, or when no bone's rotation changes, the switch is instant.
9949
+ * A call during an ease eases again from the angles shown (a call without
9950
+ * `ease` lands it at once), and `seek` or a skin change lands it at once.
9951
+ * With `physics: 'carry'` the chains under the eased bones are carried on
9952
+ * every eased frame, by the turn that frame's blend makes; with `'reset'`
9953
+ * they restart once, at the call. `0` or absent is the instant switch on
9954
+ * the same code path; anything else throws by name before anything changes.
9868
9955
  */
9869
- setSets(names) {
9956
+ setSets(names, options) {
9870
9957
  if (this.setList.length === 0) {
9871
9958
  throw new Error(`cannot setSets(${JSON.stringify(names)}): this model has no sets.json`);
9872
9959
  }
9960
+ const physics = setPhysicsOption("setSets", options);
9961
+ const ease = setEaseOption("setSets", options);
9873
9962
  const sets = names.map((name) => this.namedSet(name));
9874
9963
  for (let i = 0; i < sets.length; i++) {
9875
9964
  for (let j = 0; j < i; j++) {
9876
9965
  if (sets[i] === sets[j]) throw new Error(`set ${JSON.stringify(sets[i].name)} is named twice in setSets`);
9877
9966
  }
9878
9967
  }
9879
- this.activate(sets);
9968
+ this.activate(sets, physics, ease);
9880
9969
  }
9881
9970
  /**
9882
9971
  * Activates `name` beside the sets already active (#168): a set already
9883
9972
  * active is left as it is; one touching a slot an active set touches throws
9884
9973
  * by name (the slot and both sets) and changes nothing.
9885
9974
  */
9886
- addSet(name) {
9975
+ addSet(name, options) {
9887
9976
  if (this.setList.length === 0) {
9888
9977
  throw new Error(`cannot addSet(${JSON.stringify(name)}): this model has no sets.json`);
9889
9978
  }
9979
+ const physics = setPhysicsOption("addSet", options);
9980
+ const ease = setEaseOption("addSet", options);
9890
9981
  const found = this.namedSet(name);
9891
9982
  if (this.activeList.includes(found)) return;
9892
- this.activate([...this.activeList, found]);
9983
+ this.activate([...this.activeList, found], physics, ease);
9893
9984
  }
9894
9985
  /**
9895
9986
  * Deactivates `name`, keeping every other active set (#168). An unknown name
9896
9987
  * throws by name; a set that is not active is left as it is.
9897
9988
  */
9898
- removeSet(name) {
9989
+ removeSet(name, options) {
9899
9990
  if (this.setList.length === 0) {
9900
9991
  throw new Error(`cannot removeSet(${JSON.stringify(name)}): this model has no sets.json`);
9901
9992
  }
9993
+ const physics = setPhysicsOption("removeSet", options);
9994
+ const ease = setEaseOption("removeSet", options);
9902
9995
  const found = this.namedSet(name);
9903
9996
  if (!this.activeList.includes(found)) return;
9904
- this.activate(this.activeList.filter((s) => s !== found));
9997
+ this.activate(this.activeList.filter((s) => s !== found), physics, ease);
9905
9998
  }
9906
9999
  /** The declared set named `name`, or a throw naming it and the available sets. */
9907
10000
  namedSet(name) {
@@ -9947,11 +10040,203 @@ class RigPlayer {
9947
10040
  }
9948
10041
  return touched;
9949
10042
  }
10043
+ /**
10044
+ * The physics under the bones a set switch re-poses (#173), before the
10045
+ * repose step: the `changed` bones' rotations went from what the held pose
10046
+ * shows to what the player now applies (`drawnPose`, or an ease's next
10047
+ * blend, #174), and every physics constraint whose bone is one of the
10048
+ * changed bones or a descendant of one is carried or reset. Nothing to do
10049
+ * before `play` (the setup pose carries no physics state) or when no
10050
+ * constraint hangs under a changed bone.
10051
+ */
10052
+ handOverPhysics(changed, mode) {
10053
+ const stepper = this.tracks ?? this.track;
10054
+ if (stepper === null || this.pose === null || this.rawPose === null) return;
10055
+ if (changed.size === 0) return;
10056
+ const parentOf = new Map(this.view.bones.map((b) => [b.name, b.parent]));
10057
+ const anchorOf = (bone2) => {
10058
+ for (let b = bone2; b !== void 0; b = parentOf.get(b)) if (changed.has(b)) return b;
10059
+ return void 0;
10060
+ };
10061
+ const ctx = physicsContextOf(stepper);
10062
+ const affected = [];
10063
+ for (const c of this.view.constraints) {
10064
+ if (c.kind !== "physics") continue;
10065
+ const bone2 = c.record?.bone;
10066
+ const anchor = bone2 === void 0 ? void 0 : anchorOf(bone2);
10067
+ if (anchor === void 0) continue;
10068
+ const state = ctx.states.get(c.name);
10069
+ if (state === void 0 || state.pending) continue;
10070
+ affected.push({ name: c.name, anchor });
10071
+ }
10072
+ if (affected.length === 0) return;
10073
+ if (mode === "reset") {
10074
+ for (const { name } of affected) {
10075
+ const s = ctx.states.get(name);
10076
+ s.remaining = 0;
10077
+ s.lastTime = ctx.time;
10078
+ s.pending = true;
10079
+ s.xOffset = s.xLag = s.xVelocity = 0;
10080
+ s.yOffset = s.yLag = s.yVelocity = 0;
10081
+ s.rotateOffset = s.rotateLag = s.rotateVelocity = 0;
10082
+ s.scaleOffset = s.scaleLag = s.scaleVelocity = 0;
10083
+ }
10084
+ return;
10085
+ }
10086
+ const anchors = new Set(affected.map((a) => a.anchor));
10087
+ const worlds = (pose) => {
10088
+ const out = /* @__PURE__ */ new Map();
10089
+ for (const b of pose.bones) if (anchors.has(b.name)) out.set(b.name, b);
10090
+ return out;
10091
+ };
10092
+ const old = worlds(this.rawPose);
10093
+ const snapshot = captureStepperState(stepper);
10094
+ const now = worlds(stepper.step(0));
10095
+ restoreStepperState(stepper, snapshot);
10096
+ const maps = /* @__PURE__ */ new Map();
10097
+ for (const anchor of anchors) {
10098
+ const o = old.get(anchor);
10099
+ const n = now.get(anchor);
10100
+ if (o === void 0 || n === void 0) continue;
10101
+ if (this.carryPlant) {
10102
+ maps.set(anchor, [1, 0, 0, 1, 0, 0]);
10103
+ continue;
10104
+ }
10105
+ const det = o.a * o.d - o.b * o.c;
10106
+ if (!Number.isFinite(det) || det === 0) continue;
10107
+ const ia = o.d / det;
10108
+ const ib = -o.b / det;
10109
+ const ic = -o.c / det;
10110
+ const id = o.a / det;
10111
+ const a = n.a * ia + n.b * ic;
10112
+ const b = n.a * ib + n.b * id;
10113
+ const c = n.c * ia + n.d * ic;
10114
+ const d = n.c * ib + n.d * id;
10115
+ maps.set(anchor, [a, b, c, d, n.worldX - (a * o.worldX + b * o.worldY), n.worldY - (c * o.worldX + d * o.worldY)]);
10116
+ }
10117
+ const states = physicsContextOf(stepper).states;
10118
+ for (const { name, anchor } of affected) {
10119
+ const m = maps.get(anchor);
10120
+ const s = states.get(name);
10121
+ if (m === void 0 || s === void 0) continue;
10122
+ const [a, b, c, d, x, y] = m;
10123
+ const ux = s.ux;
10124
+ s.ux = a * ux + b * s.uy + x;
10125
+ s.uy = c * ux + d * s.uy + y;
10126
+ const cx = s.cx;
10127
+ s.cx = a * cx + b * s.cy + x;
10128
+ s.cy = c * cx + d * s.cy + y;
10129
+ const tx = s.tx;
10130
+ s.tx = a * tx + b * s.ty;
10131
+ s.ty = c * tx + d * s.ty;
10132
+ }
10133
+ }
10134
+ /**
10135
+ * The composition the held pose draws: `activeSet`, except before an ease's
10136
+ * swap (#174), when it is the composition the ease started from.
10137
+ */
10138
+ shownSet() {
10139
+ const easing = this.easing;
10140
+ return easing === null || easing.swapped ? this.activeSet : easing.fromSet;
10141
+ }
10142
+ /** The local an eased bone shows at the ease's current progress, given the clip's local `clip`. */
10143
+ easedLocal(easing, from, to, clip) {
10144
+ const start = from ?? clip;
10145
+ const end = to ?? clip;
10146
+ const u = easeProgress(easing);
10147
+ const w = this.easePlant === "overshoot" ? u < 1 ? 1.2 * u : 1 : smoothstep(u);
10148
+ return start + normaliseDegrees(end - start) * w;
10149
+ }
10150
+ /**
10151
+ * The ease's part of `adjustHook` (#174): every drawn bone the ease does not
10152
+ * turn takes its drawn local, as without an ease; every eased bone reads
10153
+ * the clip's local (what the timelines left) and then takes its blend.
10154
+ */
10155
+ applyEase(easing, locals) {
10156
+ for (const bone2 of easing.bones.keys()) easing.clip.set(bone2, locals.bone(bone2).rotation);
10157
+ if (this.drawnPose !== null) {
10158
+ for (const [bone2, rotation] of this.drawnPose) if (!easing.bones.has(bone2)) locals.bone(bone2).rotation = rotation;
10159
+ }
10160
+ for (const [bone2, { from, to }] of easing.bones) {
10161
+ locals.bone(bone2).rotation = this.easedLocal(easing, from, to, easing.clip.get(bone2));
10162
+ }
10163
+ }
10164
+ /**
10165
+ * Starts an eased switch (#174) to the sets already made active, from what
10166
+ * the held pose shows: an eased bone's start is the local it shows (fixed),
10167
+ * or the clip's value for a bone that showed the clip's; its end is the new
10168
+ * drawn local, or the clip's value for a bone whose set went. Returns false,
10169
+ * changing nothing, when there is nothing to ease — before `play` (no step
10170
+ * to ease over) or when no bone's rotation changes — and the call is then
10171
+ * an instant one.
10172
+ */
10173
+ startEase(duration, shownBefore, drawn, physics) {
10174
+ const stepper = this.tracks ?? this.track;
10175
+ if (stepper === null || this.pose === null) return false;
10176
+ const before = this.drawnPose;
10177
+ const after = drawn === void 0 ? null : drawnLocals(this.view, drawn);
10178
+ const previous = this.easing;
10179
+ const candidates = drawnChanged(before, after);
10180
+ if (previous !== null) for (const bone2 of previous.bones.keys()) candidates.add(bone2);
10181
+ const bones = /* @__PURE__ */ new Map();
10182
+ for (const bone2 of candidates) {
10183
+ const to = after?.get(bone2) ?? null;
10184
+ let from;
10185
+ const eased = previous?.bones.get(bone2);
10186
+ if (previous !== null && eased !== void 0) {
10187
+ from = previous.elapsed > 0 ? this.easedLocal(previous, eased.from, eased.to, previous.clip.get(bone2)) : eased.from;
10188
+ } else {
10189
+ from = before?.get(bone2) ?? null;
10190
+ }
10191
+ if (from === to) continue;
10192
+ bones.set(bone2, { from, to });
10193
+ }
10194
+ if (bones.size === 0) return false;
10195
+ const easing = {
10196
+ duration,
10197
+ elapsed: 0,
10198
+ bones,
10199
+ clip: /* @__PURE__ */ new Map(),
10200
+ fromSet: shownBefore,
10201
+ swapped: false,
10202
+ physics
10203
+ };
10204
+ if (this.easePlant === "swap-at-0" || this.swapsNow(0, this.lastStep / duration)) easing.swapped = true;
10205
+ this.easing = easing;
10206
+ this.drawnPose = after;
10207
+ if (physics === "reset") this.handOverPhysics(new Set(bones.keys()), "reset");
10208
+ this.reposeDrawn();
10209
+ return true;
10210
+ }
10211
+ /**
10212
+ * Whether the frame at progress `u` is the one nearest ½, the next frame
10213
+ * being at `next`: it is when `u` reached ½ or the next one would be
10214
+ * farther from it; a tie (within 1e-9) goes to the later frame.
10215
+ */
10216
+ swapsNow(u, next) {
10217
+ if (u >= 0.5) return true;
10218
+ return next > u && 0.5 - u < next - 0.5 - 1e-9;
10219
+ }
10220
+ /**
10221
+ * One step of an eased switch (#174), before the track steps: the elapsed
10222
+ * time advances by the step, the attachments swap when this is the frame
10223
+ * nearest ½ (`swapsNow`, the next step predicted by this one), and the
10224
+ * ease ends — the instant switch's state, `drawnPose` alone — on the frame
10225
+ * it reaches its duration. With `physics: 'carry'`, the chains under the
10226
+ * eased bones are carried by the turn this frame's blend makes, read by a
10227
+ * trial `step(0)` at the previous time (`handOverPhysics`).
10228
+ */
10229
+ advanceEase(easing, step) {
10230
+ easing.elapsed += step;
10231
+ if (easing.elapsed >= easing.duration) this.easing = null;
10232
+ else if (!easing.swapped && this.swapsNow(easing.elapsed / easing.duration, (easing.elapsed + step) / easing.duration)) easing.swapped = true;
10233
+ if (easing.physics === "carry") this.handOverPhysics(new Set(easing.bones.keys()), "carry");
10234
+ }
9950
10235
  /**
9951
10236
  * Makes `sets` (distinct) the active sets: refuses any two touching one slot
9952
10237
  * before changing anything, then applies their composition to the held pose.
9953
10238
  */
9954
- activate(sets) {
10239
+ activate(sets, physics, ease = 0) {
9955
10240
  for (let i = 0; i < sets.length; i++) {
9956
10241
  for (let j = 0; j < i; j++) {
9957
10242
  const other = this.touched(sets[j]);
@@ -9973,12 +10258,27 @@ class RigPlayer {
9973
10258
  }
9974
10259
  }
9975
10260
  const drawnBefore = this.activeSet?.drawn;
10261
+ const shownBefore = this.shownSet();
9976
10262
  this.activeList = this.setList.filter((s) => sets.includes(s));
9977
10263
  this.activeSet = composeSets(this.activeList);
9978
10264
  this.clearSeekCache();
9979
10265
  const drawn = this.activeSet?.drawn;
10266
+ if (ease > 0 && this.startEase(ease, shownBefore, drawn, physics)) return;
10267
+ const easing = this.easing;
10268
+ if (easing !== null) {
10269
+ this.easing = null;
10270
+ const posedBefore = this.drawnPose;
10271
+ this.drawnPose = drawn === void 0 ? null : drawnLocals(this.view, drawn);
10272
+ const changed = drawnChanged(posedBefore, this.drawnPose);
10273
+ for (const bone2 of easing.bones.keys()) changed.add(bone2);
10274
+ if (physics !== void 0) this.handOverPhysics(changed, physics);
10275
+ this.reposeDrawn();
10276
+ return;
10277
+ }
9980
10278
  if (drawn !== drawnBefore && !(drawn === void 0 && drawnBefore === void 0)) {
10279
+ const posedBefore = this.drawnPose;
9981
10280
  this.drawnPose = drawn === void 0 ? null : drawnLocals(this.view, drawn);
10281
+ if (physics !== void 0) this.handOverPhysics(drawnChanged(posedBefore, this.drawnPose), physics);
9982
10282
  this.reposeDrawn();
9983
10283
  return;
9984
10284
  }
@@ -10170,9 +10470,10 @@ class RigPlayer {
10170
10470
  const setupShownSlots = new Set(setupPose.shown.map((s) => s.slot));
10171
10471
  const rawRowBySlot = new Map(raw.slots.map((r) => [r[0], r]));
10172
10472
  const slots = [];
10473
+ const shownSet = this.shownSet();
10173
10474
  for (let z = 0; z < pose.drawOrder.length; z++) {
10174
10475
  const slotName = pose.drawOrder[z];
10175
- if (this.activeSet !== null && hiddenBySet(slotName, this.activeSet)) {
10476
+ if (shownSet !== null && hiddenBySet(slotName, shownSet)) {
10176
10477
  slots.push(Object.freeze({
10177
10478
  slot: slotName,
10178
10479
  z,
@@ -10207,7 +10508,7 @@ class RigPlayer {
10207
10508
  continue;
10208
10509
  }
10209
10510
  const rawRow = rawRowBySlot.get(slotName);
10210
- const overriddenBySet = this.activeSet !== null && slotName in this.activeSet.slots;
10511
+ const overriddenBySet = shownSet !== null && slotName in shownSet.slots;
10211
10512
  const setupHadAttachment = setupShownSlots.has(slotName);
10212
10513
  const isTimelineNull = !overriddenBySet && (setupHadAttachment && rawRow?.[1] === null || this.isSlotKeyedNullByTimeline(slotName, raw.animationTime));
10213
10514
  slots.push(Object.freeze({
@@ -10272,6 +10573,7 @@ class RigPlayer {
10272
10573
  this.renderer.dispose();
10273
10574
  this.clearSeekCache();
10274
10575
  this.cachedIdentity = null;
10576
+ this.easing = null;
10275
10577
  this.tracks = null;
10276
10578
  this.track = null;
10277
10579
  this.pose = null;