@rig-c/core 2.76.0 → 2.78.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 3): one
2
+ * The runtime poser's per-instance half (issue #1591, stage 5): 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,22 +9,24 @@
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
13
- * 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 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.
12
+ * transforms parents first, then the draw order and each slot's light and dark
13
+ * colour, attachment and own deform timelines, then the ik, transform, path,
14
+ * physics and slider constraints in the document's order with their timelines'
15
+ * values at `t`, each followed by the bones below what it moved posed again,
16
+ * then the sliders' draw-order, slot and deform timelines; then, when asked,
17
+ * every shown attachment's world vertices. Nothing of one unstepped pose
18
+ * survives but key cursors. A stepped pose also keeps its physics clock,
19
+ * offsets, velocities and lags; sliders select values within this pose rather
20
+ * than advancing an independent track.
19
21
  *
20
22
  * ## The arithmetic is the oracle's
21
23
  *
22
24
  * Every formula below is the one the named function of `src/core/` computes,
23
25
  * in its order of operations, on the same doubles: a bone's frame and its
24
26
  * five inherit modes (`world.ts`), the timelines (`animation.ts`), the one-
25
- * and two-bone ik, the world-space transform, the read-back of a moved bone's
26
- * local values and the update order (`constraints.ts`), a region's corners
27
- * and a mesh's vertices (`vertices.ts`). Each of those orders was fixed
27
+ * and two-bone ik, the world-space transform, the path constraint (`constraints_path.ts`),
28
+ * the read-back of a moved bone's local values and the update order (`constraints.ts`),
29
+ * a region's corners and a mesh's vertices (`vertices.ts`). Each of those orders was fixed
28
30
  * there by measurement against spine-core 4.3.13 to the last bit; the runtime
29
31
  * does not re-measure them, it is held to the oracle at tolerance 0
30
32
  * (`tools/core_gate.ts`, *The runtime poser*), so an order changed here is a
@@ -32,7 +34,7 @@
32
34
  *
33
35
  * ⚠️ The exactness hazards, each kept as the oracle has it: the setup value
34
36
  * plus 0 before a timeline's first key (which turns a `-0` setup into `0`);
35
- * a keyed ik or transform value reaching the pose through the setup blend
37
+ * a keyed ik, transform or path value reaching the pose through the setup blend
36
38
  * `setup + (value − setup)·1`; `Math.PI` for a folded uniform two-bone ik and
37
39
  * the runtime's 3.1415927 everywhere else; the `(−180, 180]` wrap; a mix of
38
40
  * exactly 0 touching nothing; an inactive bone posed as zeros by the
