@rig-c/core 2.75.0 → 2.77.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.
@@ -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 4): 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.
@@ -9,21 +9,23 @@
9
9
  * The oracle's sample, as `src/core/animation.ts` poses it (`poseAnimations`):
10
10
  * every pose is posed from the setup pose — the bones' setup local values,
11
11
  * then the animation's bone timelines at `t` at full weight, then the world
12
- * transforms parents first, then the ik and transform constraints in the
12
+ * transforms parents first, then each slot's colour, attachment and own deform
13
+ * timelines, then the ik, transform, path, physics and slider constraints in the
13
14
  * document's order with their timelines' values at `t`, each followed by the
14
- * bones below what it moved posed again; then each slot's colour and what it
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.
15
+ * bones below what it moved posed again, then the sliders' slot and deform
16
+ * timelines; then, when asked, every shown attachment's world vertices. Nothing
17
+ * of one unstepped pose survives but key cursors. A stepped pose also keeps
18
+ * its physics clock, offsets, velocities and lags; sliders select values
19
+ * within this pose rather than advancing an independent track.
18
20
  *
19
21
  * ## The arithmetic is the oracle's
20
22
  *
21
23
  * Every formula below is the one the named function of `src/core/` computes,
22
24
  * in its order of operations, on the same doubles: a bone's frame and its
23
25
  * five inherit modes (`world.ts`), the timelines (`animation.ts`), the one-
24
- * and two-bone ik, the world-space transform, the read-back of a moved bone's
25
- * local values and the update order (`constraints.ts`), a region's corners
26
- * and a mesh's vertices (`vertices.ts`). Each of those orders was fixed
26
+ * and two-bone ik, the world-space transform, the path constraint (`constraints_path.ts`),
27
+ * the read-back of a moved bone's local values and the update order (`constraints.ts`),
28
+ * a region's corners and a mesh's vertices (`vertices.ts`). Each of those orders was fixed
27
29
  * there by measurement against spine-core 4.3.13 to the last bit; the runtime
28
30
  * does not re-measure them, it is held to the oracle at tolerance 0
29
31
  * (`tools/core_gate.ts`, *The runtime poser*), so an order changed here is a
@@ -31,12 +33,14 @@
31
33
  *
32
34
  * ⚠️ The exactness hazards, each kept as the oracle has it: the setup value
33
35
  * plus 0 before a timeline's first key (which turns a `-0` setup into `0`);
34
- * a keyed ik or transform value reaching the pose through the setup blend
36
+ * a keyed ik, transform or path value reaching the pose through the setup blend
35
37
  * `setup + (value − setup)·1`; `Math.PI` for a folded uniform two-bone ik and
36
38
  * the runtime's 3.1415927 everywhere else; the `(−180, 180]` wrap; a mix of
37
39
  * exactly 0 touching nothing; an inactive bone posed as zeros by the
38
40
  * hierarchy and never posed again by a constraint, nor the bones below it.
39
41
  */
42
+ import type { CorePhysicsRecord } from '../core/constraints_physics.ts';
43
+ import { freshPhysicsState, resetPhysicsState, stepRuntimePhysics, type PhysicsState } from './physics.ts';
40
44
  import { COLLAPSED_X_AXIS_SQ, RUNTIME_DEG, RUNTIME_PI } from '../core/world.ts';
41
45
  import { channelValue, keyAt, deformFraction, CURVE_STEPPED } from './curves.ts';
