@rig-c/core 2.75.0 → 2.76.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/docs/AUTHORING.md CHANGED
@@ -12647,7 +12647,7 @@ verdict on those figures.
12647
12647
  | <a id="CE_COMPILE_ALPHA_CLIP_CONFLICT"></a>`CE_COMPILE_ALPHA_CLIP_CONFLICT` | Names authored fields or convex/inverse flags conflicting with generated inside-clipping geometry. Use one authored/generated form.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦: alphaClip conflicts with [⟧<br>⟦vertexCount⟧<br>⟦vertices⟧<br>⟦weights⟧<br>⟦boneIndexing⟧<br>⟦, ⟧<br>⟦], convex=⟧<br>⟦, inverse=⟧<br>⟦; required ordinary inside clipping with generated geometry only. Remove authored geometry/flags.⟧ |
12648
12648
  | <a id="CE_COMPILE_ALPHA_CLIP_DEFORM"></a>`CE_COMPILE_ALPHA_CLIP_DEFORM` | Names animation and source/clip key/frame counts that cannot share the existing projection contract. Give matching explicit supported transform keys in compatible actual frames, with constant world shares where needed, or author the corresponding clip geometry and motion; weighted binding does not copy source deform.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦ alphaClip from "⟧<br>⟦"⟧<br>⟦: animation "⟧<br>⟦" has ⟧<br>⟦ source and ⟧<br>⟦ clip keys; source frame=⟧<br>⟦no vertex geometry⟧<br>⟦clip frame=⟧<br>⟦, constant world influence shares=⟧<br>⟦Required matching explicit affine/depth-yaw/pitch transforms in compatible DeformGeometry frames, ⟧<br>⟦with constant shares for world-space transfer. Fix the named frame/keys or author corresponding clip motion.⟧ |
12649
12649
 
12650
- ## 15. Posing in place for a player — the runtime poser (stage 2)
12650
+ ## 15. Posing in place for a player — the runtime poser (stage 3)
12651
12651
 
12652
12652
  rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
12653
12653
  (`src/core/`) is the reference: each pose is a pure function of the document,
@@ -12682,11 +12682,11 @@ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeho
12682
12682
  The display `name` may be shared by different placeholders; it is never a table
12683
12683
  key to guess the source from. These load-time strings do not change the pose path.
12684
12684
 
12685
- | | what stage 2 poses |
12685
+ | | what stage 3 poses |
12686
12686
  | --- | --- |
12687
12687
  | bones | local and world transforms, all five inherit modes, skin-required bones under the view's skin (an inactive bone is zeros, as the oracle poses it) |
12688
12688
  | timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, `rgba`, `rgb` and `alpha`; ik and transform timelines; linear, stepped and Bézier keys, each read from a per-pose key cursor |
12689
- | constraints | one- and two-bone ik; world-space, absolute transform constraints; the document's order, each followed by the bones below what it moved posed again |
12689
+ | constraints | physics with its exact clock, reset and parameter timelines; value-mapped sliders with bone/slot/deform/physics composition; one- and two-bone ik; world-space, absolute transform constraints; the document's order, each followed by the bones below what it moved posed again |
12690
12690
  | vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
12691
12691
  | deform | weighted and unweighted mesh timelines, linear/stepped/Bézier, per-pose key cursors and in-place buffers; linked meshes inherit their source's timelines only with `timelines: true` |
12692
12692
  | skins and events | named/no-skin views with their skin-required bones and constraints; event delivery in key order over `(last, time]`, tail then head across a wrap |
@@ -12729,9 +12729,43 @@ back to the definition's **volume**, matching the measured runtime rather than
12729
12729
  inventing a correction. An explicit `ignore: ['events']` diagnostic still mutes
12730
12730
  the list; normal compilation never does.
12731
12731
 
12732
+ Physics is explicit on a direct pose: `apply(time, 'none')` leaves physics inert;
12733
+ `apply(time, 'reset', delta)` or `apply(time, 'update', delta)` first applies
12734
+ reset keys over `(last, time]` against the old clock, then adds `delta` to the
12735
+ physics clock and solves. A backwards animation time crosses the tail then head
12736
+ of the interval, as a looping track does. `step(dt)` updates physics and the
12737
+ looping track; it takes the initial reset at animation zero if needed.
12738
+ `resetPhysics('animation')` starts at that animation's pose zero;
12739
+ `resetPhysics('setup')` starts from setup before applying the animation.
12740
+ The physics clock is a running sum, never the animation time; constructor option
12741
+ `{ clock: physicsClockStep }` can select the render clock without changing track
12742
+ time. `physicsTime` exposes that sum. A non-finite or negative delta/clock result
12743
+ throws `RuntimeInputError` naming the call. `physicsTranslate(x, y)` and
12744
+ `physicsRotate(x, y, degrees)` shift the previous origins; `setEnvironment({
12745
+ wind: [x, y], gravity: [x, y] })` supplies the skeleton directions (positive y
12746
+ points down), all numbers finite. Offsets, velocities, lags and buffers belong
12747
+ to each pose; a zero physics mix does not consume its constraint's clock.
12748
+
12749
+ A slider reads its dial where earlier constraints left it: local or world
12750
+ property mapped as `to + (value - from) * scale`, clamped at zero or looped by
12751
+ its animation duration. A boneless slider uses its keyed `time`. The selected
12752
+ animation is applied at that value, never advanced as a track, and fires no
12753
+ events or physics reset keys. Mix zero does not re-pose bones; nonzero mix
12754
+ re-poses every keyed bone even before its first key. Its bone targets compose
12755
+ in authored order, including repeated names; each entry uses the same setup
12756
+ values and blends from the current pose. Ordinary tracks still read only the
12757
+ last target for each bone. An earlier unsupported inherit target is refused
12758
+ when a slider applies it, even if the track's last target would hide it.
12759
+ Absolute/additive bone and
12760
+ deform blends and slot switches follow the oracle, including direct scale and
12761
+ colour targets at mix one. Slider physics writes last one pass and affect only
12762
+ constraints later in update order. An unsupported timeline in a slider's
12763
+ animation refuses the whole model under that timeline's existing feature name,
12764
+ so an unselected track cannot hide a construct the slider always applies.
12765
+
12732
12766
  **Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
12733
12767
  every construct met, each `{ feature, detail, animation }`, and the message
12734
- reads `not posed by the runtime poser (stage 2), N construct(s) missing:
12768
+ reads `not posed by the runtime poser (stage 3), N construct(s) missing:
12735
12769
  <feature>: <detail>; …`. A model-level feature refuses the whole model; an
12736
12770
  animation-level one refuses that animation alone (`model.refused`;
12737
12771
  `model.animation(name)` and `setAnimation(name)` throw its refusal) and every
@@ -12740,7 +12774,7 @@ refused: nothing is posed approximately.
12740
12774
 
12741
12775
  | feature | refuses | what it is |
12742
12776
  | --- | --- | --- |
12743
- | `path-constraint`, `physics-constraint`, `slider-constraint` | the model | a constraint of that kind |
12777
+ | `path-constraint` | the model | a constraint of that kind |
12744
12778
  | `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
12745
12779
  | `inactive-constrained-bone` | the model | an applied ik or transform constraint naming a bone the view leaves inactive (what the oracle writes there depends on the pass before) |
12746
12780
  | `noscale-world-target` | the model | a world-space transform onto a `noScale` or `noScaleOrReflection` bone, whose local values would be read back in that mode |
@@ -12755,7 +12789,7 @@ this document's [...]`, `apply: the time NaN is not finite`.
12755
12789
 
12756
12790
  **The gate.** `tools/core_gate.ts` runs a third dumper (`runtimeDump` in
12757
12791
  `tools/pose_oracle.ts`) over every row it builds, at the oracle's samples, and
12758
- compares its document with the oracle's in full doubles at tolerance 0 twice:
12792
+ compares both unstepped and stepped documents with the oracle's in full doubles at tolerance 0 twice:
12759
12793
  through `compare` and cell by cell (which reads the bones `compare`'s
12760
12794
  ill-conditioned rule leaves out). One `RUNTIME` line per row reads `IDENTICAL`
