@rig-c/play 1.15.0 → 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
@@ -603,6 +603,46 @@ 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
+
606
646
  `sets.json` keys are read strictly. Besides `slots`, `hides` and `drawn`, a
607
647
  set may carry `drawnLength`, `registration` and `threshold`, which parts
608
648
  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,28 @@ 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 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
+ }
8965
8987
  function predicateOf(names) {
8966
8988
  const allowed = new Set(names);
8967
8989
  return (slot) => allowed.has(slot);
@@ -9261,6 +9283,8 @@ class RigPlayer {
9261
9283
  touchedSets = /* @__PURE__ */ new Map();
9262
9284
  /** The active sets' drawn bones as local rotations, applied before `adjust` on every pose (#170); null when none draws. */
9263
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;
9264
9288
  adjustFn = null;
9265
9289
  /** Current preview patches, reapplied through the public core API on replay; no edit history. */
9266
9290
  physicsPatches = /* @__PURE__ */ new Map();
@@ -9852,11 +9876,12 @@ class RigPlayer {
9852
9876
  * A name not in `sets.json` throws by name; a build without `sets.json` has
9853
9877
  * `sets()` empty and `setSet` throws.
9854
9878
  */
9855
- setSet(name) {
9879
+ setSet(name, options) {
9856
9880
  if (this.setList.length === 0) {
9857
9881
  throw new Error(`cannot setSet(${JSON.stringify(name)}): this model has no sets.json`);
9858
9882
  }
9859
- this.activate(name === null ? [] : [this.namedSet(name)]);
9883
+ const physics = setPhysicsOption("setSet", options);
9884
+ this.activate(name === null ? [] : [this.namedSet(name)], physics);
9860
9885
  }
9861
9886
  /**
9862
9887
  * Makes exactly `names` the active attachment sets (#168), together: each
@@ -9865,43 +9890,59 @@ class RigPlayer {
9865
9890
  * companion). Composition does not depend on the order of `names`. An
9866
9891
  * unknown name, a repeated name or two sets touching one slot throw by name
9867
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.
9868
9906
  */
9869
- setSets(names) {
9907
+ setSets(names, options) {
9870
9908
  if (this.setList.length === 0) {
9871
9909
  throw new Error(`cannot setSets(${JSON.stringify(names)}): this model has no sets.json`);
9872
9910
  }
9911
+ const physics = setPhysicsOption("setSets", options);
9873
9912
  const sets = names.map((name) => this.namedSet(name));
9874
9913
  for (let i = 0; i < sets.length; i++) {
9875
9914
  for (let j = 0; j < i; j++) {
9876
9915
  if (sets[i] === sets[j]) throw new Error(`set ${JSON.stringify(sets[i].name)} is named twice in setSets`);
9877
9916
  }
9878
9917
  }
9879
- this.activate(sets);
9918
+ this.activate(sets, physics);
9880
9919
  }
9881
9920
  /**
9882
9921
  * Activates `name` beside the sets already active (#168): a set already
9883
9922
  * active is left as it is; one touching a slot an active set touches throws
9884
9923
  * by name (the slot and both sets) and changes nothing.
9885
9924
  */
9886
- addSet(name) {
9925
+ addSet(name, options) {
9887
9926
  if (this.setList.length === 0) {
9888
9927
  throw new Error(`cannot addSet(${JSON.stringify(name)}): this model has no sets.json`);
9889
9928
  }
9929
+ const physics = setPhysicsOption("addSet", options);
9890
9930
  const found = this.namedSet(name);
9891
9931
  if (this.activeList.includes(found)) return;
9892
- this.activate([...this.activeList, found]);
9932
+ this.activate([...this.activeList, found], physics);
9893
9933
  }
9894
9934
  /**
9895
9935
  * Deactivates `name`, keeping every other active set (#168). An unknown name
9896
9936
  * throws by name; a set that is not active is left as it is.
9897
9937
  */
9898
- removeSet(name) {
9938
+ removeSet(name, options) {
9899
9939
  if (this.setList.length === 0) {
9900
9940
  throw new Error(`cannot removeSet(${JSON.stringify(name)}): this model has no sets.json`);
9901
9941
  }
9942
+ const physics = setPhysicsOption("removeSet", options);
9902
9943
  const found = this.namedSet(name);
9903
9944
  if (!this.activeList.includes(found)) return;
9904
- this.activate(this.activeList.filter((s) => s !== found));
9945
+ this.activate(this.activeList.filter((s) => s !== found), physics);
9905
9946
  }
9906
9947
  /** The declared set named `name`, or a throw naming it and the available sets. */
9907
9948
  namedSet(name) {
@@ -9947,11 +9988,102 @@ class RigPlayer {
9947
9988
  }
9948
9989
  return touched;
9949
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
+ }
9950
10082
  /**
9951
10083
  * Makes `sets` (distinct) the active sets: refuses any two touching one slot
9952
10084
  * before changing anything, then applies their composition to the held pose.
9953
10085
  */
9954
- activate(sets) {
10086
+ activate(sets, physics) {
9955
10087
  for (let i = 0; i < sets.length; i++) {
9956
10088
  for (let j = 0; j < i; j++) {
9957
10089
  const other = this.touched(sets[j]);
@@ -9978,7 +10110,9 @@ class RigPlayer {
9978
10110
  this.clearSeekCache();
9979
10111
  const drawn = this.activeSet?.drawn;
9980
10112
  if (drawn !== drawnBefore && !(drawn === void 0 && drawnBefore === void 0)) {
10113
+ const posedBefore = this.drawnPose;
9981
10114
  this.drawnPose = drawn === void 0 ? null : drawnLocals(this.view, drawn);
10115
+ if (physics !== void 0) this.handOverPhysics(posedBefore, this.drawnPose, physics);
9982
10116
  this.reposeDrawn();
9983
10117
  return;
9984
10118
  }