@@ -62,6 +64,13 @@ import {
62
64
  MODE_NO_SCALE_OR_REFLECTION,
63
65
  MODE_NORMAL,
64
66
  MODE_ONLY_TRANSLATION,
67
+ PATH_POSITION_PERCENT,
68
+ PATH_ROTATE_CHAIN,
69
+ PATH_ROTATE_CHAIN_SCALE,
70
+ PATH_ROTATE_TANGENT,
71
+ PATH_SPACING_LENGTH,
72
+ PATH_SPACING_PERCENT,
73
+ PATH_SPACING_PROPORTIONAL,
65
74
  RAD,
66
75
  RuntimeInputError,
67
76
  SCALE_Y_UNIFORM,
@@ -74,9 +83,11 @@ import {
74
83
  W_Y,
75
84
  WORLDS,
76
85
  type RuntimeAnimation,
86
+ type RuntimeConstraint,
77
87
  type RuntimeEvent,
78
88
  type RuntimeIk,
79
89
  type RuntimeModel,
90
+ type RuntimePath,
80
91
  type RuntimeTransform,
81
92
  type RuntimeSlider,
82
93
  type RuntimePhysicsTimeline,
@@ -84,6 +95,7 @@ import {
84
95
 
85
96
  const DEG = RUNTIME_DEG;
86
97
  const TWO_PI = 2 * RUNTIME_PI;
98
+ const PATH_EPSILON = 0.00001;
87
99
  // The transform properties, by `TRANSFORM_PROPERTIES` index (src/core/constraints.ts).
88
100
  const P_ROTATE = 0;
89
101
  const P_X = 1;
@@ -107,7 +119,7 @@ const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
107
119
  export interface RuntimePoseOptions { readonly clock?: (dt: number) => number }
108
120
  export type RuntimePhysicsPhase = 'none' | 'reset' | 'update';
109
121
 
110
- /** One skeleton's pose, every value in place. Read `world`, `light`, `shown` and `vertices` after a pose. */
122
+ /** One skeleton's pose, every value in place. Read `world`, `light`, `dark`, `shown`, `drawOrder` and `vertices` after a pose. */
111
123
  export class RuntimePose {
112
124
  readonly model: RuntimeModel;
113
125
  /** Every bone's local values, `LOCALS` per bone (`L_*`). */
@@ -116,8 +128,12 @@ export class RuntimePose {
116
128
  readonly world: Float64Array;
117
129
  /** Every slot's light colour, four per slot. */
118
130
  readonly light: Float64Array;
131
+ /** Every slot's dark colour, three per slot (`r2`, `g2`, `b2`), or zeros when `model.slots[s].dark === null`. */
132
+ readonly dark: Float64Array;
119
133
  /** What every slot shows: an index into `model.attachments`, or −1 for nothing. */
120
134
  readonly shown: Int32Array;
135
+ /** The slot indices in the live frame's draw order (`0 .. model.slots.length - 1`), updated in place. */
136
+ readonly drawOrder: Int32Array;
121
137
  /** Every attachment's world vertices at its `offset`, written by `computeVertices` for the attachments shown. */
122
138
  readonly vertices: Float64Array;
123
139
  /** Held/interpolated deform values, one pair per binding when weighted. */
@@ -134,11 +150,20 @@ export class RuntimePose {
134
150
 
135
151
  private current: RuntimeAnimation | null = null;
136
152
  private readonly cursors: Int32Array;
137
- // The constraints' values as posed: an ik's mix, softness and flags, a transform's six mixes.
153
+ // 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.
138
154
  private readonly ikMix: Float64Array;
139
155
  private readonly ikSoftness: Float64Array;
140
156
  private readonly ikFlags: Uint8Array;
141
157
  private readonly mixes: Float64Array;
158
+ private readonly pathValues: Float64Array;
159
+ private readonly pathSlotSign: Int8Array;
160
+ private readonly pathSpaces: Float64Array;
161
+ private readonly pathLengths: Float64Array;
162
+ private readonly pathPositions: Float64Array;
163
+ private readonly pathWorldPts: Float64Array;
164
+ private readonly pathChain: Float64Array;
165
+ private readonly pathCurveEnds: Float64Array;
166
+ private readonly pathSegmentEnds = new Float64Array(10);
142
167
  // 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.
143
168
  private readonly changed: Int32Array;
144
169
  private readonly inWorld: Int32Array;
@@ -156,6 +181,7 @@ export class RuntimePose {
156
181
  private readonly hasPhysics: boolean;
157
182
  private readonly physicsStates: PhysicsState[];
158
183
  private readonly physicsRecords: Array<CorePhysicsRecord | null>;
184
+ private readonly physicsScope: Uint8Array;
159
185
  private readonly environment = new Float64Array([1, 0, 0, 1]);
160
186
  private readonly sliderTime: Float64Array;
161
187
  private readonly sliderMix: Float64Array;
@@ -174,10 +200,14 @@ export class RuntimePose {
174
200
  const nb = model.boneNames.length;
175
201
  const ns = model.slots.length;
176
202
  const nc = model.constraints.length;
203
+ this.physicsScope = new Uint8Array(nc);
177
204
  this.locals = new Float64Array(nb * LOCALS);
178
205
  this.world = new Float64Array(nb * WORLDS);
179
206
  this.light = new Float64Array(ns * 4);
207
+ this.dark = new Float64Array(ns * 3);
180
208
  this.shown = new Int32Array(ns);
209
+ this.drawOrder = new Int32Array(ns);
210
+ for (let s = 0; s < ns; s++) this.drawOrder[s] = s;
181
211
  this.vertices = new Float64Array(model.vertexCount);
182
212
  this.deform = new Float64Array(model.deformCount);
183
213
  this.deformed = new Uint8Array(model.attachments.length);
@@ -186,6 +216,16 @@ export class RuntimePose {
186
216
  this.ikSoftness = new Float64Array(nc);
187
217
  this.ikFlags = new Uint8Array(nc);
188
218
  this.mixes = new Float64Array(nc * 6);
219
+ this.pathValues = new Float64Array(nc * 5);
220
+ this.pathSlotSign = new Int8Array(nc);
221
+ const ms = model.maxPathSpaces;
222
+ const mv = model.maxPathVertices;
223
+ this.pathSpaces = new Float64Array(ms);
224
+ this.pathLengths = new Float64Array(ms);
225
+ this.pathPositions = new Float64Array(ms > 0 ? ms * 3 + 2 : 0);
226
+ this.pathWorldPts = new Float64Array(mv);
227
+ this.pathChain = new Float64Array(mv > 0 ? mv + 4 : 0);
228
+ this.pathCurveEnds = new Float64Array(mv > 0 ? Math.ceil(mv / 6) + 1 : 0);
189
229
  // Sized by the longest target list (`RuntimeModel.scratch`): a transform may name one bone several times, and every entry is applied and reposed.
190
230
  this.changed = new Int32Array(model.scratch);
191
231
  this.inWorld = new Int32Array(model.scratch);
@@ -233,9 +273,11 @@ export class RuntimePose {
233
273
  this.physicsValues(null, 0, false);
234
274
  this.locals.set(this.model.setup);
235
275
  this.hierarchy();
276
+ this.slotValues(null, 0);
277
+ this.deformValues(null, 0);
236
278
  this.constraintValues(null, 0);
237
279
  this.solveConstraints();
238
- this.attachmentValues(null, 0);
280
+ this.sliderAttachments();
239
281
  }
240
282
 
241
283
  /** The animation posed at its time `t`, from the setup pose (the header). */
@@ -255,9 +297,11 @@ export class RuntimePose {
255
297
  this.time = t;
256
298
  this.boneTimelines(anim, t);
257
299
  this.hierarchy();
300
+ this.slotValues(anim, t);
301
+ this.deformValues(anim, t);
258
302
  this.constraintValues(anim, t);
259
303
  this.solveConstraints();
260
- this.attachmentValues(anim, t);
304
+ this.sliderAttachments();
261
305
  }
262
306
 
263
307
  /** 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). */
@@ -496,6 +540,37 @@ export class RuntimePose {
496
540
  }
497
541
  this.sliderTime[k] = time;
498
542
  this.sliderMix[k] = mix;
543
+ } else if (c.kind === 'path') {
544
+ const keyed = anim === null ? null : anim.paths[k];
545
+ let position = c.position;
546
+ let spacing = c.spacing;
547
+ let mixRotate = c.mixRotate;
548
+ let mixX = c.mixX;
549
+ let mixY = c.mixY;
550
+ if (keyed !== null) {
551
+ if (keyed.position !== null) {
552
+ const q = keyAt(keyed.position, this.cursors, keyed.positionCursor, t);
553
+ if (q >= 0) position = c.position + (channelValue(keyed.position, q, 0, t) - c.position) * 1;
554
+ }
555
+ if (keyed.spacing !== null) {
556
+ const q = keyAt(keyed.spacing, this.cursors, keyed.spacingCursor, t);
557
+ if (q >= 0) spacing = c.spacing + (channelValue(keyed.spacing, q, 0, t) - c.spacing) * 1;
558
+ }
559
+ if (keyed.mix !== null) {
560
+ const q = keyAt(keyed.mix, this.cursors, keyed.mixCursor, t);
561
+ if (q >= 0) {
562
+ mixRotate = c.mixRotate + (channelValue(keyed.mix, q, 0, t) - c.mixRotate) * 1;
563
+ mixX = c.mixX + (channelValue(keyed.mix, q, 1, t) - c.mixX) * 1;
564
+ mixY = c.mixY + (channelValue(keyed.mix, q, 2, t) - c.mixY) * 1;
565
+ }
566
+ }
567
+ }
568
+ const o = k * 5;
569
+ this.pathValues[o] = position;
570
+ this.pathValues[o + 1] = spacing;
571
+ this.pathValues[o + 2] = mixRotate;
572
+ this.pathValues[o + 3] = mixX;
573
+ this.pathValues[o + 4] = mixY;
499
574
  }
500
575
  }
501
576
  }
@@ -503,12 +578,15 @@ export class RuntimePose {
503
578
  /** Every applied constraint in the document's order, each followed by the update order's repose (`applyConstraints`). */
504
579
  private solveConstraints(): void {
505
580
  const cs = this.model.constraints;
581
+ const hasPath = this.model.hasPath;
506
582
  for (let k = 0; k < cs.length; k++) {
583
+ if (hasPath) this.snapSlotBones(k, false);
507
584
  const c = cs[k];
508
585
  if (!c.applied) continue;
509
586
  if (c.kind === 'ik') this.solveIk(c, k);
510
587
  else if (c.kind === 'transform') this.solveTransform(c, k);
511
588
  else if (c.kind === 'slider') this.solveSlider(c, k);
589
+ else if (c.kind === 'path') this.solvePath(c, k);
512
590
  else if (this.physicsPhase !== 'none' && !this.model.omitPhysics) {
513
591
  const b = c.bones[0];
514
592
  if (stepRuntimePhysics(this.physicsRecords[k]!, this.physicsStates[k], this.world, b * WORLDS, this.model.lengths[b], this.physicsTime, this.physicsPhase === 'reset', this.environment)) {
@@ -516,6 +594,18 @@ export class RuntimePose {
516
594
  this.repose(1, 1);
517
595
  }
518
596
  }
597
+ if (hasPath) this.snapSlotBones(k, true);
598
+ }
599
+ }
600
+
601
+ private snapSlotBones(at: number, after: boolean): void {
602
+ const cs = this.model.constraints;
603
+ const W = this.world;
604
+ for (let k = 0; k < cs.length; k++) {
605
+ const c = cs[k];
606
+ if (c.kind !== 'path' || c.slotEventAt !== at || c.slotEventAfter !== after || (k === at && !after)) continue;
607
+ const sb = c.slotBone * WORLDS;
608
+ this.pathSlotSign[k] = W[sb + W_A] * W[sb + W_D] - W[sb + W_B] * W[sb + W_C] > 0 ? 1 : -1;
519
609
  }
520
610
  }
521
611
 
@@ -533,19 +623,54 @@ export class RuntimePose {
533
623
  this.physicsReady = true;
534
624
  }
535
625
 
626
+ private checkPhysicsScope(scope: readonly string[] | undefined): Uint8Array | null {
627
+ if (scope === undefined) return null;
628
+ if (!Array.isArray(scope)) throw new RuntimeInputError(`physics movement scope is ${String(scope)}, not an array of physics constraint names`);
629
+ const selected = this.physicsScope;
630
+ selected.fill(0);
631
+ const cs = this.model.constraints;
632
+ for (let i = 0; i < scope.length; i++) {
633
+ const name = scope[i];
634
+ if (typeof name !== 'string') throw new RuntimeInputError(`physics movement scope name ${String(name)} is not a string`);
635
+ let found: RuntimeConstraint | undefined;
636
+ for (let k = 0; k < cs.length; k++) {
637
+ if (cs[k].kind === 'physics' && cs[k].name === name) {
638
+ found = cs[k];
639
+ break;
640
+ }
641
+ }
642
+ if (found === undefined) {
643
+ for (let k = 0; k < cs.length; k++) {
644
+ if (cs[k].name === name) {
645
+ found = cs[k];
646
+ break;
647
+ }
648
+ }
649
+ }
650
+ if (found === undefined) throw new RuntimeInputError(`physics movement scope: no constraint "${name}"; this document declares [${cs.map((c) => c.name).join(', ')}]`);
651
+ if (found.kind !== 'physics') throw new RuntimeInputError(`physics movement scope: constraint "${name}" is ${found.kind}, not physics`);
652
+ selected[found.index] = 1;
653
+ }
654
+ return this.model.ignorePhysicsScope ? null : selected;
655
+ }
656
+
536
657
  /** External motion changes the previous origin, never the authored locals. */
537
- physicsTranslate(x: number, y: number): void {
658
+ physicsTranslate(x: number, y: number, scope?: readonly string[]): void {
659
+ const selected = this.checkPhysicsScope(scope);
538
660
  if (!Number.isFinite(x) || !Number.isFinite(y)) throw new RuntimeInputError('physicsTranslate: x and y must be finite');
539
661
  for (let k = 0; k < this.physicsStates.length; k++) {
662
+ if (selected !== null && selected[k] === 0) continue;
540
663
  const s = this.physicsStates[k];
541
664
  s.ux -= x; s.uy -= y; s.cx -= x; s.cy -= y;
542
665
  }
543
666
  }
544
667
 
545
- physicsRotate(x: number, y: number, degrees: number): void {
668
+ physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void {
669
+ const selected = this.checkPhysicsScope(scope);
546
670
  if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(degrees)) throw new RuntimeInputError('physicsRotate: x, y and degrees must be finite');
547
671
  const r = degrees * RAD, cos = Math.cos(r), sin = Math.sin(r);
548
672
  for (let k = 0; k < this.physicsStates.length; k++) {
673
+ if (selected !== null && selected[k] === 0) continue;
549
674
  const s = this.physicsStates[k];
550
675
  const dx = s.cx - x, dy = s.cy - y;
551
676
  const tx = dx * cos - dy * sin - dx, ty = dx * sin + dy * cos - dy;
@@ -662,9 +787,394 @@ export class RuntimePose {
662
787
  if (c.bones.length > 0) this.repose(c.bones.length, 0);
663
788
  }
664
789
 
665
- private attachmentValues(anim: RuntimeAnimation | null, t: number): void {
666
- this.slotValues(anim, t);
667
- this.deformValues(anim, t);
790
+ /** A path constraint (`solvePath` in `src/core/constraints_path.ts`), every buffer overwritten in place. */
791
+ private solvePath(c: RuntimePath, k: number): void {
792
+ if (c.attachment < 0 || this.model.omitPath) return;
793
+ const o5 = k * 5;
794
+ const position = this.pathValues[o5];
795
+ const spacing = this.pathValues[o5 + 1];
796
+ const mixRotate = this.pathValues[o5 + 2];
797
+ const mixX = this.pathValues[o5 + 3];
798
+ const mixY = this.pathValues[o5 + 4];
799
+ if (mixRotate === 0 && mixX === 0 && mixY === 0) return;
800
+ const wantsTangent = c.rotateMode === PATH_ROTATE_TANGENT;
801
+ const scale = c.rotateMode === PATH_ROTATE_CHAIN_SCALE;
802
+ const bones = c.bones;
803
+ const boneCount = bones.length;
804
+ const spaceCount = wantsTangent ? boneCount : boneCount + 1;
805
+ const spaces = this.pathSpaces;
806
+ const lengths = this.pathLengths;
807
+ const W = this.world;
808
+ const modelLengths = this.model.lengths;
809
+ spaces[0] = 0;
810
+ if (c.spacingMode === PATH_SPACING_PERCENT) {
811
+ if (scale) {
812
+ for (let i = 0; i < spaceCount - 1; i++) {
813
+ const bi = bones[i];
814
+ const boneLength = modelLengths[bi];
815
+ const w = bi * WORLDS;
816
+ const x = boneLength * W[w + W_A];
817
+ const y = boneLength * W[w + W_C];
818
+ lengths[i] = Math.sqrt(x * x + y * y);
819
+ }
820
+ }
821
+ for (let i = 1; i < spaceCount; i++) spaces[i] = spacing;
822
+ } else if (c.spacingMode === PATH_SPACING_PROPORTIONAL) {
823
+ let sum = 0;
824
+ for (let i = 0, n = spaceCount - 1; i < n; ) {
825
+ const bi = bones[i];
826
+ const boneLength = modelLengths[bi];
827
+ if (boneLength < PATH_EPSILON) {
828
+ if (scale) lengths[i] = 0;
829
+ spaces[++i] = spacing;
830
+ } else {
831
+ const w = bi * WORLDS;
832
+ const x = boneLength * W[w + W_A];
833
+ const y = boneLength * W[w + W_C];
834
+ const length = Math.sqrt(x * x + y * y);
835
+ if (scale) lengths[i] = length;
836
+ spaces[++i] = length;
837
+ sum += length;
838
+ }
839
+ }
840
+ if (sum > 0) {
841
+ sum = (spaceCount / sum) * spacing;
842
+ for (let i = 1; i < spaceCount; i++) spaces[i] *= sum;
843
+ }
844
+ } else {
845
+ const addsLength = c.spacingMode === PATH_SPACING_LENGTH;
846
+ for (let i = 0, n = spaceCount - 1; i < n; ) {
847
+ const bi = bones[i];
848
+ const boneLength = modelLengths[bi];
849
+ if (boneLength < PATH_EPSILON) {
850
+ if (scale) lengths[i] = 0;
851
+ spaces[++i] = spacing;
852
+ } else {
853
+ const w = bi * WORLDS;
854
+ const x = boneLength * W[w + W_A];
855
+ const y = boneLength * W[w + W_C];
856
+ const length = Math.sqrt(x * x + y * y);
857
+ if (scale) lengths[i] = length;
858
+ spaces[++i] = ((addsLength ? Math.max(0, boneLength + spacing) : spacing) * length) / boneLength;
859
+ }
860
+ }
861
+ }
862
+ this.positionsOfPath(c, position, spaceCount, wantsTangent);
863
+ const positions = this.pathPositions;
864
+ let atX = positions[0];
865
+ let atY = positions[1];
866
+ let offsetRotation = c.offsetRotation;
867
+ let fromTip: boolean;
868
+ if (offsetRotation === 0) fromTip = c.rotateMode === PATH_ROTATE_CHAIN;
869
+ else {
870
+ fromTip = false;
871
+ let positive: boolean;
872
+ if (c.slotEventAt === k && !c.slotEventAfter) {
873
+ const sb = c.slotBone * WORLDS;
874
+ positive = W[sb + W_A] * W[sb + W_D] - W[sb + W_B] * W[sb + W_C] > 0;
875
+ } else {
876
+ positive = c.slotEventAt >= 0 && this.pathSlotSign[k] > 0;
877
+ }
878
+ offsetRotation *= positive ? RAD : -RAD;
879
+ }
880
+ for (let i = 0, p = 3; i < boneCount; i++, p += 3) {
881
+ const bi = bones[i];
882
+ const o = bi * WORLDS;
883
+ let wa = W[o + W_A];
884
+ let wb = W[o + W_B];
885
+ let wc = W[o + W_C];
886
+ let wd = W[o + W_D];
887
+ const wx = W[o + W_X] + (atX - W[o + W_X]) * mixX;
888
+ const wy = W[o + W_Y] + (atY - W[o + W_Y]) * mixY;
889
+ const x = positions[p];
890
+ const y = positions[p + 1];
891
+ const dx = x - atX;
892
+ const dy = y - atY;
893
+ if (scale) {
894
+ const length = lengths[i];
895
+ if (length >= PATH_EPSILON) {
896
+ const s = (Math.sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
897
+ wa *= s;
898
+ wc *= s;
899
+ }
900
+ }
901
+ atX = x;
902
+ atY = y;
903
+ if (mixRotate > 0) {
904
+ const a = wa;
905
+ const bb = wb;
906
+ const cc = wc;
907
+ const d = wd;
908
+ let r: number;
909
+ if (wantsTangent) r = positions[p - 1];
910
+ else if (spaces[i + 1] === 0) r = positions[p + 2];
911
+ else r = Math.atan2(dy, dx);
912
+ r -= Math.atan2(cc, a);
913
+ if (fromTip) {
914
+ const cos = Math.cos(r);
915
+ const sin = Math.sin(r);
916
+ const length = modelLengths[bi];
917
+ atX += (length * (cos * a - sin * cc) - dx) * mixRotate;
918
+ atY += (length * (sin * a + cos * cc) - dy) * mixRotate;
919
+ } else r += offsetRotation;
920
+ if (r > RUNTIME_PI) r -= TWO_PI;
921
+ else if (r < -RUNTIME_PI) r += TWO_PI;
922
+ r *= mixRotate;
923
+ const cos = Math.cos(r);
924
+ const sin = Math.sin(r);
925
+ wa = cos * a - sin * cc;
926
+ wb = cos * bb - sin * d;
927
+ wc = sin * a + cos * cc;
928
+ wd = sin * bb + cos * d;
929
+ }
930
+ W[o + W_A] = wa;
931
+ W[o + W_B] = wb;
932
+ W[o + W_C] = wc;
933
+ W[o + W_D] = wd;
934
+ W[o + W_X] = wx;
935
+ W[o + W_Y] = wy;
936
+ this.changed[i] = bi;
937
+ this.inWorld[i] = bi;
938
+ }
939
+ if (boneCount > 0) this.repose(boneCount, boneCount);
940
+ }
941
+
942
+ private computePathWorld(c: RuntimePath): void {
943
+ const at = this.model.attachments[c.attachment];
944
+ const W = this.world;
945
+ const pts = this.pathWorldPts;
946
+ const deformed = this.deformed[c.attachment] !== 0;
947
+ if (at.bindingCounts.length === 0) {
948
+ const b = c.slotBone * WORLDS;
949
+ const a = W[b + W_A];
950
+ const bb = W[b + W_B];
951
+ const cc = W[b + W_C];
952
+ const d = W[b + W_D];
953
+ const wx = W[b + W_X];
954
+ const wy = W[b + W_Y];
955
+ const local = deformed ? this.deform : at.local;
956
+ const lo = deformed ? at.deformOffset : 0;
957
+ for (let q = 0; q < c.pathVertices; q += 2) {
958
+ const x = local[lo + q];
959
+ const y = local[lo + q + 1];
960
+ pts[q] = x * a + y * bb + wx;
961
+ pts[q + 1] = x * cc + y * d + wy;
962
+ }
963
+ return;
964
+ }
965
+ const counts = at.bindingCounts;
966
+ const bones = at.bindingBones;
967
+ const xs = at.bindingX;
968
+ const ys = at.bindingY;
969
+ const weights = at.bindingWeights;
970
+ let n = 0;
971
+ let o = 0;
972
+ for (let v = 0; v < counts.length; v++) {
973
+ let wx = 0;
974
+ let wy = 0;
975
+ const end = n + counts[v];
976
+ for (; n < end; n++) {
977
+ const b = bones[n] * WORLDS;
978
+ const weight = weights[n];
979
+ const x = deformed ? xs[n] + this.deform[at.deformOffset + n * 2] : xs[n];
980
+ const y = deformed ? ys[n] + this.deform[at.deformOffset + n * 2 + 1] : ys[n];
981
+ wx += (x * W[b + W_A] + y * W[b + W_B] + W[b + W_X]) * weight;
982
+ wy += (x * W[b + W_C] + y * W[b + W_D] + W[b + W_Y]) * weight;
983
+ }
984
+ pts[o++] = wx;
985
+ pts[o++] = wy;
986
+ }
987
+ }
988
+
989
+ private positionsOfPath(c: RuntimePath, position: number, spaceCount: number, wantsTangent: boolean): void {
990
+ this.computePathWorld(c);
991
+ const pts = this.pathWorldPts;
992
+ const vc = c.pathVertices;
993
+ const n = vc / 2;
994
+ const spaces = this.pathSpaces;
995
+ const out = this.pathPositions;
996
+ const outLen = spaceCount * 3 + 2;
997
+ for (let i = 0; i < outLen; i++) out[i] = 0;
998
+ const closed = c.closed;
999
+ if (!c.constantSpeed) {
1000
+ const lengths = c.lengths;
1001
+ const curveCount = n / 3 - (closed ? 1 : 2);
1002
+ const total = lengths[curveCount];
1003
+ if (c.positionMode === PATH_POSITION_PERCENT) position *= total;
1004
+ const spaceScale = c.spacingMode === PATH_SPACING_PERCENT ? total : c.spacingMode === PATH_SPACING_PROPORTIONAL ? total / spaceCount : 1;
1005
+ let curve = 0;
1006
+ for (let i = 0, o = 0; i < spaceCount; i++, o += 3) {
1007
+ const space = spaces[i] * spaceScale;
1008
+ position += space;
1009
+ let p = position;
1010
+ if (closed) {
1011
+ p %= total;
1012
+ if (p < 0) p += total;
1013
+ curve = 0;
1014
+ } else if (p < 0) {
1015
+ onLineBefore(p, pts[2], pts[3], pts[4], pts[5], out, o);
1016
+ continue;
1017
+ } else if (p > total) {
1018
+ const m0 = 2 * (n - 3);
1019
+ const m1 = 2 * (n - 2);
1020
+ onLinePast(p - total, pts[m0], pts[m0 + 1], pts[m1], pts[m1 + 1], out, o);
1021
+ continue;
1022
+ }
1023
+ for (;; curve++) {
1024
+ const length = lengths[curve];
1025
+ if (p > length) continue;
1026
+ if (curve === 0) p /= length;
1027
+ else {
1028
+ const prev = lengths[curve - 1];
1029
+ p = (p - prev) / (length - prev);
1030
+ }
1031
+ break;
1032
+ }
1033
+ const kk = 3 * curve + 1;
1034
+ const i0 = (kk % n) * 2;
1035
+ const i1 = ((kk + 1) % n) * 2;
1036
+ const i2 = ((kk + 2) % n) * 2;
1037
+ const i3 = ((kk + 3) % n) * 2;
1038
+ 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));
1039
+ }
1040
+ return;
1041
+ }
1042
+ const chain = this.pathChain;
1043
+ let chainLen = 0;
1044
+ if (closed) {
1045
+ for (let q = 2; q < vc; q++) chain[chainLen++] = pts[q];
1046
+ chain[chainLen++] = pts[0];
1047
+ chain[chainLen++] = pts[1];
1048
+ chain[chainLen++] = pts[2];
1049
+ chain[chainLen++] = pts[3];
1050
+ } else {
1051
+ for (let q = 2; q < vc - 2; q++) chain[chainLen++] = pts[q];
1052
+ }
1053
+ const curveCount = closed ? n / 3 : n / 3 - 1;
1054
+ const curveEnds = this.pathCurveEnds;
1055
+ let total = 0;
1056
+ let cx0 = chain[0];
1057
+ let cy0 = chain[1];
1058
+ for (let i = 0, w = 2; i < curveCount; i++, w += 6) {
1059
+ const cx1 = chain[w];
1060
+ const cy1 = chain[w + 1];
1061
+ const cx2 = chain[w + 2];
1062
+ const cy2 = chain[w + 3];
1063
+ const x2 = chain[w + 4];
1064
+ const y2 = chain[w + 5];
1065
+ const h2x = (cx0 - cx1 * 2 + cx2) * 0.1875;
1066
+ const h2y = (cy0 - cy1 * 2 + cy2) * 0.1875;
1067
+ const d3x = ((cx1 - cx2) * 3 - cx0 + x2) * 0.09375;
1068
+ const d3y = ((cy1 - cy2) * 3 - cy0 + y2) * 0.09375;
1069
+ let d2x = h2x * 2 + d3x;
1070
+ let d2y = h2y * 2 + d3y;
1071
+ let d1x = (cx1 - cx0) * 0.75 + h2x + d3x * 0.16666667;
1072
+ let d1y = (cy1 - cy0) * 0.75 + h2y + d3y * 0.16666667;
1073
+ total += Math.sqrt(d1x * d1x + d1y * d1y);
1074
+ d1x += d2x;
1075
+ d1y += d2y;
1076
+ d2x += d3x;
1077
+ d2y += d3y;
1078
+ total += Math.sqrt(d1x * d1x + d1y * d1y);
1079
+ d1x += d2x;
1080
+ d1y += d2y;
1081
+ total += Math.sqrt(d1x * d1x + d1y * d1y);
1082
+ d1x += d2x + d3x;
1083
+ d1y += d2y + d3y;
1084
+ total += Math.sqrt(d1x * d1x + d1y * d1y);
1085
+ curveEnds[i] = total;
1086
+ cx0 = x2;
1087
+ cy0 = y2;
1088
+ }
1089
+ const pathTotal = curveCount > 0 ? curveEnds[curveCount - 1] : 0;
1090
+ if (c.positionMode === PATH_POSITION_PERCENT) position *= pathTotal;
1091
+ const spaceScale = c.spacingMode === PATH_SPACING_PERCENT ? pathTotal : c.spacingMode === PATH_SPACING_PROPORTIONAL ? pathTotal / spaceCount : 1;
1092
+ const segmentEnds = this.pathSegmentEnds;
1093
+ for (let s = 0; s < 10; s++) segmentEnds[s] = 0;
1094
+ let curveTotal = 0;
1095
+ let lastCurve = -1;
1096
+ let x1 = 0, y1 = 0, cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0;
1097
+ for (let i = 0, o = 0, curve = 0, segment = 0; i < spaceCount; i++, o += 3) {
1098
+ const space = spaces[i] * spaceScale;
1099
+ position += space;
1100
+ let p = position;
1101
+ if (closed) {
1102
+ p %= pathTotal;
1103
+ if (p < 0) p += pathTotal;
1104
+ curve = 0;
1105
+ segment = 0;
1106
+ } else if (p < 0) {
1107
+ onLineBefore(p, chain[0], chain[1], chain[2], chain[3], out, o);
1108
+ continue;
1109
+ } else if (p > pathTotal) {
1110
+ onLinePast(p - pathTotal, chain[chainLen - 4], chain[chainLen - 3], chain[chainLen - 2], chain[chainLen - 1], out, o);
1111
+ continue;
1112
+ }
1113
+ for (;; curve++) {
1114
+ const length = curveEnds[curve];
1115
+ if (p > length) continue;
1116
+ if (curve === 0) p /= length;
1117
+ else {
1118
+ const prev = curveEnds[curve - 1];
1119
+ p = (p - prev) / (length - prev);
1120
+ }
1121
+ break;
1122
+ }
1123
+ if (curve !== lastCurve) {
1124
+ lastCurve = curve;
1125
+ const ii = curve * 6;
1126
+ x1 = chain[ii];
1127
+ y1 = chain[ii + 1];
1128
+ cx1 = chain[ii + 2];
1129
+ cy1 = chain[ii + 3];
1130
+ cx2 = chain[ii + 4];
1131
+ cy2 = chain[ii + 5];
1132
+ x2 = chain[ii + 6];
1133
+ y2 = chain[ii + 7];
1134
+ const h2x = (x1 - cx1 * 2 + cx2) * 0.03;
1135
+ const h2y = (y1 - cy1 * 2 + cy2) * 0.03;
1136
+ const d3x = ((cx1 - cx2) * 3 - x1 + x2) * 0.006;
1137
+ const d3y = ((cy1 - cy2) * 3 - y1 + y2) * 0.006;
1138
+ let d2x = h2x * 2 + d3x;
1139
+ let d2y = h2y * 2 + d3y;
1140
+ let d1x = (cx1 - x1) * 0.3 + h2x + d3x * 0.16666667;
1141
+ let d1y = (cy1 - y1) * 0.3 + h2y + d3y * 0.16666667;
1142
+ curveTotal = Math.sqrt(d1x * d1x + d1y * d1y);
1143
+ segmentEnds[0] = curveTotal;
1144
+ for (let s = 1; s < 8; s++) {
1145
+ d1x += d2x;
1146
+ d1y += d2y;
1147
+ d2x += d3x;
1148
+ d2y += d3y;
1149
+ curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
1150
+ segmentEnds[s] = curveTotal;
1151
+ }
1152
+ d1x += d2x;
1153
+ d1y += d2y;
1154
+ curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
1155
+ segmentEnds[8] = curveTotal;
1156
+ d1x += d2x + d3x;
1157
+ d1y += d2y + d3y;
1158
+ curveTotal += Math.sqrt(d1x * d1x + d1y * d1y);
1159
+ segmentEnds[9] = curveTotal;
1160
+ segment = 0;
1161
+ }
1162
+ p *= curveTotal;
1163
+ for (;; segment++) {
1164
+ const length = segmentEnds[segment];
1165
+ if (p > length) continue;
1166
+ if (segment === 0) p /= length;
1167
+ else {
1168
+ const prev = segmentEnds[segment - 1];
1169
+ p = segment + (p - prev) / (length - prev);
1170
+ }
1171
+ break;
1172
+ }
1173
+ onCurve(p * 0.1, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, wantsTangent || (i > 0 && space === 0));
1174
+ }
1175
+ }
1176
+
1177
+ private sliderAttachments(): void {
668
1178
  for (let k = 0; k < this.model.constraints.length; k++) {
669
1179
  this.sliderSlots(k);
670
1180
  this.sliderDeforms(k);
@@ -677,6 +1187,13 @@ export class RuntimePose {
677
1187
  const k = index, c = cs[k];
678
1188
  if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
679
1189
  const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
1190
+ if (!this.model.omitDrawOrder && a.drawOrder !== null) {
1191
+ const di = keyAt(a.drawOrder.curve, this.cursors, a.drawOrder.cursor, t);
1192
+ if (di >= 0) {
1193
+ const ns = this.drawOrder.length, orders = a.drawOrder.orders, base = di * ns;
1194
+ for (let s = 0; s < ns; s++) this.drawOrder[s] = orders[base + s];
1195
+ }
1196
+ }
680
1197
  for (let q = 0; q < a.slots.length; q++) {
681
1198
  const tl = a.slots[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
682
1199
  if (i < 0) continue;
@@ -687,9 +1204,17 @@ export class RuntimePose {
687
1204
  }
688
1205
  else {
689
1206
  const channels = tl.channels!;
1207
+ const slot = this.model.slots[tl.slot];
1208
+ const o = tl.slot * 4, d = tl.slot * 3;
690
1209
  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);
1210
+ const at = channels[f], v = channelValue(tl.curve, i, f, t);
1211
+ if (at < 4) {
1212
+ const n = o + at;
1213
+ this.light[n] = clamp01(alpha === 1 ? v : this.light[n] + (v - this.light[n]) * alpha);
1214
+ } else if (slot.dark !== null && !this.model.omitDarkColour) {
1215
+ const n = d + (at - 4);
1216
+ this.dark[n] = clamp01(alpha === 1 ? v : this.dark[n] + (v - this.dark[n]) * alpha);
1217
+ }
693
1218
  }
694
1219
  }
695
1220
  }
@@ -1213,16 +1738,32 @@ export class RuntimePose {
1213
1738
  private slotValues(anim: RuntimeAnimation | null, t: number): void {
1214
1739
  const slots = this.model.slots;
1215
1740
  const light = this.light;
1741
+ const dark = this.dark;
1216
1742
  for (let s = 0; s < slots.length; s++) {
1217
1743
  const slot = slots[s];
1218
1744
  this.shown[s] = slot.setupAttachment;
1745
+ this.drawOrder[s] = s;
1219
1746
  const o = s * 4;
1220
1747
  light[o] = slot.light[0];
1221
1748
  light[o + 1] = slot.light[1];
1222
1749
  light[o + 2] = slot.light[2];
1223
1750
  light[o + 3] = slot.light[3];
1751
+ const setupDark = slot.dark;
1752
+ if (setupDark !== null) {
1753
+ const d = s * 3;
1754
+ dark[d] = setupDark[0];
1755
+ dark[d + 1] = setupDark[1];
1756
+ dark[d + 2] = setupDark[2];
1757
+ }
1224
1758
  }
1225
1759
  if (anim === null) return;
1760
+ if (!this.model.omitDrawOrder && anim.drawOrder !== null) {
1761
+ const di = keyAt(anim.drawOrder.curve, this.cursors, anim.drawOrder.cursor, t);
1762
+ if (di >= 0) {
1763
+ const ns = slots.length, orders = anim.drawOrder.orders, base = di * ns;
1764
+ for (let s = 0; s < ns; s++) this.drawOrder[s] = orders[base + s];
1765
+ }
1766
+ }
1226
1767
  const timelines = anim.slots;
1227
1768
  for (let k = 0; k < timelines.length; k++) {
1228
1769
  const tl = timelines[k];
@@ -1233,11 +1774,18 @@ export class RuntimePose {
1233
1774
  continue;
1234
1775
  }
1235
1776
  const channels = tl.channels as Int32Array;
1236
- const setup = slots[tl.slot].light;
1777
+ const slot = slots[tl.slot];
1778
+ const setup = slot.light;
1779
+ const setupDark = slot.dark;
1237
1780
  const o = tl.slot * 4;
1781
+ const d = tl.slot * 3;
1238
1782
  for (let c = 0; c < channels.length; c++) {
1239
1783
  const at = channels[c];
1240
- light[o + at] = i < 0 ? setup[at] : clamp01(channelValue(tl.curve, i, c, t));
1784
+ if (at < 4) {
1785
+ light[o + at] = i < 0 ? setup[at] : clamp01(channelValue(tl.curve, i, c, t));
1786
+ } else if (setupDark !== null && !this.model.omitDarkColour) {
1787
+ dark[d + (at - 4)] = i < 0 ? setupDark[at - 4] : clamp01(channelValue(tl.curve, i, c, t));
1788
+ }
1241
1789
  }
1242
1790
  }
1243
1791
  }
@@ -1388,3 +1936,43 @@ function applyWorld(w: Float64Array, property: number, v: number, mix: number):
1388
1936
  }
1389
1937
  }
1390
1938
  }
1939
+
1940
+ function onLineBefore(p: number, x1: number, y1: number, x2: number, y2: number, out: Float64Array, o: number): void {
1941
+ const r = Math.atan2(y2 - y1, x2 - x1);
1942
+ out[o] = x1 + p * Math.cos(r);
1943
+ out[o + 1] = y1 + p * Math.sin(r);
1944
+ out[o + 2] = r;
1945
+ }
1946
+
1947
+ function onLinePast(p: number, x0: number, y0: number, x1: number, y1: number, out: Float64Array, o: number): void {
1948
+ const r = Math.atan2(y1 - y0, x1 - x0);
1949
+ out[o] = x1 + p * Math.cos(r);
1950
+ out[o + 1] = y1 + p * Math.sin(r);
1951
+ out[o + 2] = r;
1952
+ }
1953
+
1954
+ 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 {
1955
+ if (Number.isNaN(p)) {
1956
+ out[o] = x1;
1957
+ out[o + 1] = y1;
1958
+ out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1);
1959
+ return;
1960
+ }
1961
+ const t2 = p * p;
1962
+ const t3 = t2 * p;
1963
+ const u = 1 - p;
1964
+ const u2 = u * u;
1965
+ const u3 = u2 * u;
1966
+ const ut = u * p;
1967
+ const ut3x = ut * 3;
1968
+ const w1 = u * ut3x;
1969
+ const w2 = ut3x * p;
1970
+ const x = x1 * u3 + cx1 * w1 + cx2 * w2 + x2 * t3;
1971
+ const y = y1 * u3 + cy1 * w1 + cy2 * w2 + y2 * t3;
1972
+ out[o] = x;
1973
+ out[o + 1] = y;
1974
+ if (wantsTangent) {
1975
+ if (p < 0.001) out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1);
1976
+ else out[o + 2] = Math.atan2(y - (y1 * u2 + cy1 * ut * 2 + cy2 * t2), x - (x1 * u2 + cx1 * ut * 2 + cx2 * t2));
1977
+ }
1978
+ }