12761
12795
  (covered whole and equal), `SKIP` (refused at load, or animations or events left
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/core",
3
- "version": "2.75.0",
3
+ "version": "2.76.0",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -78,7 +78,9 @@ export function compileCurve(keys: readonly CoreKey[], channels: number): Runtim
78
78
  /** The last key at or before `t` (−1 before the first), found from `cursors[slot]` and left there — the header's first read. */
79
79
  export function keyAt(curve: RuntimeCurve, cursors: Int32Array, slot: number, t: number): number {
80
80
  const times = curve.times;
81
- let i = cursors[slot];
81
+ // A slider can evaluate another animation between two reads of this one.
82
+ // Shared cursor slots are only search hints: keep the hint inside THIS curve.
83
+ let i = Math.min(cursors[slot], curve.keys - 1);
82
84
  while (i >= 0 && times[i] > t) i--;
83
85
  while (i + 1 < curve.keys && times[i + 1] <= t) i++;
84
86
  cursors[slot] = i;
@@ -21,12 +21,12 @@
21
21
  * corpus through both at the oracle's samples and compares full doubles at
22
22
  * tolerance 0 (*The runtime poser* there). The oracle is not changed for it.
23
23
  *
24
- * ## Stage 2: what it poses, and what it refuses
24
+ * ## Stage 3: what it poses, and what it refuses
25
25
  *
26
26
  * Bones and their five inherit modes; the rotate, translate, scale and shear
27
27
  * timelines; a slot's attachment and colour (`rgba`, `rgb`, `alpha`)
28
28
  * timelines; weighted/unweighted deform timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
29
- * two-bone ik and world-space, absolute transform constraints with their
29
+ * two-bone ik, world-space absolute transforms, physics and value-mapped sliders with their
30
30
  * timelines; skin-required bones and constraints under the view's skin. One
31
31
  * animation at full weight from the setup pose, at a time or stepped as a
32
32
  * looping track. Everything else is refused at load by name with the list of
@@ -81,4 +81,5 @@ export {
81
81
  L_SHEAR_Y,
82
82
  } from './model.ts';
83
83
  export type { RuntimeAnimation, RuntimeEvent, RuntimeAttachment, RuntimeFeature, RuntimeMissing, RuntimeModel, RuntimeOptions, RuntimePlant, RuntimeSlot } from './model.ts';
84
+ export type { RuntimePoseOptions, RuntimePhysicsPhase } from './pose.ts';
84
85
  export { RuntimePose } from './pose.ts';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The runtime poser's load-time half (issue #1591, stage 2): a skin view of
2
+ * The runtime poser's load-time half (issue #1591, stage 3): a skin view of
3
3
  * the compiled model (`readModel`, then `underSkin` / `underNoSkin` — the
4
4
  * oracle's reader and its views, read and never written) compiled once into
5
5
  * index-resolved bones and slots, a constraint plan, compiled timelines and
@@ -12,7 +12,7 @@
12
12
  * (`RuntimeMissing`), and:
13
13
  *
14
14
  * - **refuses the model** (`RuntimeRefusal`, every missing feature listed)
15
- * when one is a property of the whole model — a path, physics or slider constraint, a transform constraint reading or
15
+ * when one is a property of the whole model — a path constraint, a transform constraint reading or
16
16
  * writing local values or adding (`localSource`, `localTarget`,
17
17
  * `additive`), an ik or transform constraint over a bone the view leaves
18
18
  * inactive, a world-space transform onto a `noScale` or
@@ -26,9 +26,9 @@
26
26
  * attachment key showing a clipping attachment or a region with no atlas
27
27
  * rectangle. Every other animation of the model is posed.
28
28
  *
29
- * `RUNTIME_FEATURES` is the closed list of what can be named. Stage 2 adds mesh deform timelines to the
30
- * named/no-skin views and event keys, with immutable payloads compiled
31
- * at load. `ignore: ['events']` remains an explicit diagnostic opt-out; it is
29
+ * `RUNTIME_FEATURES` is the closed list of what can be named. Stage 3 adds indexed physics and value-mapped sliders, including their
30
+ * parameter timelines and per-pass writes. Mesh deforms, named/no-skin views
31
+ * and immutable event deliveries remain covered. `ignore: ['events']` remains an explicit diagnostic opt-out; it is
32
32
  * never the default. A merged view with conflicting attachments still refuses
33
33
  * by name, as the oracle itself cannot resolve that view without file order.
34
34
  *
@@ -50,16 +50,16 @@ import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, rea
50
50
  import { constraintInactiveWhy, constraintsAbsentWhy, TRANSFORM_PROPERTIES, type CoreIkRecord, type CoreTransformRecord } from '../core/constraints.ts';
51
51
  import { RUNTIME_PI } from '../core/world.ts';
52
52
  import { heldArray, timelineIdentity } from '../core/deform.ts';
53
+ import type { CorePhysicsRecord, PhysicsTimelineKind } from '../core/constraints_physics.ts';
54
+ import type { CoreSliderRecord } from '../core/constraints_slider.ts';
53
55
  import { compileCurve, compileDeformCurve, type RuntimeCurve } from './curves.ts';
54
56
 
55
57
  /** The stage of issue #1591 this poser implements. */
56
- export const RUNTIME_STAGE = 2;
58
+ export const RUNTIME_STAGE = 3;
57
59
 
58
60
  /** Every construct the runtime poser can refuse, by the name its refusals carry. */
59
61
  export const RUNTIME_FEATURES = [
60
62
  'path-constraint',
61
- 'physics-constraint',
62
- 'slider-constraint',
63
63
  'local-transform',
64
64
  'additive-transform',
65
65
  'inactive-constrained-bone',
@@ -126,6 +126,8 @@ export interface RuntimePlant {
126
126
  /** The old restore behavior, which repeats keys up to the restored time. */
127
127
  readonly restoreEventsFromStart?: boolean;
128
128
  /** Omit deform / multiply offsets by weights too early: negative controls only. */
129
+ readonly omitPhysics?: boolean;
130
+ readonly sliderAsTrack?: boolean;
129
131
  readonly omitDeform?: boolean;
130
132
  readonly deformAfterWeights?: boolean;
131
133
  }
@@ -257,7 +259,41 @@ export interface RuntimeTransform {
257
259
  readonly clamp: boolean;
258
260
  }
259
261
 
260
- export type RuntimeConstraint = RuntimeIk | RuntimeTransform;
262
+ export interface RuntimePhysics {
263
+ readonly kind: 'physics';
264
+ readonly name: string;
265
+ readonly index: number;
266
+ readonly applied: boolean;
267
+ readonly bones: Int32Array;
268
+ readonly setup: CorePhysicsRecord;
269
+ }
270
+
271
+ export interface RuntimeSlider {
272
+ readonly kind: 'slider';
273
+ readonly name: string;
274
+ readonly index: number;
275
+ readonly applied: boolean;
276
+ readonly bones: Int32Array;
277
+ readonly bone: number;
278
+ readonly property: number;
279
+ readonly setup: CoreSliderRecord;
280
+ /** Resolved after every animation is compiled. Never a track. */
281
+ animation: RuntimeAnimation;
282
+ }
283
+
284
+ export interface RuntimePhysicsTimeline {
285
+ readonly kind: PhysicsTimelineKind;
286
+ readonly targets: Int32Array;
287
+ readonly curve: RuntimeCurve;
288
+ readonly cursor: number;
289
+ }
290
+ export interface RuntimeSliderTimeline {
291
+ readonly time: RuntimeCurve | null;
292
+ readonly mix: RuntimeCurve | null;
293
+ readonly timeCursor: number;
294
+ readonly mixCursor: number;
295
+ }
296
+ export type RuntimeConstraint = RuntimeIk | RuntimeTransform | RuntimePhysics | RuntimeSlider;
261
297
 
262
298
  /** A bone timeline: the bone, its curve, and per channel the local field it writes and whether it multiplies the setup value (scale) or adds to it. */
263
299
  export interface RuntimeBoneTimeline {
@@ -310,7 +346,11 @@ export interface RuntimeAnimation {
310
346
  readonly name: string;
311
347
  readonly duration: number;
312
348
  readonly bones: readonly RuntimeBoneTimeline[];
349
+ /** Every authored target in order; sliders compose duplicates that tracks discard. */
350
+ readonly sliderBones: readonly RuntimeBoneTimeline[];
313
351
  readonly deforms: readonly RuntimeDeformTimeline[];
352
+ readonly physics: readonly RuntimePhysicsTimeline[];
353
+ readonly sliders: ReadonlyArray<RuntimeSliderTimeline | null>;
314
354
  readonly slots: readonly RuntimeSlotTimeline[];
315
355
  /** Per constraint of the plan, its timeline in this animation or `null`. */
316
356
  readonly constraints: ReadonlyArray<RuntimeConstraintTimeline | null>;
@@ -326,6 +366,8 @@ export interface RuntimeModel {
326
366
  /** Control-only boundary defect; fixed at load. */
327
367
  readonly omitDeform: boolean;
328
368
  readonly deformAfterWeights: boolean;
369
+ readonly omitPhysics: boolean;
370
+ readonly sliderAsTrack: boolean;
329
371
  readonly deformCount: number;
330
372
  readonly eventWrapOffByOne: boolean;
331
373
  readonly restoreEventsFromStart: boolean;
@@ -449,13 +491,21 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
449
491
 
450
492
  // --- constraints, in the document's order ---
451
493
  const constraints: RuntimeConstraint[] = [];
452
- for (const c of doc.constraints) if (c.kind === 'path' || c.kind === 'physics' || c.kind === 'slider') refusals.note(`${c.kind}-constraint`, `${c.kind} constraint "${c.name}"`);
494
+ for (const c of doc.constraints) if (c.kind === 'path') refusals.note(`${c.kind}-constraint`, `${c.kind} constraint "${c.name}"`);
453
495
  for (const r of constraintRecords(doc)) {
454
- if (r.kind !== 'ik' && r.kind !== 'transform') continue;
496
+ if (r.kind === 'path') continue;
455
497
  const applied = constraintInactiveWhy(r, active) === null;
456
- const bones = Int32Array.from(r.bones.map(indexOf));
457
- if (applied && r.bones.some((b) => !active.has(b))) refusals.note('inactive-constrained-bone', `${r.kind} constraint "${r.name}" names bone(s) ${r.bones.filter((b) => !active.has(b)).map((b) => `"${b}"`).join(', ')}, which the view leaves inactive — what the oracle writes there depends on the pass before`);
498
+ const bones = Int32Array.from((r.kind === 'physics' ? [r.bone] : r.kind === 'slider' ? r.timelines.bones.filter(t => t.timelines.length > 0).map(t => t.name) : r.bones).map(indexOf));
458
499
  const index = constraints.length;
500
+ if (r.kind === 'physics') {
501
+ constraints.push({ kind: 'physics', name: r.name, index, applied, bones, setup: r });
502
+ continue;
503
+ }
504
+ if (r.kind === 'slider') {
505
+ constraints.push({ kind: 'slider', name: r.name, index, applied, bones, setup: r, bone: r.bone === null ? -1 : indexOf(r.bone), property: TRANSFORM_PROPERTIES.indexOf(r.property), animation: null as unknown as RuntimeAnimation });
506
+ continue;
507
+ }
508
+ if (applied && r.bones.some((b) => !active.has(b))) refusals.note('inactive-constrained-bone', `${r.kind} constraint "${r.name}" names bone(s) ${r.bones.filter((b) => !active.has(b)).map((b) => `"${b}"`).join(', ')}, which the view leaves inactive — what the oracle writes there depends on the pass before`);
459
509
  if (r.kind === 'ik') constraints.push(compileIk(r, index, applied, bones, indexOf));
460
510
  else {
461
511
  if (r.localSource || r.localTarget) refusals.note('local-transform', `transform constraint "${r.name}" ${[r.localSource ? 'reads local values (localSource)' : '', r.localTarget ? 'writes local values (localTarget)' : ''].filter((x) => x !== '').join(' and ')}`);
@@ -573,9 +623,16 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
573
623
  }
574
624
  }
575
625
 
626
+ for (const c of constraints) if (c.kind === 'slider') {
627
+ const a = animations.get(c.setup.animation);
628
+ if (a !== undefined) c.animation = a;
629
+ else for (const m of refusals.byAnimation.get(c.setup.animation) ?? []) refusals.note(m.feature, `slider "${c.name}" applies refused animation "${c.setup.animation}": ${m.detail}`);
630
+ }
576
631
  if (refusals.model.length > 0) throw new RuntimeRefusal(refusals.model);
577
632
  const names = doc.animations.map((a) => a.name);
578
633
  return {
634
+ omitPhysics: plant.omitPhysics === true,
635
+ sliderAsTrack: plant.sliderAsTrack === true,
579
636
  omitDeform: plant.omitDeform === true,
580
637
  deformAfterWeights: plant.deformAfterWeights === true,
581
638
  deformCount,
@@ -714,18 +771,32 @@ function compileAnimation(
714
771
 
715
772
  const lastBy = <T extends { name: string }>(targets: readonly T[]): Map<string, T> => new Map(targets.map((t) => [t.name, t]));
716
773
  const bones: RuntimeBoneTimeline[] = [];
717
- for (const target of lastBy(tl.bones).values()) {
774
+ type BoneTarget = (typeof tl.bones)[number];
775
+ const byTarget = new Map<BoneTarget, RuntimeBoneTimeline[]>();
776
+ const compileBones = (target: BoneTarget, forSlider: boolean): RuntimeBoneTimeline[] => {
777
+ const cached = byTarget.get(target);
778
+ if (cached !== undefined) return cached;
779
+ const keyed: RuntimeBoneTimeline[] = [];
780
+ byTarget.set(target, keyed);
718
781
  const bone = boneIndex.get(target.name);
719
- if (bone === undefined) continue;
782
+ if (bone === undefined) return keyed;
720
783
  for (const t of target.timelines) {
721
784
  if (t.kind === 'inherit') {
722
- refusals.note('inherit-timeline', `bone "${target.name}" keys its inherit mode`, name);
785
+ if (!forSlider) refusals.note('inherit-timeline', `bone "${target.name}" keys its inherit mode`, name);
786
+ else for (const c of constraints) if (c.kind === 'slider' && c.setup.animation === name) {
787
+ refusals.note('inherit-timeline', `slider "${c.name}" applies animation "${name}" whose bone "${target.name}" keys its inherit mode`);
788
+ }
723
789
  continue;
724
790
  }
725
791
  const spec = BONE_KINDS[t.kind];
726
- bones.push({ bone, curve: compileCurve(t.keys, spec.length), cursor: cursors++, fields: Int32Array.from(spec.map((s) => s[0])), multiply: Uint8Array.from(spec.map((s) => s[1])) });
792
+ keyed.push({ bone, curve: compileCurve(t.keys, spec.length), cursor: cursors++, fields: Int32Array.from(spec.map((s) => s[0])), multiply: Uint8Array.from(spec.map((s) => s[1])) });
727
793
  }
728
- }
794
+ return keyed;
795
+ };
796
+ // Keep the track's last-target Map order and reuse those exact curves/cursors.
797
+ for (const target of lastBy(tl.bones).values()) bones.push(...compileBones(target, false));
798
+ const sliderBones = constraints.some(c => c.kind === 'slider' && c.setup.animation === name)
799
+ ? tl.bones.flatMap(target => compileBones(target, true)) : [];
729
800
  const slotTimelines: RuntimeSlotTimeline[] = [];
730
801
  const keyedSlots = [...lastBy(tl.slots).values()].flatMap((target) => {
731
802
  const si = slotIndex.get(target.name);
@@ -752,12 +823,22 @@ function compileAnimation(
752
823
  const keyed = anim.constraints.ik.find((x) => x.name === c.name);
753
824
  if (keyed === undefined) continue;
754
825
  constraintTimelines[c.index] = { constraint: c.index, curve: compileCurve(keyed.keys, 2), cursor: cursors++, flags: Uint8Array.from(keyed.keys.map((k) => ikFlags(k.flags.bendPositive, k.flags.compress, k.flags.stretch))) };
755
- } else {
826
+ } else if (c.kind === 'transform') {
756
827
  const keyed = anim.constraints.transform.find((x) => x.name === c.name);
757
828
  if (keyed === undefined) continue;
758
829
  constraintTimelines[c.index] = { constraint: c.index, curve: compileCurve(keyed.keys, TRANSFORM_PROPERTIES.length), cursor: cursors++, flags: null };
759
830
  }
760
831
  }
832
+ const sliders = constraints.map((c): RuntimeSliderTimeline | null => {
833
+ if (c.kind !== 'slider') return null;
834
+ const keyed = anim.constraints.slider.find(t => t.name === c.name);
835
+ if (keyed === undefined) return null;
836
+ return { time: keyed.time === null ? null : compileCurve(keyed.time, 1), mix: keyed.mix === null ? null : compileCurve(keyed.mix, 1), timeCursor: cursors++, mixCursor: cursors++ };
837
+ });
838
+ const physics: RuntimePhysicsTimeline[] = (anim.constraints.physicsKeyed ?? []).map(t => ({
839
+ kind: t.kind, cursor: cursors++, curve: compileCurve(t.keys, t.kind === 'reset' ? 0 : 1),
840
+ targets: Int32Array.from(constraints.filter((c): c is RuntimePhysics => c.kind === 'physics' && c.applied && (t.name === '*' ? t.kind === 'reset' || c.setup.global[t.kind] : c.name === t.name)).map(c => c.index)),
841
+ }));
761
842
  const deforms: RuntimeDeformTimeline[] = deform.map(a => {
762
843
  const g = doc.skins.find(s => s.name === a.skin)?.attachments[a.slot]?.[a.attachment]?.geometry;
763
844
  if (g === undefined || g.kind === 'region' || g.kind === 'linkedmesh') throw new RuntimeInputError(`deform "${a.skin}/${a.slot}/${a.attachment}" has no vertex geometry (readModel refuses it first)`);
@@ -765,5 +846,5 @@ function compileAnimation(
765
846
  const arrays = keys.map(k => heldArray(g.vertices, k));
766
847
  return { identity: `${a.skin}/${a.slot}/${a.attachment}`, cursor: cursors++, curve: compileDeformCurve(keys), count: arrays[0].length, held: Float64Array.from(arrays.flat()) };
767
848
  });
768
- return { name, duration: tl.duration, deforms, bones, slots: slotTimelines, constraints: constraintTimelines, cursors, events: tl.events.length, eventKeys: Object.freeze(ignoreEvents ? [] : tl.events.map(key => Object.freeze({ name: key.name, time: key.time, int: key.int, float: key.float, string: key.string, volume: key.volume ?? 0, balance: key.balance ?? 0 }))) };
849
+ return { name, duration: tl.duration, physics, sliders, deforms, bones, sliderBones, slots: slotTimelines, constraints: constraintTimelines, cursors, events: tl.events.length, eventKeys: Object.freeze(ignoreEvents ? [] : tl.events.map(key => Object.freeze({ name: key.name, time: key.time, int: key.int, float: key.float, string: key.string, volume: key.volume ?? 0, balance: key.balance ?? 0 }))) };
769
850
  }
@@ -0,0 +1,235 @@
1
+ /** Indexed in-place physics. Arithmetic and operation order mirror the measured oracle;
2
+ * state is allocated once per pose, never during an update. */
3
+ import type { CorePhysicsRecord } from '../core/constraints_physics.ts';
4
+ import { RUNTIME_PI } from '../core/world.ts';
5
+ const PI2 = RUNTIME_PI * 2;
6
+ const INV_PI2 = 1 / PI2;
7
+ export interface PhysicsState {
8
+ /** The next update takes the bone where it is and integrates nothing: set by a reset and on a constraint never updated. */
9
+ pending: boolean;
10
+ lastTime: number;
11
+ remaining: number;
12
+ ux: number;
13
+ uy: number;
14
+ cx: number;
15
+ cy: number;
16
+ tx: number;
17
+ ty: number;
18
+ xOffset: number;
19
+ xLag: number;
20
+ xVelocity: number;
21
+ yOffset: number;
22
+ yLag: number;
23
+ yVelocity: number;
24
+ rotateOffset: number;
25
+ rotateLag: number;
26
+ rotateVelocity: number;
27
+ scaleOffset: number;
28
+ scaleLag: number;
29
+ scaleVelocity: number;
30
+ }
31
+
32
+ /** A constraint's state before its first update: pending, its clock at 0. */
33
+ export function freshPhysicsState(): PhysicsState {
34
+ return { pending: true, lastTime: 0, remaining: 0, ux: 0, uy: 0, cx: 0, cy: 0, tx: 0, ty: 0, xOffset: 0, xLag: 0, xVelocity: 0, yOffset: 0, yLag: 0, yVelocity: 0, rotateOffset: 0, rotateLag: 0, rotateVelocity: 0, scaleOffset: 0, scaleLag: 0, scaleVelocity: 0 };
35
+ }
36
+
37
+ /** A reset: offsets, velocities and lags cleared, nothing remaining, the clock read at `time`, pending. */
38
+ export function resetPhysicsState(s: PhysicsState, time: number): void {
39
+ s.remaining = 0;
40
+ s.lastTime = time;
41
+ s.pending = true;
42
+ s.xOffset = s.xLag = s.xVelocity = 0;
43
+ s.yOffset = s.yLag = s.yVelocity = 0;
44
+ s.rotateOffset = s.rotateLag = s.rotateVelocity = 0;
45
+ s.scaleOffset = s.scaleLag = s.scaleVelocity = 0;
46
+ }
47
+
48
+ export function stepRuntimePhysics(r: CorePhysicsRecord, s: PhysicsState, w: Float64Array, boneOffset: number, length: number, time: number, reset: boolean, env: Float64Array): boolean {
49
+ const mix = r.mix;
50
+ if (mix === 0) return false;
51
+ const x = r.x > 0, y = r.y > 0, rotateOrShearX = r.rotate > 0 || r.shearX > 0, scaleX = r.scaleX > 0;
52
+ const l = length;
53
+ if (reset) resetPhysicsState(s, time);
54
+ const delta = Math.max(time - s.lastTime, 0);
55
+ // The interpolation weight of the previous update, before this one's time is added: the rotate and the scale-only inertia read the pose as it was shown.
56
+ const shown = Math.max(0, 1 - s.remaining / r.step);
57
+ s.remaining += delta;
58
+ s.lastTime = time;
59
+ const bx = w[boneOffset + 4];
60
+ const by = w[boneOffset + 5];
61
+ let z = 0;
62
+ if (s.pending) {
63
+ s.pending = false;
64
+ s.ux = bx;
65
+ s.uy = by;
66
+ } else {
67
+ let a = s.remaining;
68
+ const i = r.inertia;
69
+ const t = r.step;
70
+ const f = r.referenceScale;
71
+ let d = -1;
72
+ const qx = r.limit * delta;
73
+ const qy = qx;
74
+ if (x || y) {
75
+ if (x) {
76
+ const u = (s.ux - bx) * i;
77
+ s.xOffset += u > qx ? qx : u < -qx ? -qx : u;
78
+ s.ux = bx;
79
+ }
80
+ if (y) {
81
+ const u = (s.uy - by) * i;
82
+ s.yOffset += u > qy ? qy : u < -qy ? -qy : u;
83
+ s.uy = by;
84
+ }
85
+ if (a >= t) {
86
+ d = Math.pow(r.damping, 60 * t);
87
+ const m = r.massInverse * t;
88
+ const e = r.strength;
89
+ const wind = r.wind * f;
90
+ const g = r.gravity * f;
91
+ // The environment's directions (issue #1416), as the runtime weighs them; the skeleton's scale is 1 here, so its factor is left out.
92
+ const ax = wind * env[0] + g * env[2];
93
+ const ay = wind * env[1] + g * env[3];
94
+ const xs = s.xOffset;
95
+ const ys = s.yOffset;
96
+ do {
97
+ if (x) {
98
+ s.xVelocity += (ax - s.xOffset * e) * m;
99
+ s.xOffset += s.xVelocity * t;
100
+ s.xVelocity *= d;
101
+ }
102
+ if (y) {
103
+ s.yVelocity -= (ay + s.yOffset * e) * m;
104
+ s.yOffset += s.yVelocity * t;
105
+ s.yVelocity *= d;
106
+ }
107
+ a -= t;
108
+ } while (a >= t);
109
+ s.xLag = s.xOffset - xs;
110
+ s.yLag = s.yOffset - ys;
111
+ }
112
+ z = Math.max(0, 1 - a / t);
113
+ if (x) w[boneOffset + 4] += (s.xOffset - s.xLag * z) * mix * r.x;
114
+ if (y) w[boneOffset + 5] += (s.yOffset - s.yLag * z) * mix * r.y;
115
+ }
116
+ if (rotateOrShearX || scaleX) {
117
+ const ca = Math.atan2(w[boneOffset + 2], w[boneOffset + 0]);
118
+ let c = 0;
119
+ let sn = 0;
120
+ let mr = 0;
121
+ let dx = s.cx - w[boneOffset + 4];
122
+ let dy = s.cy - w[boneOffset + 5];
123
+ if (dx > qx) dx = qx;
124
+ else if (dx < -qx) dx = -qx;
125
+ if (dy > qy) dy = qy;
126
+ else if (dy < -qy) dy = -qy;
127
+ if (rotateOrShearX) {
128
+ mr = (r.rotate + r.shearX) * mix;
129
+ const lagged = s.rotateLag * shown;
130
+ let rr = Math.atan2(dy + s.ty, dx + s.tx) - ca - (s.rotateOffset - lagged) * mr;
131
+ s.rotateOffset += (rr - Math.ceil(rr * INV_PI2 - 0.5) * PI2) * i;
132
+ rr = (s.rotateOffset - lagged) * mr + ca;
133
+ c = Math.cos(rr);
134
+ sn = Math.sin(rr);
135
+ if (scaleX) {
136
+ rr = l * Math.sqrt(w[boneOffset + 0] * w[boneOffset + 0] + w[boneOffset + 2] * w[boneOffset + 2]);
137
+ if (rr > 0) s.scaleOffset += ((dx * c + dy * sn) * i) / rr;
138
+ }
139
+ } else {
140
+ c = Math.cos(ca);
141
+ sn = Math.sin(ca);
142
+ const rr = l * Math.sqrt(w[boneOffset + 0] * w[boneOffset + 0] + w[boneOffset + 2] * w[boneOffset + 2]) - s.scaleLag * shown;
143
+ if (rr > 0) s.scaleOffset += ((dx * c + dy * sn) * i) / rr;
144
+ }
145
+ a = s.remaining;
146
+ if (a >= t) {
147
+ if (d === -1) d = Math.pow(r.damping, 60 * t);
148
+ const m = r.massInverse * t;
149
+ const e = r.strength;
150
+ const ax = r.wind * env[0] + r.gravity * env[2];
151
+ const ay = r.wind * env[1] + r.gravity * env[3];
152
+ const h = l / f;
153
+ const rs = s.rotateOffset;
154
+ const ss = s.scaleOffset;
155
+ for (;;) {
156
+ a -= t;
157
+ if (scaleX) {
158
+ s.scaleVelocity += (ax * c - ay * sn - s.scaleOffset * e) * m;
159
+ s.scaleOffset += s.scaleVelocity * t;
160
+ s.scaleVelocity *= d;
161
+ }
162
+ if (rotateOrShearX) {
163
+ s.rotateVelocity -= ((ax * sn + ay * c) * h + s.rotateOffset * e) * m;
164
+ s.rotateOffset += s.rotateVelocity * t;
165
+ s.rotateVelocity *= d;
166
+ if (a < t) break;
167
+ const rr = s.rotateOffset * mr + ca;
168
+ c = Math.cos(rr);
169
+ sn = Math.sin(rr);
170
+ } else if (a < t) break;
171
+ }
172
+ s.rotateLag = s.rotateOffset - rs;
173
+ s.scaleLag = s.scaleOffset - ss;
174
+ }
175
+ z = Math.max(0, 1 - a / t);
176
+ }
177
+ s.remaining = a;
178
+ }
179
+ s.cx = w[boneOffset + 4];
180
+ s.cy = w[boneOffset + 5];
181
+ if (rotateOrShearX) {
182
+ let angle = (s.rotateOffset - s.rotateLag * z) * mix;
183
+ let sn = 0;
184
+ let c = 0;
185
+ let a = 0;
186
+ if (r.shearX > 0) {
187
+ let rr = 0;
188
+ if (r.rotate > 0) {
189
+ // The rotation's share turns the y column here, and the x column with the shear below.
190
+ rr = angle * r.rotate;
191
+ sn = Math.sin(rr);
192
+ c = Math.cos(rr);
193
+ a = w[boneOffset + 1];
194
+ w[boneOffset + 1] = c * a - sn * w[boneOffset + 3];
195
+ w[boneOffset + 3] = sn * a + c * w[boneOffset + 3];
196
+ }
197
+ rr += angle * r.shearX;
198
+ sn = Math.sin(rr);
199
+ c = Math.cos(rr);
200
+ a = w[boneOffset + 0];
201
+ w[boneOffset + 0] = c * a - sn * w[boneOffset + 2];
202
+ w[boneOffset + 2] = sn * a + c * w[boneOffset + 2];
203
+ } else {
204
+ angle *= r.rotate;
205
+ sn = Math.sin(angle);
206
+ c = Math.cos(angle);
207
+ a = w[boneOffset + 0];
208
+ w[boneOffset + 0] = c * a - sn * w[boneOffset + 2];
209
+ w[boneOffset + 2] = sn * a + c * w[boneOffset + 2];
210
+ a = w[boneOffset + 1];
211
+ w[boneOffset + 1] = c * a - sn * w[boneOffset + 3];
212
+ w[boneOffset + 3] = sn * a + c * w[boneOffset + 3];
213
+ }
214
+ }
215
+ if (scaleX) {
216
+ const k = 1 + (s.scaleOffset - s.scaleLag * z) * mix * r.scaleX;
217
+ w[boneOffset + 0] *= k;
218
+ w[boneOffset + 2] *= k;
219
+ // The transverse column follows the authored mode (issue #1453), after
220
+ // the X column: the native Volume branch bounds reciprocal compression
221
+ // below 0.7; it is not an area-preservation promise on that branch.
222
+ if (r.scaleYMode === 'Uniform') {
223
+ w[boneOffset + 1] *= k;
224
+ w[boneOffset + 3] *= k;
225
+ } else if (r.scaleYMode === 'Volume') {
226
+ const magnitude = Math.abs(k);
227
+ const transverse = magnitude >= 0.7 ? 1 / magnitude : 4 - 3.67347 * magnitude;
228
+ w[boneOffset + 1] *= transverse;
229
+ w[boneOffset + 3] *= transverse;
230
+ }
231
+ }
232
+ s.tx = l * w[boneOffset + 0];
233
+ s.ty = l * w[boneOffset + 2];
234
+ return true;
235
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The runtime poser's per-instance half (issue #1591, stage 2): one
2
+ * The runtime poser's per-instance half (issue #1591, stage 3): one
3
3
  * `RuntimePose` per skeleton on screen, every buffer allocated when it is
4
4
  * made and overwritten in place on every pose, so that posing a frame
5
5
  * allocates nothing.
@@ -13,8 +13,9 @@
13
13
  * document's order with their timelines' values at `t`, each followed by the
14
14
  * bones below what it moved posed again; then each slot's colour and what it
15
15
  * shows; then, when asked, every shown attachment's world vertices. Nothing
16
- * of one pose survives into the next but the key cursors, which only make
17
- * the key search start nearer.
16
+ * of one unstepped pose survives but key cursors. A stepped pose also keeps
17
+ * its physics clock, offsets, velocities and lags; sliders select values
18
+ * within this pose rather than advancing an independent track.
18
19
  *
19
20
  * ## The arithmetic is the oracle's
20
21
  *
@@ -37,6 +38,8 @@
37
38
  * exactly 0 touching nothing; an inactive bone posed as zeros by the
38
39
  * hierarchy and never posed again by a constraint, nor the bones below it.
39
40
  */
41
+ import type { CorePhysicsRecord } from '../core/constraints_physics.ts';
42
+ import { freshPhysicsState, resetPhysicsState, stepRuntimePhysics, type PhysicsState } from './physics.ts';
40
43
  import { COLLAPSED_X_AXIS_SQ, RUNTIME_DEG, RUNTIME_PI } from '../core/world.ts';
41
44
  import { channelValue, keyAt, deformFraction, CURVE_STEPPED } from './curves.ts';
42
45
  import {
@@ -75,6 +78,8 @@ import {
75
78
  type RuntimeIk,
76
79
  type RuntimeModel,
77
80
  type RuntimeTransform,
81
+ type RuntimeSlider,
82
+ type RuntimePhysicsTimeline,
78
83
  } from './model.ts';
79
84
 
80
85
  const DEG = RUNTIME_DEG;
@@ -98,6 +103,10 @@ function volumeScaleY(scaleY: number, s: number): number {
98
103
 
99
104
  const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
100
105
 
106
+ /** A caller-supplied physics clock; animation/track time still uses dt itself. */
107
+ export interface RuntimePoseOptions { readonly clock?: (dt: number) => number }
108
+ export type RuntimePhysicsPhase = 'none' | 'reset' | 'update';
109
+
101
110
  /** One skeleton's pose, every value in place. Read `world`, `light`, `shown` and `vertices` after a pose. */
102
111
  export class RuntimePose {
103
112
  readonly model: RuntimeModel;
@@ -139,8 +148,29 @@ export class RuntimePose {
139
148
  private readonly visit: Uint8Array;
140
149
  private readonly w = new Float64Array(WORLDS);
141
150
 
142
- constructor(model: RuntimeModel) {
151
+ /** Sum of caller-supplied physics deltas, independent of animation time. */
152
+ physicsTime = 0;
153
+ private physicsLast = -1;
154
+ private physicsPhase: RuntimePhysicsPhase = 'none';
155
+ private physicsReady = false;
156
+ private readonly hasPhysics: boolean;
157
+ private readonly physicsStates: PhysicsState[];
158
+ private readonly physicsRecords: Array<CorePhysicsRecord | null>;
159
+ private readonly environment = new Float64Array([1, 0, 0, 1]);
160
+ private readonly sliderTime: Float64Array;
161
+ private readonly sliderMix: Float64Array;
162
+ private readonly sliderAt: Float64Array;
163
+ private readonly clock: ((dt: number) => number) | undefined;
164
+
165
+ constructor(model: RuntimeModel, options: RuntimePoseOptions = {}) {
143
166
  this.model = model;
167
+ this.clock = options.clock;
168
+ this.hasPhysics = model.constraints.some(c => c.kind === 'physics');
169
+ this.physicsStates = model.constraints.map(() => freshPhysicsState());
170
+ this.physicsRecords = model.constraints.map(c => c.kind === 'physics' ? { ...c.setup } : null);
171
+ this.sliderTime = new Float64Array(model.constraints.length);
172
+ this.sliderMix = new Float64Array(model.constraints.length);
173
+ this.sliderAt = new Float64Array(model.constraints.length);
144
174
  const nb = model.boneNames.length;
145
175
  const ns = model.slots.length;
146
176
  const nc = model.constraints.length;
@@ -168,6 +198,10 @@ export class RuntimePose {
168
198
  /** The animation this pose plays (a covered one: `model.animation` throws a `RuntimeRefusal` for a refused one), from track time 0. */
169
199
  setAnimation(name: string): void {
170
200
  this.current = this.model.animation(name);
201
+ this.physicsTime = 0;
202
+ this.physicsLast = -1;
203
+ this.physicsReady = false;
204
+ for (let k = 0; k < this.physicsStates.length; k++) resetPhysicsState(this.physicsStates[k], 0);
171
205
  this.cursors.fill(-1);
172
206
  this.trackTime = 0;
173
207
  this.time = 0;
@@ -190,40 +224,57 @@ export class RuntimePose {
190
224
  }
191
225
 
192
226
  /** The setup pose: every constraint applied with its own values, every slot as it is set up. */
193
- setupPose(): void {
227
+ setupPose(physics: RuntimePhysicsPhase = 'none'): void {
228
+ this.physicsPhase = physics;
194
229
  this.current = null;
195
230
  this.time = 0;
196
231
  this.eventTime = -1;
197
232
  this.fired.length = 0;
233
+ this.physicsValues(null, 0, false);
198
234
  this.locals.set(this.model.setup);
199
235
  this.hierarchy();
200
236
  this.constraintValues(null, 0);
201
237
  this.solveConstraints();
202
- this.slotValues(null, 0);
203
- this.deformValues(null, 0);
238
+ this.attachmentValues(null, 0);
204
239
  }
205
240
 
206
241
  /** The animation posed at its time `t`, from the setup pose (the header). */
207
- apply(t: number): void {
242
+ apply(t: number, physics: RuntimePhysicsPhase = 'none', delta = 0): void {
208
243
  const anim = this.current;
209
244
  if (anim === null) throw new RuntimeInputError('apply: no animation is set (setAnimation first)');
210
245
  if (!Number.isFinite(t)) throw new RuntimeInputError(`apply: the time ${t} is not finite`);
246
+ if (!Number.isFinite(delta) || delta < 0) throw new RuntimeInputError(`apply: physics delta ${delta} is not finite and at or above 0`);
247
+ this.physicsPhase = physics;
248
+ // Reset keys see the old clock. The skeleton moves by delta only after application.
249
+ this.physicsValues(anim, t, physics !== 'none');
250
+ if (physics !== 'none') {
251
+ this.physicsTime += delta;
252
+ this.physicsReady = true;
253
+ }
211
254
  this.collectEvents(anim, t);
212
255
  this.time = t;
213
256
  this.boneTimelines(anim, t);
214
257
  this.hierarchy();
215
258
  this.constraintValues(anim, t);
216
259
  this.solveConstraints();
217
- this.slotValues(anim, t);
218
- this.deformValues(anim, t);
260
+ this.attachmentValues(anim, t);
219
261
  }
220
262
 
221
263
  /** One step of a looping track: the track time moved by `dt`, the animation posed at it wrapped by the duration (`loopedTime` in src/core/raw.ts). */
222
264
  step(dt: number): void {
223
265
  const anim = this.current;
224
266
  if (anim === null) throw new RuntimeInputError('step: no animation is set (setAnimation first)');
267
+ if (!Number.isFinite(dt) || dt < 0) throw new RuntimeInputError(`step: delta ${dt} is not finite and at or above 0`);
268
+ if (!this.physicsReady && this.hasPhysics) {
269
+ // Initial physics setup must not consume the event interval of this step.
270
+ const lastEventTime = this.eventTime;
271
+ this.apply(this.time, 'reset', 0);
272
+ this.eventTime = lastEventTime;
273
+ }
274
+ const moved = this.clock === undefined ? dt : this.clock(dt);
275
+ if (!Number.isFinite(moved) || moved < 0) throw new RuntimeInputError(`step: physics clock moved by ${moved}, not finite and at or above 0`);
225
276
  this.trackTime += dt;
226
- this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration);
277
+ this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration, 'update', moved);
227
278
  }
228
279
 
229
280
  /** The oracle interval (last,t], tail then head on a wrap; never copies a payload. */
@@ -427,9 +478,24 @@ export class RuntimePose {
427
478
  this.ikSoftness[k] = c.softness + (channelValue(tl.curve, i, 1, t) - c.softness) * 1;
428
479
  this.ikFlags[k] = (tl.flags as Uint8Array)[i];
429
480
  }
430
- } else {
481
+ } else if (c.kind === 'transform') {
431
482
  const o = k * 6;
432
483
  for (let p = 0; p < 6; p++) this.mixes[o + p] = tl === null || i < 0 ? c.mixes[p] : c.mixes[p] + (channelValue(tl.curve, i, p, t) - c.mixes[p]) * 1;
484
+ } else if (c.kind === 'slider') {
485
+ const keyed = anim === null ? null : anim.sliders[k];
486
+ let time = c.setup.time, mix = c.setup.mix;
487
+ if (keyed !== null) {
488
+ if (keyed.time !== null) {
489
+ const q = keyAt(keyed.time, this.cursors, keyed.timeCursor, t);
490
+ if (q >= 0) time += (channelValue(keyed.time, q, 0, t) - time) * 1;
491
+ }
492
+ if (keyed.mix !== null) {
493
+ const q = keyAt(keyed.mix, this.cursors, keyed.mixCursor, t);
494
+ if (q >= 0) mix += (channelValue(keyed.mix, q, 0, t) - mix) * 1;
495
+ }
496
+ }
497
+ this.sliderTime[k] = time;
498
+ this.sliderMix[k] = mix;
433
499
  }
434
500
  }
435
501
  }
@@ -441,7 +507,222 @@ export class RuntimePose {
441
507
  const c = cs[k];
442
508
  if (!c.applied) continue;
443
509
  if (c.kind === 'ik') this.solveIk(c, k);
444
- else this.solveTransform(c, k);
510
+ else if (c.kind === 'transform') this.solveTransform(c, k);
511
+ else if (c.kind === 'slider') this.solveSlider(c, k);
512
+ else if (this.physicsPhase !== 'none' && !this.model.omitPhysics) {
513
+ const b = c.bones[0];
514
+ if (stepRuntimePhysics(this.physicsRecords[k]!, this.physicsStates[k], this.world, b * WORLDS, this.model.lengths[b], this.physicsTime, this.physicsPhase === 'reset', this.environment)) {
515
+ this.changed[0] = this.inWorld[0] = b;
516
+ this.repose(1, 1);
517
+ }
518
+ }
519
+ }
520
+ }
521
+
522
+ /** Reset where the consumer starts: the setup pose or animation pose zero. */
523
+ resetPhysics(at: 'setup' | 'animation' = 'animation'): void {
524
+ this.physicsTime = 0;
525
+ this.physicsLast = -1;
526
+ for (let k = 0; k < this.physicsStates.length; k++) resetPhysicsState(this.physicsStates[k], 0);
527
+ if (at === 'animation') this.apply(0, 'reset', 0);
528
+ else {
529
+ const current = this.current;
530
+ this.setupPose('reset');
531
+ this.current = current;
532
+ }
533
+ this.physicsReady = true;
534
+ }
535
+
536
+ /** External motion changes the previous origin, never the authored locals. */
537
+ physicsTranslate(x: number, y: number): void {
538
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new RuntimeInputError('physicsTranslate: x and y must be finite');
539
+ for (let k = 0; k < this.physicsStates.length; k++) {
540
+ const s = this.physicsStates[k];
541
+ s.ux -= x; s.uy -= y; s.cx -= x; s.cy -= y;
542
+ }
543
+ }
544
+
545
+ physicsRotate(x: number, y: number, degrees: number): void {
546
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(degrees)) throw new RuntimeInputError('physicsRotate: x, y and degrees must be finite');
547
+ const r = degrees * RAD, cos = Math.cos(r), sin = Math.sin(r);
548
+ for (let k = 0; k < this.physicsStates.length; k++) {
549
+ const s = this.physicsStates[k];
550
+ const dx = s.cx - x, dy = s.cy - y;
551
+ const tx = dx * cos - dy * sin - dx, ty = dx * sin + dy * cos - dy;
552
+ s.ux -= tx; s.uy -= ty; s.cx -= tx; s.cy -= ty;
553
+ }
554
+ }
555
+
556
+ setEnvironment(env: { readonly wind: readonly [number, number]; readonly gravity: readonly [number, number] }): void {
557
+ if (!Number.isFinite(env.wind[0]) || !Number.isFinite(env.wind[1]) || !Number.isFinite(env.gravity[0]) || !Number.isFinite(env.gravity[1])) throw new RuntimeInputError('setEnvironment: wind and gravity must be finite');
558
+ this.environment[0] = env.wind[0]; this.environment[1] = env.wind[1];
559
+ this.environment[2] = env.gravity[0]; this.environment[3] = env.gravity[1];
560
+ }
561
+
562
+ /** The per-pass values start at setup, before any slider writes. */
563
+ private physicsValues(anim: RuntimeAnimation | null, t: number, resets: boolean): void {
564
+ const cs = this.model.constraints;
565
+ for (let k = 0; k < cs.length; k++) {
566
+ const c = cs[k];
567
+ if (c.kind !== 'physics') continue;
568
+ const r = this.physicsRecords[k]!;
569
+ r.inertia = c.setup.inertia; r.strength = c.setup.strength; r.damping = c.setup.damping;
570
+ r.massInverse = c.setup.massInverse; r.wind = c.setup.wind; r.gravity = c.setup.gravity; r.mix = c.setup.mix;
571
+ }
572
+ if (anim !== null) this.physicsTimelines(anim.physics, t, 1, false, true, resets);
573
+ if (resets) this.physicsLast = t;
574
+ }
575
+
576
+ private physicsTimelines(timelines: readonly RuntimePhysicsTimeline[], t: number, alpha: number, additive: boolean, setup: boolean, resets = false): void {
577
+ for (let q = 0; q < timelines.length; q++) {
578
+ const tl = timelines[q];
579
+ if (tl.kind === 'reset') {
580
+ if (!resets) continue; // a slider applies at one value; no key is crossed
581
+ let crossed = false;
582
+ const last = this.physicsLast;
583
+ for (let i = 0; i < tl.curve.keys; i++) {
584
+ const time = tl.curve.times[i];
585
+ if (t < last ? time > last || time <= t : time > last && time <= t) crossed = true;
586
+ }
587
+ if (crossed) for (let j = 0; j < tl.targets.length; j++) resetPhysicsState(this.physicsStates[tl.targets[j]], this.physicsTime);
588
+ continue;
589
+ }
590
+ const i = keyAt(tl.curve, this.cursors, tl.cursor, t);
591
+ if (i < 0) {
592
+ // A track's before-first key restores setup even if an earlier global
593
+ // timeline wrote this parameter. A slider's before-first key writes nothing.
594
+ if (setup) for (let j = 0; j < tl.targets.length; j++) {
595
+ const k = tl.targets[j], r = this.physicsRecords[k]!;
596
+ const original = (this.model.constraints[k] as { setup: CorePhysicsRecord }).setup;
597
+ if (tl.kind === 'mass') r.massInverse = original.massInverse;
598
+ else r[tl.kind] = original[tl.kind];
599
+ }
600
+ continue;
601
+ }
602
+ const v = channelValue(tl.curve, i, 0, t);
603
+ for (let j = 0; j < tl.targets.length; j++) {
604
+ const k = tl.targets[j], r = this.physicsRecords[k]!;
605
+ const from = setup ? (this.model.constraints[k] as { setup: CorePhysicsRecord }).setup : r;
606
+ if (tl.kind === 'mass') {
607
+ const mass = 1 / from.massInverse;
608
+ r.massInverse = 1 / (mass + (v - mass) * alpha);
609
+ } else {
610
+ r[tl.kind] = additive && (tl.kind === 'wind' || tl.kind === 'gravity') ? r[tl.kind] + v * alpha : from[tl.kind] + (v - from[tl.kind]) * alpha;
611
+ }
612
+ }
613
+ }
614
+ }
615
+
616
+ /** Read the dial where previous constraints left it; this value is never track time. */
617
+ private solveSlider(c: RuntimeSlider, k: number): void {
618
+ const alpha = this.sliderMix[k];
619
+ if (alpha === 0) return;
620
+ let time = this.sliderTime[k];
621
+ const r = c.setup, a = c.animation;
622
+ if (c.bone >= 0) {
623
+ const b = c.bone, w = b * WORLDS, l = b * LOCALS;
624
+ let value: number;
625
+ if (r.local) {
626
+ const field = c.property === 0 ? L_ROTATION : c.property === 1 ? L_X : c.property === 2 ? L_Y : c.property === 3 ? L_SCALE_X : c.property === 4 ? L_SCALE_Y : L_SHEAR_Y;
627
+ value = this.locals[l + field] + 0;
628
+ } else {
629
+ const W = this.world, x = W[w], y = W[w + 2], bx = W[w + 1], by = W[w + 3];
630
+ switch (c.property) {
631
+ case 0: value = Math.atan2(y, x) * DEG + (x * by - bx * y > 0 ? 0 : -0); if (value < 0) value += 360; break;
632
+ case 1: value = x * 0 + bx * 0 + W[w + W_X]; break;
633
+ case 2: value = y * 0 + by * 0 + W[w + W_Y]; break;
634
+ case 3: value = Math.sqrt(x * x + y * y) + 0; break;
635
+ case 4: value = Math.sqrt(bx * bx + by * by) + 0; break;
636
+ default: value = (Math.atan2(by, bx) - Math.atan2(y, x)) * DEG - 90 + 0;
637
+ }
638
+ }
639
+ time = r.to + (value - r.from) * r.scale;
640
+ time = r.loop ? a.duration === 0 ? 0 : a.duration + time % a.duration : Math.max(0, time);
641
+ }
642
+ const at = this.model.sliderAsTrack ? this.time : r.loop && a.duration !== 0 ? time % a.duration : time;
643
+ this.sliderAt[k] = at;
644
+ if (this.physicsPhase !== 'none') this.physicsTimelines(a.physics, at, alpha, r.additive, false);
645
+ const L = this.locals, S = this.model.setup;
646
+ for (let q = 0; q < a.sliderBones.length; q++) {
647
+ const tl = a.sliderBones[q], i = keyAt(tl.curve, this.cursors, tl.cursor, at);
648
+ if (i < 0) continue;
649
+ const o = tl.bone * LOCALS;
650
+ for (let f = 0; f < tl.fields.length; f++) {
651
+ const n = o + tl.fields[f], v = channelValue(tl.curve, i, f, at), current = L[n], setup = S[n];
652
+ if (tl.multiply[f]) {
653
+ if (r.additive) L[n] = current + (v * setup - setup) * alpha;
654
+ else {
655
+ const target = setup * v, from = Math.abs(current) * Math.sign(target);
656
+ L[n] = alpha === 1 ? target : from + (target - from) * alpha;
657
+ }
658
+ } else L[n] = r.additive ? current + v * alpha : current + (setup + v - current) * alpha;
659
+ }
660
+ }
661
+ for (let q = 0; q < c.bones.length; q++) this.changed[q] = c.bones[q];
662
+ if (c.bones.length > 0) this.repose(c.bones.length, 0);
663
+ }
664
+
665
+ private attachmentValues(anim: RuntimeAnimation | null, t: number): void {
666
+ this.slotValues(anim, t);
667
+ this.deformValues(anim, t);
668
+ for (let k = 0; k < this.model.constraints.length; k++) {
669
+ this.sliderSlots(k);
670
+ this.sliderDeforms(k);
671
+ }
672
+ }
673
+
674
+ private sliderSlots(index: number): void {
675
+ const cs = this.model.constraints;
676
+ {
677
+ const k = index, c = cs[k];
678
+ if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
679
+ const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
680
+ for (let q = 0; q < a.slots.length; q++) {
681
+ const tl = a.slots[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
682
+ if (i < 0) continue;
683
+ if (tl.attachments !== null) {
684
+ const next = tl.attachments[i];
685
+ if (next !== this.shown[tl.slot] && next >= 0) this.deformed[next] = 0;
686
+ this.shown[tl.slot] = next;
687
+ }
688
+ else {
689
+ const channels = tl.channels!;
690
+ for (let f = 0; f < channels.length; f++) {
691
+ const n = tl.slot * 4 + channels[f], v = channelValue(tl.curve, i, f, t);
692
+ this.light[n] = clamp01(alpha === 1 ? v : this.light[n] + (v - this.light[n]) * alpha);
693
+ }
694
+ }
695
+ }
696
+ }
697
+ }
698
+
699
+ private sliderDeforms(index: number): void {
700
+ if (this.model.omitDeform) return;
701
+ const cs = this.model.constraints;
702
+ {
703
+ const k = index, c = cs[k];
704
+ if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
705
+ const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
706
+ for (let q = 0; q < a.deforms.length; q++) {
707
+ const tl = a.deforms[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
708
+ if (i < 0) continue;
709
+ const held = i + 1 === tl.curve.keys || tl.curve.kinds[i] === CURVE_STEPPED;
710
+ const p = held ? 0 : deformFraction(tl.curve, i, t), start = i * tl.count;
711
+ for (let slot = 0; slot < this.shown.length; slot++) {
712
+ const index = this.shown[slot];
713
+ if (index < 0 || !this.model.slots[slot].live) continue;
714
+ const at = this.model.attachments[index];
715
+ if (at.timeline !== tl.identity || at.deformCount === 0) continue;
716
+ const current = this.deformed[index] !== 0;
717
+ for (let f = 0; f < tl.count; f++) {
718
+ const v = tl.held[start + f], target = held ? v : v + (tl.held[start + tl.count + f] - v) * p;
719
+ const setup = at.draw === DRAW_MESH ? at.local[f] : 0, n = at.deformOffset + f;
720
+ const from = current ? this.deform[n] : setup;
721
+ this.deform[n] = alpha === 1 && (!c.setup.additive || !current) ? target : c.setup.additive ? from + (target - setup) * alpha : from + (target - from) * alpha;
722
+ }
723
+ this.deformed[index] = 1;
724
+ }
725
+ }
445
726
  }
446
727
  }
447
728
 
@@ -868,6 +1149,20 @@ export class RuntimePose {
868
1149
  const wb = W[w + W_B];
869
1150
  const wc = W[w + W_C];
870
1151
  const wd = W[w + W_D];
1152
+ if (mode === MODE_NO_SCALE || mode === MODE_NO_SCALE_OR_REFLECTION) {
1153
+ const det = pa * pd - pb * pc;
1154
+ const flip = mode === MODE_NO_SCALE && det < 0 ? -1 : 1;
1155
+ const sign = det < 0 ? -1 : 1;
1156
+ const rotation = Math.atan2((pa * wc - pc * wa) * sign, (pd * wa - pb * wc) * sign) * DEG;
1157
+ const rr = rotation * RAD;
1158
+ const cos = Math.cos(rr), sin = Math.sin(rr);
1159
+ let ux = pa * cos + pb * sin, uy = pc * cos + pd * sin;
1160
+ const length = 1 / Math.sqrt(ux * ux + uy * uy);
1161
+ ux *= length; uy *= length;
1162
+ this.decompose(l, ux * wa + uy * wc, (ux * wc - uy * wa) * flip, ux * wb + uy * wd, (ux * wd - uy * wb) * flip, true);
1163
+ L[l + L_ROTATION] = rotation;
1164
+ return;
1165
+ }
871
1166
  if (mode === MODE_ONLY_TRANSLATION) {
872
1167
  this.decompose(l, wa, wc, wb, wd);
873
1168
  return;
@@ -887,7 +1182,7 @@ export class RuntimePose {
887
1182
  }
888
1183
 
889
1184
  /** A local matrix with columns `(la, lc)` and `(lb, ld)` as rotation, scales and shear (`decompose`). */
890
- private decompose(l: number, la: number, lc: number, lb: number, ld: number): void {
1185
+ private decompose(l: number, la: number, lc: number, lb: number, ld: number, shearX = false): void {
891
1186
  const L = this.locals;
892
1187
  const sx = Math.sqrt(la * la + lc * lc);
893
1188
  const yLength = Math.sqrt(lb * lb + ld * ld);
@@ -901,11 +1196,11 @@ export class RuntimePose {
901
1196
  const xAngle = Math.atan2(lc, la) * DEG;
902
1197
  const det = la * ld - lb * lc;
903
1198
  const yAngle = Math.atan2(ld, lb) * DEG;
904
- L[l + L_ROTATION] = xAngle;
1199
+ L[l + (shearX ? L_SHEAR_X : L_ROTATION)] = xAngle;
905
1200
  L[l + L_SCALE_X] = sx;
906
1201
  L[l + L_SCALE_Y] = det < 0 ? -yLength : yLength;
907
1202
  const right = det < 0 ? -90 : 90;
908
- let shear = yAngle - (xAngle + right);
1203
+ let shear = yAngle - ((shearX ? 0 : xAngle) + right);
909
1204
  shear = shear > 180 ? shear - 360 : shear < -180 ? shear + 360 : shear;
910
1205
  L[l + L_SHEAR_Y] = yLength > 0.00001 ? shear : 0;
911
1206
  }