@rig-c/core 2.74.1 → 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 +77 -6
- package/package.json +1 -1
- package/src/core/constraints_physics.ts +6 -4
- package/src/core/raw.ts +20 -4
- package/src/core/track.ts +11 -9
- package/src/core/tracks.ts +11 -9
- package/src/runtime/curves.ts +3 -1
- package/src/runtime/index.ts +3 -2
- package/src/runtime/model.ts +101 -20
- package/src/runtime/physics.ts +235 -0
- package/src/runtime/pose.ts +311 -16
package/docs/AUTHORING.md
CHANGED
|
@@ -5578,6 +5578,43 @@ skeleton's units and axes, and its change of rotation as `physicsRotate(0, 0,
|
|
|
5578
5578
|
Δ)`, before the `step` that poses the frame. Nothing set, every pose is the
|
|
5579
5579
|
one the track posed before these existed (`CO57`).
|
|
5580
5580
|
|
|
5581
|
+
Both `LiveTrack` and `LiveTracks` also accept an optional last `scope` argument
|
|
5582
|
+
for movement, a readonly array of **exact physics constraint names** (play #197):
|
|
5583
|
+
|
|
5584
|
+
```ts
|
|
5585
|
+
track.physicsTranslate(dx, dy, ['hair_left', 'hair_right']);
|
|
5586
|
+
track.physicsRotate(pivotX, pivotY, deltaDegrees, ['hair_left', 'hair_right']);
|
|
5587
|
+
```
|
|
5588
|
+
|
|
5589
|
+
Omitted or `undefined` means every existing state, exactly as before. `[]`
|
|
5590
|
+
selects none; repeated names select once, in the existing state order. Every
|
|
5591
|
+
name is checked against the document before any state changes: an unknown name
|
|
5592
|
+
is refused as `physics movement scope: no constraint "name"`; a non-physics
|
|
5593
|
+
constraint is refused naming its kind. A non-array scope or non-string name
|
|
5594
|
+
also refuses. Finite movement/pivot checks still apply, including an empty
|
|
5595
|
+
scope. A declared inactive constraint is a valid selection; movement creates
|
|
5596
|
+
no state for it. No document, parameter override, clock or held pose is changed.
|
|
5597
|
+
|
|
5598
|
+
These are synchronous **before-evaluation** inputs: only the selected remembered
|
|
5599
|
+
origins (`ux`, `uy`, `cx`, `cy`) change, so the next `step` sees extra apparent
|
|
5600
|
+
motion. Distances and the explicit pivot are skeleton world units, x right,
|
|
5601
|
+
y up; the angle is degrees CCW **in the XY plane**, not yaw or pitch. The turn
|
|
5602
|
+
is the displacement of each remembered origin around the pivot, not a direct
|
|
5603
|
+
angular impulse; an origin at the pivot has no displacement. The caller derives
|
|
5604
|
+
any projected yaw/pitch displacement from its own geometry and depth. Do not
|
|
5605
|
+
supply it again for movement the bone itself already makes.
|
|
5606
|
+
|
|
5607
|
+
The ordinary step still owns reset, inertia, limits and integration. A reset
|
|
5608
|
+
or first active update can overwrite this input; an intervening `step(0)` may
|
|
5609
|
+
consume it without the intended positive-time response. There is no queue or
|
|
5610
|
+
motion-history replay. Input writes leave all unselected states unchanged;
|
|
5611
|
+
after stepping, exact isolation holds for independent chains. Descendants or
|
|
5612
|
+
constraints that read a selected bone may react through their normal solver
|
|
5613
|
+
dependencies. Scope does not suppress that coupling or expand itself to cover it.
|
|
5614
|
+
`CO64` checks single/composed tracks, both resets, exact independent-state
|
|
5615
|
+
isolation, native per-constraint operations, empty/all/repeated selections and
|
|
5616
|
+
atomic named refusals; the existing `CO56`/`CO57` cover unscoped movement.
|
|
5617
|
+
|
|
5581
5618
|
For a live preview of one named constraint (issue #1541), both `LiveTrack` and
|
|
5582
5619
|
`LiveTracks` on `rig-c/core` expose:
|
|
5583
5620
|
|
|
@@ -12610,7 +12647,7 @@ verdict on those figures.
|
|
|
12610
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.⟧ |
|
|
12611
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.⟧ |
|
|
12612
12649
|
|
|
12613
|
-
## 15. Posing in place for a player — the runtime poser (stage
|
|
12650
|
+
## 15. Posing in place for a player — the runtime poser (stage 3)
|
|
12614
12651
|
|
|
12615
12652
|
rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
|
|
12616
12653
|
(`src/core/`) is the reference: each pose is a pure function of the document,
|
|
@@ -12645,11 +12682,11 @@ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeho
|
|
|
12645
12682
|
The display `name` may be shared by different placeholders; it is never a table
|
|
12646
12683
|
key to guess the source from. These load-time strings do not change the pose path.
|
|
12647
12684
|
|
|
12648
|
-
| | what stage
|
|
12685
|
+
| | what stage 3 poses |
|
|
12649
12686
|
| --- | --- |
|
|
12650
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) |
|
|
12651
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 |
|
|
12652
|
-
| 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 |
|
|
12653
12690
|
| vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
|
|
12654
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` |
|
|
12655
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 |
|
|
@@ -12692,9 +12729,43 @@ back to the definition's **volume**, matching the measured runtime rather than
|
|
|
12692
12729
|
inventing a correction. An explicit `ignore: ['events']` diagnostic still mutes
|
|
12693
12730
|
the list; normal compilation never does.
|
|
12694
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
|
+
|
|
12695
12766
|
**Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
|
|
12696
12767
|
every construct met, each `{ feature, detail, animation }`, and the message
|
|
12697
|
-
reads `not posed by the runtime poser (stage
|
|
12768
|
+
reads `not posed by the runtime poser (stage 3), N construct(s) missing:
|
|
12698
12769
|
<feature>: <detail>; …`. A model-level feature refuses the whole model; an
|
|
12699
12770
|
animation-level one refuses that animation alone (`model.refused`;
|
|
12700
12771
|
`model.animation(name)` and `setAnimation(name)` throw its refusal) and every
|
|
@@ -12703,7 +12774,7 @@ refused: nothing is posed approximately.
|
|
|
12703
12774
|
|
|
12704
12775
|
| feature | refuses | what it is |
|
|
12705
12776
|
| --- | --- | --- |
|
|
12706
|
-
| `path-constraint
|
|
12777
|
+
| `path-constraint` | the model | a constraint of that kind |
|
|
12707
12778
|
| `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
|
|
12708
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) |
|
|
12709
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 |
|
|
@@ -12718,7 +12789,7 @@ this document's [...]`, `apply: the time NaN is not finite`.
|
|
|
12718
12789
|
|
|
12719
12790
|
**The gate.** `tools/core_gate.ts` runs a third dumper (`runtimeDump` in
|
|
12720
12791
|
`tools/pose_oracle.ts`) over every row it builds, at the oracle's samples, and
|
|
12721
|
-
compares
|
|
12792
|
+
compares both unstepped and stepped documents with the oracle's in full doubles at tolerance 0 twice:
|
|
12722
12793
|
through `compare` and cell by cell (which reads the bones `compare`'s
|
|
12723
12794
|
ill-conditioned rule leaves out). One `RUNTIME` line per row reads `IDENTICAL`
|
|
12724
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.
|
|
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": {
|
|
@@ -645,8 +645,9 @@ export const DEFAULT_PHYSICS_ENVIRONMENT: PhysicsEnvironment = Object.freeze({ w
|
|
|
645
645
|
* pose. A constraint with no state yet has not been updated, and its first
|
|
646
646
|
* update is pending, so the runtime's translation of it is read by nothing.
|
|
647
647
|
*/
|
|
648
|
-
export function translatePhysics(ctx: PhysicsStepContext, x: number, y: number): void {
|
|
649
|
-
for (const s of ctx.states
|
|
648
|
+
export function translatePhysics(ctx: PhysicsStepContext, x: number, y: number, scope?: ReadonlySet<string>): void {
|
|
649
|
+
for (const [name, s] of ctx.states) {
|
|
650
|
+
if (scope !== undefined && !scope.has(name)) continue;
|
|
650
651
|
s.ux -= x;
|
|
651
652
|
s.uy -= y;
|
|
652
653
|
s.cx -= x;
|
|
@@ -661,11 +662,12 @@ export function translatePhysics(ctx: PhysicsStepContext, x: number, y: number):
|
|
|
661
662
|
* a translation of each constraint by how far that turn carries the origin it
|
|
662
663
|
* last saw, as `PhysicsConstraint.rotate` computes it.
|
|
663
664
|
*/
|
|
664
|
-
export function rotatePhysics(ctx: PhysicsStepContext, x: number, y: number, degrees: number): void {
|
|
665
|
+
export function rotatePhysics(ctx: PhysicsStepContext, x: number, y: number, degrees: number, scope?: ReadonlySet<string>): void {
|
|
665
666
|
const r = degrees * RUNTIME_RAD;
|
|
666
667
|
const cos = Math.cos(r);
|
|
667
668
|
const sin = Math.sin(r);
|
|
668
|
-
for (const s of ctx.states
|
|
669
|
+
for (const [name, s] of ctx.states) {
|
|
670
|
+
if (scope !== undefined && !scope.has(name)) continue;
|
|
669
671
|
const dx = s.cx - x;
|
|
670
672
|
const dy = s.cy - y;
|
|
671
673
|
const tx = dx * cos - dy * sin - dx;
|
package/src/core/raw.ts
CHANGED
|
@@ -728,20 +728,36 @@ export function environmentOf(ctx: PhysicsStepContext): PhysicsEnvironment {
|
|
|
728
728
|
return ctx.env ?? DEFAULT_PHYSICS_ENVIRONMENT;
|
|
729
729
|
}
|
|
730
730
|
|
|
731
|
+
/** Validate the complete explicit selection before any remembered state changes. */
|
|
732
|
+
export function checkedPhysicsScope(doc: CompiledDocument, scope: readonly string[] | undefined, who: string): ReadonlySet<string> | undefined {
|
|
733
|
+
if (scope === undefined) return undefined;
|
|
734
|
+
if (!Array.isArray(scope)) throw new CoreInputError(`${who} physics movement scope is ${String(scope)}, not an array of physics constraint names`);
|
|
735
|
+
const selected = new Set<string>();
|
|
736
|
+
for (const name of scope) {
|
|
737
|
+
if (typeof name !== 'string') throw new CoreInputError(`${who} physics movement scope name ${String(name)} is not a string`);
|
|
738
|
+
const constraint = doc.constraints.find((c) => c.kind === 'physics' && c.name === name)
|
|
739
|
+
?? doc.constraints.find((c) => c.name === name);
|
|
740
|
+
if (constraint === undefined) throw new CoreInputError(`${who} physics movement scope: no constraint "${name}"; this document declares [${doc.constraints.map((c) => c.name).join(', ')}]`);
|
|
741
|
+
if (constraint.kind !== 'physics') throw new CoreInputError(`${who} physics movement scope: constraint "${name}" is ${constraint.kind}, not physics`);
|
|
742
|
+
selected.add(name);
|
|
743
|
+
}
|
|
744
|
+
return selected;
|
|
745
|
+
}
|
|
746
|
+
|
|
731
747
|
/** A live track's `physicsTranslate` (issue #1416): `x` and `y` finite, or refused by name before anything moves; then `translatePhysics`. */
|
|
732
|
-
export function translateOn(ctx: PhysicsStepContext, x: number, y: number, who: string): void {
|
|
748
|
+
export function translateOn(ctx: PhysicsStepContext, x: number, y: number, who: string, scope?: ReadonlySet<string>): void {
|
|
733
749
|
for (const [k, v] of [['x', x], ['y', y]] as const) {
|
|
734
750
|
if (typeof v !== 'number' || !Number.isFinite(v)) throw new CoreInputError(`${who} physicsTranslate's ${k} is ${String(v)}, not a finite distance in the skeleton's world`);
|
|
735
751
|
}
|
|
736
|
-
translatePhysics(ctx, x, y);
|
|
752
|
+
translatePhysics(ctx, x, y, scope);
|
|
737
753
|
}
|
|
738
754
|
|
|
739
755
|
/** A live track's `physicsRotate` (issue #1416): `x`, `y` and `degrees` finite, or refused by name before anything moves; then `rotatePhysics`. */
|
|
740
|
-
export function rotateOn(ctx: PhysicsStepContext, x: number, y: number, degrees: number, who: string): void {
|
|
756
|
+
export function rotateOn(ctx: PhysicsStepContext, x: number, y: number, degrees: number, who: string, scope?: ReadonlySet<string>): void {
|
|
741
757
|
for (const [k, v] of [['x', x], ['y', y], ['degrees', degrees]] as const) {
|
|
742
758
|
if (typeof v !== 'number' || !Number.isFinite(v)) throw new CoreInputError(`${who} physicsRotate's ${k} is ${String(v)}, not a finite number`);
|
|
743
759
|
}
|
|
744
|
-
rotatePhysics(ctx, x, y, degrees);
|
|
760
|
+
rotatePhysics(ctx, x, y, degrees, scope);
|
|
745
761
|
}
|
|
746
762
|
|
|
747
763
|
/**
|
package/src/core/track.ts
CHANGED
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
* history, so a seek is "open, then step to the time".
|
|
223
223
|
*/
|
|
224
224
|
import { CoreInputError, type CompiledDocument } from './index.ts';
|
|
225
|
-
import { checkedClock, environmentOf, openRawTrack, rotateOn, setEnvironmentOn, stepRawTrack, translateOn, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
|
|
225
|
+
import { checkedClock, checkedPhysicsScope, environmentOf, openRawTrack, rotateOn, setEnvironmentOn, stepRawTrack, translateOn, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
|
|
226
226
|
import type { LocalAdjust, TimelinePlant } from './animation.ts';
|
|
227
227
|
import type { PhysicsEnvironment } from './constraints_physics.ts';
|
|
228
228
|
import { LivePhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead } from './physics_parameters.ts';
|
|
@@ -261,10 +261,12 @@ export interface LiveTrack {
|
|
|
261
261
|
readPhysicsParameters(name: string): PhysicsParameterRead;
|
|
262
262
|
/** Removes the patch for the next pose without resetting velocities, offsets or the clock. */
|
|
263
263
|
clearPhysicsParameters(name: string): void;
|
|
264
|
-
/**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
/** Scope omitted: all physics; otherwise exact physics names, validated atomically. Empty selects none, duplicates select once.
|
|
265
|
+
* Movement changes remembered world origins synchronously, before the next evaluation.
|
|
266
|
+
* spine-core's `Skeleton.physicsTranslate`: the consumer moved the whole rig by `(x, y)` in the skeleton's world (y up) since the last pose, and every physics constraint's next update sees it. */
|
|
267
|
+
physicsTranslate(x: number, y: number, scope?: readonly string[]): void;
|
|
268
|
+
/** Same scope contract as physicsTranslate. The consumer turned by `degrees` (counter-clockwise) about explicit `(x, y)` in the skeleton's world; changes remembered origins, not tip direction or angular velocity. */
|
|
269
|
+
physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void;
|
|
268
270
|
}
|
|
269
271
|
|
|
270
272
|
/** The two modes: a track that holds at its duration is the raw entry's walk; a looping one fires its events across the wrap and cuts its clipped rows (`live`). Neither keeps a non-finite value. */
|
|
@@ -336,14 +338,14 @@ class Track implements LiveTrack {
|
|
|
336
338
|
this.parameters.clear(name);
|
|
337
339
|
}
|
|
338
340
|
|
|
339
|
-
physicsTranslate(x: number, y: number): void {
|
|
341
|
+
physicsTranslate(x: number, y: number, scope?: readonly string[]): void {
|
|
340
342
|
this.live('physicsTranslate');
|
|
341
|
-
translateOn(this.track.ctx, x, y, 'the live track\'s');
|
|
343
|
+
translateOn(this.track.ctx, x, y, 'the live track\'s', checkedPhysicsScope(this.track.doc, scope, 'the live track\'s'));
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
physicsRotate(x: number, y: number, degrees: number): void {
|
|
346
|
+
physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void {
|
|
345
347
|
this.live('physicsRotate');
|
|
346
|
-
rotateOn(this.track.ctx, x, y, degrees, 'the live track\'s');
|
|
348
|
+
rotateOn(this.track.ctx, x, y, degrees, 'the live track\'s', checkedPhysicsScope(this.track.doc, scope, 'the live track\'s'));
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
/** A refused track refuses every call that would move it, naming the step it was refused at. */
|
package/src/core/tracks.ts
CHANGED
|
@@ -164,7 +164,7 @@ import { activeBones, CoreInputError, rawNumber, shownAttachment, viewColours, W
|
|
|
164
164
|
import { channelAt, keyIndexAt, SLOT_COLOUR_TIMELINES, slotRowsFrom, steppedLocalsWorld, type BoneTimelineKind, type CoreKey, type LocalAdjust, type SlotBase, type SlotColourKind, type TimelinePlant } from './animation.ts';
|
|
165
165
|
import { deformAt, setupArray, type CoreAttachmentTimeline } from './deform.ts';
|
|
166
166
|
import { eventsFired, type CoreEventKey, type CoreEventRow } from './events.ts';
|
|
167
|
-
import { checkedClock, environmentOf, openRawTracksPath, rotateOn, setEnvironmentOn, translateOn, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
|
|
167
|
+
import { checkedClock, checkedPhysicsScope, environmentOf, openRawTracksPath, rotateOn, setEnvironmentOn, translateOn, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
|
|
168
168
|
import type { PhysicsEnvironment } from './constraints_physics.ts';
|
|
169
169
|
import { LivePhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead } from './physics_parameters.ts';
|
|
170
170
|
import { slotTimelinesApply } from './skins.ts';
|
|
@@ -240,10 +240,12 @@ export interface LiveTracks {
|
|
|
240
240
|
readPhysicsParameters(name: string): PhysicsParameterRead;
|
|
241
241
|
/** Removes the patch without resetting the physics history, as LiveTrack.clearPhysicsParameters. */
|
|
242
242
|
clearPhysicsParameters(name: string): void;
|
|
243
|
-
/**
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
/** Scope omitted: all physics; otherwise exact physics names, validated atomically. Empty selects none, duplicates select once.
|
|
244
|
+
* Movement changes remembered world origins synchronously, before the next evaluation.
|
|
245
|
+
* spine-core's `Skeleton.physicsTranslate`, as `LiveTrack.physicsTranslate`. */
|
|
246
|
+
physicsTranslate(x: number, y: number, scope?: readonly string[]): void;
|
|
247
|
+
/** Same exact-name scope and explicit world pivot as `LiveTrack.physicsRotate`. */
|
|
248
|
+
physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void;
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
/**
|
|
@@ -1384,14 +1386,14 @@ class Tracks implements LiveTracks {
|
|
|
1384
1386
|
this.parameters.clear(name);
|
|
1385
1387
|
}
|
|
1386
1388
|
|
|
1387
|
-
physicsTranslate(x: number, y: number): void {
|
|
1389
|
+
physicsTranslate(x: number, y: number, scope?: readonly string[]): void {
|
|
1388
1390
|
this.live('physicsTranslate');
|
|
1389
|
-
translateOn(this.path.ctx, x, y, 'the live tracks\'');
|
|
1391
|
+
translateOn(this.path.ctx, x, y, 'the live tracks\'', checkedPhysicsScope(this.doc, scope, 'the live tracks\''));
|
|
1390
1392
|
}
|
|
1391
1393
|
|
|
1392
|
-
physicsRotate(x: number, y: number, degrees: number): void {
|
|
1394
|
+
physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void {
|
|
1393
1395
|
this.live('physicsRotate');
|
|
1394
|
-
rotateOn(this.path.ctx, x, y, degrees, 'the live tracks\'');
|
|
1396
|
+
rotateOn(this.path.ctx, x, y, degrees, 'the live tracks\'', checkedPhysicsScope(this.doc, scope, 'the live tracks\''));
|
|
1395
1397
|
}
|
|
1396
1398
|
|
|
1397
1399
|
private live(call: string): void {
|
package/src/runtime/curves.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/runtime/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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';
|
package/src/runtime/model.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime poser's load-time half (issue #1591, stage
|
|
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
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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 =
|
|
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
|
|
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'
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
+
}
|
package/src/runtime/pose.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime poser's per-instance half (issue #1591, stage
|
|
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
|
|
17
|
-
*
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|