42
46
  import {
@@ -59,6 +63,13 @@ import {
59
63
  MODE_NO_SCALE_OR_REFLECTION,
60
64
  MODE_NORMAL,
61
65
  MODE_ONLY_TRANSLATION,
66
+ PATH_POSITION_PERCENT,
67
+ PATH_ROTATE_CHAIN,
68
+ PATH_ROTATE_CHAIN_SCALE,
69
+ PATH_ROTATE_TANGENT,
70
+ PATH_SPACING_LENGTH,
71
+ PATH_SPACING_PERCENT,
72
+ PATH_SPACING_PROPORTIONAL,
62
73
  RAD,
63
74
  RuntimeInputError,
64
75
  SCALE_Y_UNIFORM,
@@ -71,14 +82,19 @@ import {
71
82
  W_Y,
72
83
  WORLDS,
73
84
  type RuntimeAnimation,
85
+ type RuntimeConstraint,
74
86
  type RuntimeEvent,
75
87
  type RuntimeIk,
76
88
  type RuntimeModel,
89
+ type RuntimePath,
77
90
  type RuntimeTransform,
91
+ type RuntimeSlider,
92
+ type RuntimePhysicsTimeline,
78
93
  } from './model.ts';
79
94
 
80
95
  const DEG = RUNTIME_DEG;
81
96
  const TWO_PI = 2 * RUNTIME_PI;
97
+ const PATH_EPSILON = 0.00001;
82
98
  // The transform properties, by `TRANSFORM_PROPERTIES` index (src/core/constraints.ts).
83
99
  const P_ROTATE = 0;
84
100
  const P_X = 1;
@@ -98,6 +114,10 @@ function volumeScaleY(scaleY: number, s: number): number {
98
114
 
99
115
  const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
100
116
 
117
+ /** A caller-supplied physics clock; animation/track time still uses dt itself. */
118
+ export interface RuntimePoseOptions { readonly clock?: (dt: number) => number }
119
+ export type RuntimePhysicsPhase = 'none' | 'reset' | 'update';
120
+
101
121
  /** One skeleton's pose, every value in place. Read `world`, `light`, `shown` and `vertices` after a pose. */
102
122
  export class RuntimePose {
103
123
  readonly model: RuntimeModel;
@@ -125,11 +145,20 @@ export class RuntimePose {
125
145
 
126
146
  private current: RuntimeAnimation | null = null;
127
147
  private readonly cursors: Int32Array;
128
- // The constraints' values as posed: an ik's mix, softness and flags, a transform's six mixes.
148
+ // The constraints' values as posed: an ik's mix, softness and flags, a transform's six mixes, a path's position, spacing and three mixes.
129
149
  private readonly ikMix: Float64Array;
130
150
  private readonly ikSoftness: Float64Array;
131
151
  private readonly ikFlags: Uint8Array;
132
152
  private readonly mixes: Float64Array;
153
+ private readonly pathValues: Float64Array;
154
+ private readonly pathSlotSign: Int8Array;
155
+ private readonly pathSpaces: Float64Array;
156
+ private readonly pathLengths: Float64Array;
157
+ private readonly pathPositions: Float64Array;
158
+ private readonly pathWorldPts: Float64Array;
159
+ private readonly pathChain: Float64Array;
160
+ private readonly pathCurveEnds: Float64Array;
161
+ private readonly pathSegmentEnds = new Float64Array(10);
133
162
  // The update order's scratch: the bones a constraint changed and moved in world space, in order (repeats kept, as the oracle keeps them), and per-bone marks.
134
163
  private readonly changed: Int32Array;
135
164
  private readonly inWorld: Int32Array;
@@ -139,11 +168,34 @@ export class RuntimePose {
139
168
  private readonly visit: Uint8Array;
140
169
  private readonly w = new Float64Array(WORLDS);
141
170
 
142
- constructor(model: RuntimeModel) {
171
+ /** Sum of caller-supplied physics deltas, independent of animation time. */
172
+ physicsTime = 0;
173
+ private physicsLast = -1;
174
+ private physicsPhase: RuntimePhysicsPhase = 'none';
175
+ private physicsReady = false;
176
+ private readonly hasPhysics: boolean;
177
+ private readonly physicsStates: PhysicsState[];
178
+ private readonly physicsRecords: Array<CorePhysicsRecord | null>;
179
+ private readonly physicsScope: Uint8Array;
180
+ private readonly environment = new Float64Array([1, 0, 0, 1]);
181
+ private readonly sliderTime: Float64Array;
182
+ private readonly sliderMix: Float64Array;
183
+ private readonly sliderAt: Float64Array;
184
+ private readonly clock: ((dt: number) => number) | undefined;
185
+
186
+ constructor(model: RuntimeModel, options: RuntimePoseOptions = {}) {
143
187
  this.model = model;
188
+ this.clock = options.clock;
189
+ this.hasPhysics = model.constraints.some(c => c.kind === 'physics');
190
+ this.physicsStates = model.constraints.map(() => freshPhysicsState());
191
+ this.physicsRecords = model.constraints.map(c => c.kind === 'physics' ? { ...c.setup } : null);
192
+ this.sliderTime = new Float64Array(model.constraints.length);
193
+ this.sliderMix = new Float64Array(model.constraints.length);
194
+ this.sliderAt = new Float64Array(model.constraints.length);
144
195
  const nb = model.boneNames.length;
145
196
  const ns = model.slots.length;
146
197
  const nc = model.constraints.length;
198
+ this.physicsScope = new Uint8Array(nc);
147
199
  this.locals = new Float64Array(nb * LOCALS);
148
200
  this.world = new Float64Array(nb * WORLDS);
149
201
  this.light = new Float64Array(ns * 4);
@@ -156,6 +208,16 @@ export class RuntimePose {
156
208
  this.ikSoftness = new Float64Array(nc);
157
209
  this.ikFlags = new Uint8Array(nc);
158
210
  this.mixes = new Float64Array(nc * 6);
211
+ this.pathValues = new Float64Array(nc * 5);
212
+ this.pathSlotSign = new Int8Array(nc);
213
+ const ms = model.maxPathSpaces;
214
+ const mv = model.maxPathVertices;
215
+ this.pathSpaces = new Float64Array(ms);
216
+ this.pathLengths = new Float64Array(ms);
217
+ this.pathPositions = new Float64Array(ms > 0 ? ms * 3 + 2 : 0);
218
+ this.pathWorldPts = new Float64Array(mv);
219
+ this.pathChain = new Float64Array(mv > 0 ? mv + 4 : 0);
220
+ this.pathCurveEnds = new Float64Array(mv > 0 ? Math.ceil(mv / 6) + 1 : 0);
159
221
  // Sized by the longest target list (`RuntimeModel.scratch`): a transform may name one bone several times, and every entry is applied and reposed.
160
222
  this.changed = new Int32Array(model.scratch);
161
223
  this.inWorld = new Int32Array(model.scratch);
@@ -168,6 +230,10 @@ export class RuntimePose {
168
230
  /** The animation this pose plays (a covered one: `model.animation` throws a `RuntimeRefusal` for a refused one), from track time 0. */
169
231
  setAnimation(name: string): void {
170
232
  this.current = this.model.animation(name);
233
+ this.physicsTime = 0;
234
+ this.physicsLast = -1;
235
+ this.physicsReady = false;
236
+ for (let k = 0; k < this.physicsStates.length; k++) resetPhysicsState(this.physicsStates[k], 0);
171
237
  this.cursors.fill(-1);
172
238
  this.trackTime = 0;
173
239
  this.time = 0;
@@ -190,40 +256,61 @@ export class RuntimePose {
190
256
  }
191
257
 
192
258
  /** The setup pose: every constraint applied with its own values, every slot as it is set up. */
193
- setupPose(): void {
259
+ setupPose(physics: RuntimePhysicsPhase = 'none'): void {
260
+ this.physicsPhase = physics;
194
261
  this.current = null;
195
262
  this.time = 0;
196
263
  this.eventTime = -1;
197
264
  this.fired.length = 0;
265
+ this.physicsValues(null, 0, false);
198
266
  this.locals.set(this.model.setup);
199
267
  this.hierarchy();
200
- this.constraintValues(null, 0);
201
- this.solveConstraints();
202
268
  this.slotValues(null, 0);
203
269
  this.deformValues(null, 0);
270
+ this.constraintValues(null, 0);
271
+ this.solveConstraints();
272
+ this.sliderAttachments();
204
273
  }
205
274
 
206
275
  /** The animation posed at its time `t`, from the setup pose (the header). */
207
- apply(t: number): void {
276
+ apply(t: number, physics: RuntimePhysicsPhase = 'none', delta = 0): void {
208
277
  const anim = this.current;
209
278
  if (anim === null) throw new RuntimeInputError('apply: no animation is set (setAnimation first)');
210
279
  if (!Number.isFinite(t)) throw new RuntimeInputError(`apply: the time ${t} is not finite`);
280
+ if (!Number.isFinite(delta) || delta < 0) throw new RuntimeInputError(`apply: physics delta ${delta} is not finite and at or above 0`);
281
+ this.physicsPhase = physics;
282
+ // Reset keys see the old clock. The skeleton moves by delta only after application.
283
+ this.physicsValues(anim, t, physics !== 'none');
284
+ if (physics !== 'none') {
285
+ this.physicsTime += delta;
286
+ this.physicsReady = true;
287
+ }
211
288
  this.collectEvents(anim, t);
212
289
  this.time = t;
213
290
  this.boneTimelines(anim, t);
214
291
  this.hierarchy();
215
- this.constraintValues(anim, t);
216
- this.solveConstraints();
217
292
  this.slotValues(anim, t);
218
293
  this.deformValues(anim, t);
294
+ this.constraintValues(anim, t);
295
+ this.solveConstraints();
296
+ this.sliderAttachments();
219
297
  }
220
298
 
221
299
  /** 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
300
  step(dt: number): void {
223
301
  const anim = this.current;
224
302
  if (anim === null) throw new RuntimeInputError('step: no animation is set (setAnimation first)');
303
+ if (!Number.isFinite(dt) || dt < 0) throw new RuntimeInputError(`step: delta ${dt} is not finite and at or above 0`);
304
+ if (!this.physicsReady && this.hasPhysics) {
305
+ // Initial physics setup must not consume the event interval of this step.
306
+ const lastEventTime = this.eventTime;
307
+ this.apply(this.time, 'reset', 0);
308
+ this.eventTime = lastEventTime;
309
+ }
310
+ const moved = this.clock === undefined ? dt : this.clock(dt);
311
+ if (!Number.isFinite(moved) || moved < 0) throw new RuntimeInputError(`step: physics clock moved by ${moved}, not finite and at or above 0`);
225
312
  this.trackTime += dt;
226
- this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration);
313
+ this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration, 'update', moved);
227
314
  }
228
315
 
229
316
  /** The oracle interval (last,t], tail then head on a wrap; never copies a payload. */
@@ -427,9 +514,55 @@ export class RuntimePose {
427
514
  this.ikSoftness[k] = c.softness + (channelValue(tl.curve, i, 1, t) - c.softness) * 1;
428
515
  this.ikFlags[k] = (tl.flags as Uint8Array)[i];
429
516
  }
430
- } else {
517
+ } else if (c.kind === 'transform') {
431
518
  const o = k * 6;
432
519
  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;
520
+ } else if (c.kind === 'slider') {
521
+ const keyed = anim === null ? null : anim.sliders[k];
522
+ let time = c.setup.time, mix = c.setup.mix;
523
+ if (keyed !== null) {
524
+ if (keyed.time !== null) {
525
+ const q = keyAt(keyed.time, this.cursors, keyed.timeCursor, t);
526
+ if (q >= 0) time += (channelValue(keyed.time, q, 0, t) - time) * 1;
527
+ }
528
+ if (keyed.mix !== null) {
529
+ const q = keyAt(keyed.mix, this.cursors, keyed.mixCursor, t);
530
+ if (q >= 0) mix += (channelValue(keyed.mix, q, 0, t) - mix) * 1;
531
+ }
532
+ }
533
+ this.sliderTime[k] = time;
534
+ this.sliderMix[k] = mix;
535
+ } else if (c.kind === 'path') {
536
+ const keyed = anim === null ? null : anim.paths[k];
537
+ let position = c.position;
538
+ let spacing = c.spacing;
539
+ let mixRotate = c.mixRotate;
540
+ let mixX = c.mixX;
541
+ let mixY = c.mixY;
542
+ if (keyed !== null) {
543
+ if (keyed.position !== null) {
544
+ const q = keyAt(keyed.position, this.cursors, keyed.positionCursor, t);
545
+ if (q >= 0) position = c.position + (channelValue(keyed.position, q, 0, t) - c.position) * 1;
546
+ }
547
+ if (keyed.spacing !== null) {
548
+ const q = keyAt(keyed.spacing, this.cursors, keyed.spacingCursor, t);
549
+ if (q >= 0) spacing = c.spacing + (channelValue(keyed.spacing, q, 0, t) - c.spacing) * 1;
550
+ }
551
+ if (keyed.mix !== null) {
552
+ const q = keyAt(keyed.mix, this.cursors, keyed.mixCursor, t);
553
+ if (q >= 0) {
554
+ mixRotate = c.mixRotate + (channelValue(keyed.mix, q, 0, t) - c.mixRotate) * 1;
555
+ mixX = c.mixX + (channelValue(keyed.mix, q, 1, t) - c.mixX) * 1;
556
+ mixY = c.mixY + (channelValue(keyed.mix, q, 2, t) - c.mixY) * 1;
557
+ }
558
+ }
559
+ }
560
+ const o = k * 5;
561
+ this.pathValues[o] = position;
562
+ this.pathValues[o + 1] = spacing;
563
+ this.pathValues[o + 2] = mixRotate;
564
+ this.pathValues[o + 3] = mixX;
565
+ this.pathValues[o + 4] = mixY;
433
566
  }
434
567
  }
435
568
  }
@@ -437,11 +570,661 @@ export class RuntimePose {
437
570
  /** Every applied constraint in the document's order, each followed by the update order's repose (`applyConstraints`). */
438
571
  private solveConstraints(): void {
439
572
  const cs = this.model.constraints;
573
+ const hasPath = this.model.hasPath;
440
574
  for (let k = 0; k < cs.length; k++) {
575
+ if (hasPath) this.snapSlotBones(k, false);
441
576
  const c = cs[k];
442
577
  if (!c.applied) continue;
443
578
  if (c.kind === 'ik') this.solveIk(c, k);
444
- else this.solveTransform(c, k);
579
+ else if (c.kind === 'transform') this.solveTransform(c, k);
580
+ else if (c.kind === 'slider') this.solveSlider(c, k);
581
+ else if (c.kind === 'path') this.solvePath(c, k);
582
+ else if (this.physicsPhase !== 'none' && !this.model.omitPhysics) {
583
+ const b = c.bones[0];
584
+ if (stepRuntimePhysics(this.physicsRecords[k]!, this.physicsStates[k], this.world, b * WORLDS, this.model.lengths[b], this.physicsTime, this.physicsPhase === 'reset', this.environment)) {
585
+ this.changed[0] = this.inWorld[0] = b;
586
+ this.repose(1, 1);
587
+ }
588
+ }
589
+ if (hasPath) this.snapSlotBones(k, true);
590
+ }
591
+ }
592
+
593
+ private snapSlotBones(at: number, after: boolean): void {
594
+ const cs = this.model.constraints;
595
+ const W = this.world;
596
+ for (let k = 0; k < cs.length; k++) {
597
+ const c = cs[k];
598
+ if (c.kind !== 'path' || c.slotEventAt !== at || c.slotEventAfter !== after || (k === at && !after)) continue;
599
+ const sb = c.slotBone * WORLDS;
600
+ this.pathSlotSign[k] = W[sb + W_A] * W[sb + W_D] - W[sb + W_B] * W[sb + W_C] > 0 ? 1 : -1;
601
+ }
602
+ }
603
+
604
+ /** Reset where the consumer starts: the setup pose or animation pose zero. */
605
+ resetPhysics(at: 'setup' | 'animation' = 'animation'): void {
606
+ this.physicsTime = 0;
607
+ this.physicsLast = -1;
608
+ for (let k = 0; k < this.physicsStates.length; k++) resetPhysicsState(this.physicsStates[k], 0);
609
+ if (at === 'animation') this.apply(0, 'reset', 0);
610
+ else {
611
+ const current = this.current;
612
+ this.setupPose('reset');
613
+ this.current = current;
614
+ }
615
+ this.physicsReady = true;
616
+ }
617
+
618
+ private checkPhysicsScope(scope: readonly string[] | undefined): Uint8Array | null {
619
+ if (scope === undefined) return null;
620
+ if (!Array.isArray(scope)) throw new RuntimeInputError(`physics movement scope is ${String(scope)}, not an array of physics constraint names`);
621
+ const selected = this.physicsScope;
622
+ selected.fill(0);
623
+ const cs = this.model.constraints;
624
+ for (let i = 0; i < scope.length; i++) {
625
+ const name = scope[i];
626
+ if (typeof name !== 'string') throw new RuntimeInputError(`physics movement scope name ${String(name)} is not a string`);
627
+ let found: RuntimeConstraint | undefined;
628
+ for (let k = 0; k < cs.length; k++) {
629
+ if (cs[k].kind === 'physics' && cs[k].name === name) {
630
+ found = cs[k];
631
+ break;
632
+ }
633
+ }
634
+ if (found === undefined) {
635
+ for (let k = 0; k < cs.length; k++) {
636
+ if (cs[k].name === name) {
637
+ found = cs[k];
638
+ break;
639
+ }
640
+ }
641
+ }
642
+ if (found === undefined) throw new RuntimeInputError(`physics movement scope: no constraint "${name}"; this document declares [${cs.map((c) => c.name).join(', ')}]`);
643
+ if (found.kind !== 'physics') throw new RuntimeInputError(`physics movement scope: constraint "${name}" is ${found.kind}, not physics`);
644
+ selected[found.index] = 1;
645
+ }
646
+ return this.model.ignorePhysicsScope ? null : selected;
647
+ }
648
+
649
+ /** External motion changes the previous origin, never the authored locals. */
650
+ physicsTranslate(x: number, y: number, scope?: readonly string[]): void {
651
+ const selected = this.checkPhysicsScope(scope);
652
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new RuntimeInputError('physicsTranslate: x and y must be finite');
653
+ for (let k = 0; k < this.physicsStates.length; k++) {
654
+ if (selected !== null && selected[k] === 0) continue;
655
+ const s = this.physicsStates[k];
656
+ s.ux -= x; s.uy -= y; s.cx -= x; s.cy -= y;
657
+ }
658
+ }
659
+
660
+ physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void {
661
+ const selected = this.checkPhysicsScope(scope);
662
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(degrees)) throw new RuntimeInputError('physicsRotate: x, y and degrees must be finite');
663
+ const r = degrees * RAD, cos = Math.cos(r), sin = Math.sin(r);
664
+ for (let k = 0; k < this.physicsStates.length; k++) {
665
+ if (selected !== null && selected[k] === 0) continue;
666
+ const s = this.physicsStates[k];
667
+ const dx = s.cx - x, dy = s.cy - y;
668
+ const tx = dx * cos - dy * sin - dx, ty = dx * sin + dy * cos - dy;
669
+ s.ux -= tx; s.uy -= ty; s.cx -= tx; s.cy -= ty;
670
+ }
671
+ }
672
+
673
+ setEnvironment(env: { readonly wind: readonly [number, number]; readonly gravity: readonly [number, number] }): void {
674
+ 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');
675
+ this.environment[0] = env.wind[0]; this.environment[1] = env.wind[1];
676
+ this.environment[2] = env.gravity[0]; this.environment[3] = env.gravity[1];
677
+ }
678
+
679
+ /** The per-pass values start at setup, before any slider writes. */
680
+ private physicsValues(anim: RuntimeAnimation | null, t: number, resets: boolean): void {
681
+ const cs = this.model.constraints;
682
+ for (let k = 0; k < cs.length; k++) {
683
+ const c = cs[k];
684
+ if (c.kind !== 'physics') continue;
685
+ const r = this.physicsRecords[k]!;
686
+ r.inertia = c.setup.inertia; r.strength = c.setup.strength; r.damping = c.setup.damping;
687
+ r.massInverse = c.setup.massInverse; r.wind = c.setup.wind; r.gravity = c.setup.gravity; r.mix = c.setup.mix;
688
+ }
689
+ if (anim !== null) this.physicsTimelines(anim.physics, t, 1, false, true, resets);
690
+ if (resets) this.physicsLast = t;
691
+ }
692
+
693
+ private physicsTimelines(timelines: readonly RuntimePhysicsTimeline[], t: number, alpha: number, additive: boolean, setup: boolean, resets = false): void {
694
+ for (let q = 0; q < timelines.length; q++) {
695
+ const tl = timelines[q];
696
+ if (tl.kind === 'reset') {
697
+ if (!resets) continue; // a slider applies at one value; no key is crossed
698
+ let crossed = false;
699
+ const last = this.physicsLast;
700
+ for (let i = 0; i < tl.curve.keys; i++) {
701
+ const time = tl.curve.times[i];
702
+ if (t < last ? time > last || time <= t : time > last && time <= t) crossed = true;
703
+ }
704
+ if (crossed) for (let j = 0; j < tl.targets.length; j++) resetPhysicsState(this.physicsStates[tl.targets[j]], this.physicsTime);
705
+ continue;
706
+ }
707
+ const i = keyAt(tl.curve, this.cursors, tl.cursor, t);
708
+ if (i < 0) {
709
+ // A track's before-first key restores setup even if an earlier global
710
+ // timeline wrote this parameter. A slider's before-first key writes nothing.
711
+ if (setup) for (let j = 0; j < tl.targets.length; j++) {
712
+ const k = tl.targets[j], r = this.physicsRecords[k]!;
713
+ const original = (this.model.constraints[k] as { setup: CorePhysicsRecord }).setup;
714
+ if (tl.kind === 'mass') r.massInverse = original.massInverse;
715
+ else r[tl.kind] = original[tl.kind];
716
+ }
717
+ continue;
718
+ }
719
+ const v = channelValue(tl.curve, i, 0, t);
720
+ for (let j = 0; j < tl.targets.length; j++) {
721
+ const k = tl.targets[j], r = this.physicsRecords[k]!;
722
+ const from = setup ? (this.model.constraints[k] as { setup: CorePhysicsRecord }).setup : r;
723
+ if (tl.kind === 'mass') {
724
+ const mass = 1 / from.massInverse;
725
+ r.massInverse = 1 / (mass + (v - mass) * alpha);
726
+ } else {
727
+ r[tl.kind] = additive && (tl.kind === 'wind' || tl.kind === 'gravity') ? r[tl.kind] + v * alpha : from[tl.kind] + (v - from[tl.kind]) * alpha;
728
+ }
729
+ }
730
+ }
731
+ }
732
+
733
+ /** Read the dial where previous constraints left it; this value is never track time. */
734
+ private solveSlider(c: RuntimeSlider, k: number): void {
735
+ const alpha = this.sliderMix[k];
736
+ if (alpha === 0) return;
737
+ let time = this.sliderTime[k];
738
+ const r = c.setup, a = c.animation;
739
+ if (c.bone >= 0) {
740
+ const b = c.bone, w = b * WORLDS, l = b * LOCALS;
741
+ let value: number;
742
+ if (r.local) {
743
+ 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;
744
+ value = this.locals[l + field] + 0;
745
+ } else {
746
+ const W = this.world, x = W[w], y = W[w + 2], bx = W[w + 1], by = W[w + 3];
747
+ switch (c.property) {
748
+ case 0: value = Math.atan2(y, x) * DEG + (x * by - bx * y > 0 ? 0 : -0); if (value < 0) value += 360; break;
749
+ case 1: value = x * 0 + bx * 0 + W[w + W_X]; break;
750
+ case 2: value = y * 0 + by * 0 + W[w + W_Y]; break;
751
+ case 3: value = Math.sqrt(x * x + y * y) + 0; break;
752
+ case 4: value = Math.sqrt(bx * bx + by * by) + 0; break;
753
+ default: value = (Math.atan2(by, bx) - Math.atan2(y, x)) * DEG - 90 + 0;
754
+ }
755
+ }
756
+ time = r.to + (value - r.from) * r.scale;
757
+ time = r.loop ? a.duration === 0 ? 0 : a.duration + time % a.duration : Math.max(0, time);
758
+ }
759
+ const at = this.model.sliderAsTrack ? this.time : r.loop && a.duration !== 0 ? time % a.duration : time;
760
+ this.sliderAt[k] = at;
761
+ if (this.physicsPhase !== 'none') this.physicsTimelines(a.physics, at, alpha, r.additive, false);
762
+ const L = this.locals, S = this.model.setup;
763
+ for (let q = 0; q < a.sliderBones.length; q++) {
764
+ const tl = a.sliderBones[q], i = keyAt(tl.curve, this.cursors, tl.cursor, at);
765
+ if (i < 0) continue;
766
+ const o = tl.bone * LOCALS;
767
+ for (let f = 0; f < tl.fields.length; f++) {
768
+ const n = o + tl.fields[f], v = channelValue(tl.curve, i, f, at), current = L[n], setup = S[n];
769
+ if (tl.multiply[f]) {
770
+ if (r.additive) L[n] = current + (v * setup - setup) * alpha;
771
+ else {
772
+ const target = setup * v, from = Math.abs(current) * Math.sign(target);
773
+ L[n] = alpha === 1 ? target : from + (target - from) * alpha;
774
+ }
775
+ } else L[n] = r.additive ? current + v * alpha : current + (setup + v - current) * alpha;
776
+ }
777
+ }
778
+ for (let q = 0; q < c.bones.length; q++) this.changed[q] = c.bones[q];
779
+ if (c.bones.length > 0) this.repose(c.bones.length, 0);
780
+ }
781
+
782
+ /** A path constraint (`solvePath` in `src/core/constraints_path.ts`), every buffer overwritten in place. */
783
+ private solvePath(c: RuntimePath, k: number): void {
784
+ if (c.attachment < 0 || this.model.omitPath) return;
785
+ const o5 = k * 5;
786
+ const position = this.pathValues[o5];
787
+ const spacing = this.pathValues[o5 + 1];
788
+ const mixRotate = this.pathValues[o5 + 2];
789
+ const mixX = this.pathValues[o5 + 3];
790
+ const mixY = this.pathValues[o5 + 4];
791
+ if (mixRotate === 0 && mixX === 0 && mixY === 0) return;
792
+ const wantsTangent = c.rotateMode === PATH_ROTATE_TANGENT;
793
+ const scale = c.rotateMode === PATH_ROTATE_CHAIN_SCALE;
794
+ const bones = c.bones;
795
+ const boneCount = bones.length;
796
+ const spaceCount = wantsTangent ? boneCount : boneCount + 1;
797
+ const spaces = this.pathSpaces;
798
+ const lengths = this.pathLengths;
799
+ const W = this.world;
800
+ const modelLengths = this.model.lengths;
801
+ spaces[0] = 0;
802
+ if (c.spacingMode === PATH_SPACING_PERCENT) {
803
+ if (scale) {
804
+ for (let i = 0; i < spaceCount - 1; i++) {
805
+ const bi = bones[i];
806
+ const boneLength = modelLengths[bi];
807
+ const w = bi * WORLDS;
808
+ const x = boneLength * W[w + W_A];
809
+ const y = boneLength * W[w + W_C];
810
+ lengths[i] = Math.sqrt(x * x + y * y);
811
+ }
812
+ }
813
+ for (let i = 1; i < spaceCount; i++) spaces[i] = spacing;
814
+ } else if (c.spacingMode === PATH_SPACING_PROPORTIONAL) {
815
+ let sum = 0;
816
+ for (let i = 0, n = spaceCount - 1; i < n; ) {
817
+ const bi = bones[i];
818
+ const boneLength = modelLengths[bi];
819
+ if (boneLength < PATH_EPSILON) {
820
+ if (scale) lengths[i] = 0;
821
+ spaces[++i] = spacing;
822
+ } else {
823
+ const w = bi * WORLDS;
824
+ const x = boneLength * W[w + W_A];
825
+ const y = boneLength * W[w + W_C];
826
+ const length = Math.sqrt(x * x + y * y);
827
+ if (scale) lengths[i] = length;
828
+ spaces[++i] = length;
829
+ sum += length;
830
+ }
831
+ }
832
+ if (sum > 0) {
833
+ sum = (spaceCount / sum) * spacing;
834
+ for (let i = 1; i < spaceCount; i++) spaces[i] *= sum;
835
+ }
836
+ } else {
837
+ const addsLength = c.spacingMode === PATH_SPACING_LENGTH;
838
+ for (let i = 0, n = spaceCount - 1; i < n; ) {
839
+ const bi = bones[i];
840
+ const boneLength = modelLengths[bi];
841
+ if (boneLength < PATH_EPSILON) {
842
+ if (scale) lengths[i] = 0;
843
+ spaces[++i] = spacing;
844
+ } else {
845
+ const w = bi * WORLDS;
846
+ const x = boneLength * W[w + W_A];
847
+ const y = boneLength * W[w + W_C];
848
+ const length = Math.sqrt(x * x + y * y);
849
+ if (scale) lengths[i] = length;
850
+ spaces[++i] = ((addsLength ? Math.max(0, boneLength + spacing) : spacing) * length) / boneLength;
851
+ }
852
+ }
853
+ }
854
+ this.positionsOfPath(c, position, spaceCount, wantsTangent);
855
+ const positions = this.pathPositions;
856
+ let atX = positions[0];
857
+ let atY = positions[1];
858
+ let offsetRotation = c.offsetRotation;
859
+ let fromTip: boolean;
860
+ if (offsetRotation === 0) fromTip = c.rotateMode === PATH_ROTATE_CHAIN;
861
+ else {
862
+ fromTip = false;
863
+ let positive: boolean;
864
+ if (c.slotEventAt === k && !c.slotEventAfter) {
865
+ const sb = c.slotBone * WORLDS;
866
+ positive = W[sb + W_A] * W[sb + W_D] - W[sb + W_B] * W[sb + W_C] > 0;
867
+ } else {
868
+ positive = c.slotEventAt >= 0 && this.pathSlotSign[k] > 0;
869
+ }
870
+ offsetRotation *= positive ? RAD : -RAD;
871
+ }
872
+ for (let i = 0, p = 3; i < boneCount; i++, p += 3) {
873
+ const bi = bones[i];
874
+ const o = bi * WORLDS;
875
+ let wa = W[o + W_A];
876
+ let wb = W[o + W_B];
877
+ let wc = W[o + W_C];
878
+ let wd = W[o + W_D];
879
+ const wx = W[o + W_X] + (atX - W[o + W_X]) * mixX;
880
+ const wy = W[o + W_Y] + (atY - W[o + W_Y]) * mixY;
881
+ const x = positions[p];
882
+ const y = positions[p + 1];
883
+ const dx = x - atX;
884
+ const dy = y - atY;
885
+ if (scale) {
886
+ const length = lengths[i];
887
+ if (length >= PATH_EPSILON) {
888
+ const s = (Math.sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
889
+ wa *= s;
890
+ wc *= s;
891
+ }
892
+ }
893
+ atX = x;
894
+ atY = y;
895
+ if (mixRotate > 0) {
896
+ const a = wa;
897
+ const bb = wb;
898
+ const cc = wc;
899
+ const d = wd;
900
+ let r: number;
901
+ if (wantsTangent) r = positions[p - 1];
902
+ else if (spaces[i + 1] === 0) r = positions[p + 2];
903
+ else r = Math.atan2(dy, dx);
904
+ r -= Math.atan2(cc, a);
905
+ if (fromTip) {
906
+ const cos = Math.cos(r);
907
+ const sin = Math.sin(r);
908
+ const length = modelLengths[bi];
909
+ atX += (length * (cos * a - sin * cc) - dx) * mixRotate;
910
+ atY += (length * (sin * a + cos * cc) - dy) * mixRotate;
911
+ } else r += offsetRotation;
912
+ if (r > RUNTIME_PI) r -= TWO_PI;
913
+ else if (r < -RUNTIME_PI) r += TWO_PI;
914
+ r *= mixRotate;
915
+ const cos = Math.cos(r);
916
+ const sin = Math.sin(r);
917
+ wa = cos * a - sin * cc;
918
+ wb = cos * bb - sin * d;
919
+ wc = sin * a + cos * cc;
920
+ wd = sin * bb + cos * d;
921
+ }
922
+ W[o + W_A] = wa;
923
+ W[o + W_B] = wb;
924
+ W[o + W_C] = wc;
925
+ W[o + W_D] = wd;
926
+ W[o + W_X] = wx;
927
+ W[o + W_Y] = wy;
928
+ this.changed[i] = bi;
929
+ this.inWorld[i] = bi;
930
+ }
931
+ if (boneCount > 0) this.repose(boneCount, boneCount);
932
+ }
933
+
934
+ private computePathWorld(c: RuntimePath): void {
935
+ const at = this.model.attachments[c.attachment];
936
+ const W = this.world;
937
+ const pts = this.pathWorldPts;
938
+ const deformed = this.deformed[c.attachment] !== 0;
939
+ if (at.bindingCounts.length === 0) {
940
+ const b = c.slotBone * WORLDS;
941
+ const a = W[b + W_A];
942
+ const bb = W[b + W_B];
943
+ const cc = W[b + W_C];
944
+ const d = W[b + W_D];
945
+ const wx = W[b + W_X];
946
+ const wy = W[b + W_Y];
947
+ const local = deformed ? this.deform : at.local;
948
+ const lo = deformed ? at.deformOffset : 0;
949
+ for (let q = 0; q < c.pathVertices; q += 2) {
950
+ const x = local[lo + q];
951
+ const y = local[lo + q + 1];
952
+ pts[q] = x * a + y * bb + wx;
953
+ pts[q + 1] = x * cc + y * d + wy;
954
+ }
955
+ return;
956
+ }
957
+ const counts = at.bindingCounts;
958
+ const bones = at.bindingBones;
959
+ const xs = at.bindingX;
960
+ const ys = at.bindingY;
961
+ const weights = at.bindingWeights;
962
+ let n = 0;
963
+ let o = 0;
964
+ for (let v = 0; v < counts.length; v++) {
965
+ let wx = 0;
966
+ let wy = 0;
967
+ const end = n + counts[v];
968
+ for (; n < end; n++) {
969
+ const b = bones[n] * WORLDS;
970
+ const weight = weights[n];
971
+ const x = deformed ? xs[n] + this.deform[at.deformOffset + n * 2] : xs[n];
972
+ const y = deformed ? ys[n] + this.deform[at.deformOffset + n * 2 + 1] : ys[n];
973
+ wx += (x * W[b + W_A] + y * W[b + W_B] + W[b + W_X]) * weight;
974
+ wy += (x * W[b + W_C] + y * W[b + W_D] + W[b + W_Y]) * weight;
975
+ }
976
+ pts[o++] = wx;
977
+ pts[o++] = wy;
978
+ }
979
+ }
980
+
981
+ private positionsOfPath(c: RuntimePath, position: number, spaceCount: number, wantsTangent: boolean): void {
982
+ this.computePathWorld(c);
983
+ const pts = this.pathWorldPts;
984
+ const vc = c.pathVertices;
985
+ const n = vc / 2;
986
+ const spaces = this.pathSpaces;
987
+ const out = this.pathPositions;
988
+ const outLen = spaceCount * 3 + 2;
989
+ for (let i = 0; i < outLen; i++) out[i] = 0;
990
+ const closed = c.closed;
991
+ if (!c.constantSpeed) {
992
+ const lengths = c.lengths;
993
+ const curveCount = n / 3 - (closed ? 1 : 2);
994
+ const total = lengths[curveCount];
995
+ if (c.positionMode === PATH_POSITION_PERCENT) position *= total;
996
+ const spaceScale = c.spacingMode === PATH_SPACING_PERCENT ? total : c.spacingMode === PATH_SPACING_PROPORTIONAL ? total / spaceCount : 1;
997
+ let curve = 0;
998
+ for (let i = 0, o = 0; i < spaceCount; i++, o += 3) {
999
+ const space = spaces[i] * spaceScale;
1000
+ position += space;
1001
+ let p = position;
1002
+ if (closed) {
1003
+ p %= total;
1004
+ if (p < 0) p += total;
1005
+ curve = 0;
1006
+ } else if (p < 0) {
1007
+ onLineBefore(p, pts[2], pts[3], pts[4], pts[5], out, o);
1008
+ continue;
1009
+ } else if (p > total) {
1010
+ const m0 = 2 * (n - 3);
1011
+ const m1 = 2 * (n - 2);
1012
+ onLinePast(p - total, pts[m0], pts[m0 + 1], pts[m1], pts[m1 + 1], out, o);
1013
+ continue;
1014
+ }
1015
+ for (;; curve++) {
1016
+ const length = lengths[curve];
1017
+ if (p > length) continue;
1018
+ if (curve === 0) p /= length;
1019
+ else {
1020
+ const prev = lengths[curve - 1];
1021
+ p = (p - prev) / (length - prev);
1022
+ }
1023
+ break;
1024
+ }
1025
+ const kk = 3 * curve + 1;
1026
+ const i0 = (kk % n) * 2;
1027
+ const i1 = ((kk + 1) % n) * 2;
1028
+ const i2 = ((kk + 2) % n) * 2;
1029
+ const i3 = ((kk + 3) % n) * 2;
1030
+ onCurve(p, pts[i0], pts[i0 + 1], pts[i1], pts[i1 + 1], pts[i2], pts[i2 + 1], pts[i3], pts[i3 + 1], out, o, wantsTangent || (i > 0 && space === 0));
1031
+ }
1032
+ return;
1033
+ }
1034
+ const chain = this.pathChain;
1035
+ let chainLen = 0;
1036
+ if (closed) {
1037
+ for (let q = 2; q < vc; q++) chain[chainLen++] = pts[q];
1038
+ chain[chainLen++] = pts[0];
1039
+ chain[chainLen++] = pts[1];
1040
+ chain[chainLen++] = pts[2];
1041
+ chain[chainLen++] = pts[3];
1042
+ } else {
1043
+ for (let q = 2; q < vc - 2; q++) chain[chainLen++] = pts[q];
1044
+ }
1045
+ const curveCount = closed ? n / 3 : n / 3 - 1;
1046
+ const curveEnds = this.pathCurveEnds;
1047
+ let total = 0;
1048
+ let cx0 = chain[0];
1049
+ let cy0 = chain[1];
1050
+ for (let i = 0, w = 2; i < curveCount; i++, w += 6) {
1051
+ const cx1 = chain[w];
1052
+ const cy1 = chain[w + 1];
1053
+ const cx2 = chain[w + 2];
1054
+ const cy2 = chain[w + 3];
1055
+ const x2 = chain[w + 4];
1056
+ const y2 = chain[w + 5];
1057
+ const h2x = (cx0 - cx1 * 2 + cx2) * 0.1875;
1058
+ const h2y = (cy0 - cy1 * 2 + cy2) * 0.1875;
1059
+ const d3x = ((cx1 - cx2) * 3 - cx0 + x2) * 0.09375;
1060
+ const d3y = ((cy1 - cy2) * 3 - cy0 + y2) * 0.09375;
1061
+ let d2x = h2x * 2 + d3x;
1062
+ let d2y = h2y * 2 + d3y;
1063
+ let d1x = (cx1 - cx0) * 0.75 + h2x + d3x * 0.16666667;
1064
+ let d1y = (cy1 - cy0) * 0.75 + h2y + d3y * 0.16666667;
1065
+ total += Math.sqrt(d1x * d1x + d1y * d1y);
1066
+ d1x += d2x;
1067
+ d1y += d2y;
1068
+ d2x += d3x;
1069
+ d2y += d3y;
1070
+ total += Math.sqrt(d1x * d1x + d1y * d1y);
1071
+ d1x += d2x;
1072
+ d1y += d2y;
1073
+ total += Math.sqrt(d1x * d1x + d1y * d1y);
1074
+ d1x += d2x + d3x;
1075
+ d1y += d2y + d3y;
1076
+ total += Math.sqrt(d1x * d1x + d1y * d1y);
1077
+ curveEnds[i] = total;
1078
+ cx0 = x2;
1079
+ cy0 = y2;
1080
+ }
1081
+ const pathTotal = curveCount > 0 ? curveEnds[curveCount - 1] : 0;
1082
+ if (c.positionMode === PATH_POSITION_PERCENT) position *= pathTotal;
1083
+ const spaceScale = c.spacingMode === PATH_SPACING_PERCENT ? pathTotal : c.spacingMode === PATH_SPACING_PROPORTIONAL ? pathTotal / spaceCount : 1;
1084
+ const segmentEnds = this.pathSegmentEnds;
1085
+ for (let s = 0; s < 10; s++) segmentEnds[s] = 0;
1086
+ let curveTotal = 0;
1087
+ let lastCurve = -1;
1088
+ let x1 = 0, y1 = 0, cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0;
1089
+ for (let i = 0, o = 0, curve = 0, segment = 0; i < spaceCount; i++, o += 3) {
1090
+ const space = spaces[i] * spaceScale;
1091
+ position += space;
1092
+ let p = position;
1093
+ if (closed) {
1094
+ p %= pathTotal;
1095
+ if (p < 0) p += pathTotal;
1096
+ curve = 0;
1097
+ segment = 0;
1098
+ } else if (p < 0) {
1099
+ onLineBefore(p, chain[0], chain[1], chain[2], chain[3], out, o);
1100
+ continue;
1101
+ } else if (p > pathTotal) {
1102
+ onLinePast(p - pathTotal, chain[chainLen - 4], chain[chainLen - 3], chain[chainLen - 2], chain[chainLen - 1], out, o);
1103
+ continue;
1104
+ }
1105
+ for (;; curve++) {
1106
+ const length = curveEnds[curve];
1107
+ if (p > length) continue;
1108
+ if (curve === 0) p /= length;
1109
+ else {
1110
+ const prev = curveEnds[curve - 1];
1111
+ p = (p - prev) / (length - prev);
1112
+ }
1113
+ break;
1114
+ }
1115
+ if (curve !== lastCurve) {
1116
+ lastCurve = curve;
1117
+ const ii = curve * 6;
1118
+ x1 = chain[ii];
1119
+ y1 = chain[ii + 1];
1120
+ cx1 = chain[ii + 2];
1121
+ cy1 = chain[ii + 3];
1122
+ cx2 = chain[ii + 4];
1123
+ cy2 = chain[ii + 5];
1124
+ x2 = chain[ii + 6];
1125
+ y2 = chain[ii + 7];
1126
+ const h2x = (x1 - cx1 * 2 + cx2) * 0.03;
1127
+ const h2y = (y1 - cy1 * 2 + cy2) * 0.03;
1128
+ const d3x = ((cx1 - cx2) * 3 - x1 + x2) * 0.006;
1129
+ const d3y = ((cy1 - cy2) * 3 - y1 + y2) * 0.006;
1130
+ let d2x = h2x * 2 + d3x;
1131
+ let d2y = h2y * 2 + d3y;
1132
+ let d1x = (cx1 - x1) * 0.3 + h2x + d3x * 0.16666667;
1133
+ let d1y = (cy1 - y1) * 0.3 + h2y + d3y * 0.16666667;
1134
+ curveTotal = Math.sqrt(d1x * d1x + d1y * d1y);
1135
+ segmentEnds[0] = curveTotal;
1136
+ for (let s = 1; s < 8; s++) {
1137
+ d1x += d2x;
1138
+ d1y += d2y;
1139
+ d2x += d3x;
1140
+ d2y += d3y;
1141
+ curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
1142
+ segmentEnds[s] = curveTotal;
1143
+ }
1144
+ d1x += d2x;
1145
+ d1y += d2y;
1146
+ curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
1147
+ segmentEnds[8] = curveTotal;
1148
+ d1x += d2x + d3x;
1149
+ d1y += d2y + d3y;
1150
+ curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
1151
+ segmentEnds[9] = curveTotal;
1152
+ segment = 0;
1153
+ }
1154
+ p *= curveTotal;
1155
+ for (;; segment++) {
1156
+ const length = segmentEnds[segment];
1157
+ if (p > length) continue;
1158
+ if (segment === 0) p /= length;
1159
+ else {
1160
+ const prev = segmentEnds[segment - 1];
1161
+ p = segment + (p - prev) / (length - prev);
1162
+ }
1163
+ break;
1164
+ }
1165
+ onCurve(p * 0.1, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, wantsTangent || (i > 0 && space === 0));
1166
+ }
1167
+ }
1168
+
1169
+ private sliderAttachments(): void {
1170
+ for (let k = 0; k < this.model.constraints.length; k++) {
1171
+ this.sliderSlots(k);
1172
+ this.sliderDeforms(k);
1173
+ }
1174
+ }
1175
+
1176
+ private sliderSlots(index: number): void {
1177
+ const cs = this.model.constraints;
1178
+ {
1179
+ const k = index, c = cs[k];
1180
+ if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
1181
+ const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
1182
+ for (let q = 0; q < a.slots.length; q++) {
1183
+ const tl = a.slots[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
1184
+ if (i < 0) continue;
1185
+ if (tl.attachments !== null) {
1186
+ const next = tl.attachments[i];
1187
+ if (next !== this.shown[tl.slot] && next >= 0) this.deformed[next] = 0;
1188
+ this.shown[tl.slot] = next;
1189
+ }
1190
+ else {
1191
+ const channels = tl.channels!;
1192
+ for (let f = 0; f < channels.length; f++) {
1193
+ const n = tl.slot * 4 + channels[f], v = channelValue(tl.curve, i, f, t);
1194
+ this.light[n] = clamp01(alpha === 1 ? v : this.light[n] + (v - this.light[n]) * alpha);
1195
+ }
1196
+ }
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ private sliderDeforms(index: number): void {
1202
+ if (this.model.omitDeform) return;
1203
+ const cs = this.model.constraints;
1204
+ {
1205
+ const k = index, c = cs[k];
1206
+ if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
1207
+ const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
1208
+ for (let q = 0; q < a.deforms.length; q++) {
1209
+ const tl = a.deforms[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
1210
+ if (i < 0) continue;
1211
+ const held = i + 1 === tl.curve.keys || tl.curve.kinds[i] === CURVE_STEPPED;
1212
+ const p = held ? 0 : deformFraction(tl.curve, i, t), start = i * tl.count;
1213
+ for (let slot = 0; slot < this.shown.length; slot++) {
1214
+ const index = this.shown[slot];
1215
+ if (index < 0 || !this.model.slots[slot].live) continue;
1216
+ const at = this.model.attachments[index];
1217
+ if (at.timeline !== tl.identity || at.deformCount === 0) continue;
1218
+ const current = this.deformed[index] !== 0;
1219
+ for (let f = 0; f < tl.count; f++) {
1220
+ const v = tl.held[start + f], target = held ? v : v + (tl.held[start + tl.count + f] - v) * p;
1221
+ const setup = at.draw === DRAW_MESH ? at.local[f] : 0, n = at.deformOffset + f;
1222
+ const from = current ? this.deform[n] : setup;
1223
+ this.deform[n] = alpha === 1 && (!c.setup.additive || !current) ? target : c.setup.additive ? from + (target - setup) * alpha : from + (target - from) * alpha;
1224
+ }
1225
+ this.deformed[index] = 1;
1226
+ }
1227
+ }
445
1228
  }
446
1229
  }
447
1230
 
@@ -868,6 +1651,20 @@ export class RuntimePose {
868
1651
  const wb = W[w + W_B];
869
1652
  const wc = W[w + W_C];
870
1653
  const wd = W[w + W_D];
1654
+ if (mode === MODE_NO_SCALE || mode === MODE_NO_SCALE_OR_REFLECTION) {
1655
+ const det = pa * pd - pb * pc;
1656
+ const flip = mode === MODE_NO_SCALE && det < 0 ? -1 : 1;
1657
+ const sign = det < 0 ? -1 : 1;
1658
+ const rotation = Math.atan2((pa * wc - pc * wa) * sign, (pd * wa - pb * wc) * sign) * DEG;
1659
+ const rr = rotation * RAD;
1660
+ const cos = Math.cos(rr), sin = Math.sin(rr);
1661
+ let ux = pa * cos + pb * sin, uy = pc * cos + pd * sin;
1662
+ const length = 1 / Math.sqrt(ux * ux + uy * uy);
1663
+ ux *= length; uy *= length;
1664
+ this.decompose(l, ux * wa + uy * wc, (ux * wc - uy * wa) * flip, ux * wb + uy * wd, (ux * wd - uy * wb) * flip, true);
1665
+ L[l + L_ROTATION] = rotation;
1666
+ return;
1667
+ }
871
1668
  if (mode === MODE_ONLY_TRANSLATION) {
872
1669
  this.decompose(l, wa, wc, wb, wd);
873
1670
  return;
@@ -887,7 +1684,7 @@ export class RuntimePose {
887
1684
  }
888
1685
 
889
1686
  /** 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 {
1687
+ private decompose(l: number, la: number, lc: number, lb: number, ld: number, shearX = false): void {
891
1688
  const L = this.locals;
892
1689
  const sx = Math.sqrt(la * la + lc * lc);
893
1690
  const yLength = Math.sqrt(lb * lb + ld * ld);
@@ -901,11 +1698,11 @@ export class RuntimePose {
901
1698
  const xAngle = Math.atan2(lc, la) * DEG;
902
1699
  const det = la * ld - lb * lc;
903
1700
  const yAngle = Math.atan2(ld, lb) * DEG;
904
- L[l + L_ROTATION] = xAngle;
1701
+ L[l + (shearX ? L_SHEAR_X : L_ROTATION)] = xAngle;
905
1702
  L[l + L_SCALE_X] = sx;
906
1703
  L[l + L_SCALE_Y] = det < 0 ? -yLength : yLength;
907
1704
  const right = det < 0 ? -90 : 90;
908
- let shear = yAngle - (xAngle + right);
1705
+ let shear = yAngle - ((shearX ? 0 : xAngle) + right);
909
1706
  shear = shear > 180 ? shear - 360 : shear < -180 ? shear + 360 : shear;
910
1707
  L[l + L_SHEAR_Y] = yLength > 0.00001 ? shear : 0;
911
1708
  }
@@ -1093,3 +1890,43 @@ function applyWorld(w: Float64Array, property: number, v: number, mix: number):
1093
1890
  }
1094
1891
  }
1095
1892
  }
1893
+
1894
+ function onLineBefore(p: number, x1: number, y1: number, x2: number, y2: number, out: Float64Array, o: number): void {
1895
+ const r = Math.atan2(y2 - y1, x2 - x1);
1896
+ out[o] = x1 + p * Math.cos(r);
1897
+ out[o + 1] = y1 + p * Math.sin(r);
1898
+ out[o + 2] = r;
1899
+ }
1900
+
1901
+ function onLinePast(p: number, x0: number, y0: number, x1: number, y1: number, out: Float64Array, o: number): void {
1902
+ const r = Math.atan2(y1 - y0, x1 - x0);
1903
+ out[o] = x1 + p * Math.cos(r);
1904
+ out[o + 1] = y1 + p * Math.sin(r);
1905
+ out[o + 2] = r;
1906
+ }
1907
+
1908
+ function onCurve(p: number, x1: number, y1: number, cx1: number, cy1: number, cx2: number, cy2: number, x2: number, y2: number, out: Float64Array, o: number, wantsTangent: boolean): void {
1909
+ if (Number.isNaN(p)) {
1910
+ out[o] = x1;
1911
+ out[o + 1] = y1;
1912
+ out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1);
1913
+ return;
1914
+ }
1915
+ const t2 = p * p;
1916
+ const t3 = t2 * p;
1917
+ const u = 1 - p;
1918
+ const u2 = u * u;
1919
+ const u3 = u2 * u;
1920
+ const ut = u * p;
1921
+ const ut3x = ut * 3;
1922
+ const w1 = u * ut3x;
1923
+ const w2 = ut3x * p;
1924
+ const x = x1 * u3 + cx1 * w1 + cx2 * w2 + x2 * t3;
1925
+ const y = y1 * u3 + cy1 * w1 + cy2 * w2 + y2 * t3;
1926
+ out[o] = x;
1927
+ out[o + 1] = y;
1928
+ if (wantsTangent) {
1929
+ if (p < 0.001) out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1);
1930
+ else out[o + 2] = Math.atan2(y - (y1 * u2 + cy1 * ut * 2 + cy2 * t2), x - (x1 * u2 + cx1 * ut * 2 + cx2 * t2));
1931
+ }
1932
+ }