@rig-c/core 2.76.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.
- package/docs/AUTHORING.md +14 -11
- package/package.json +1 -1
- package/src/runtime/index.ts +2 -2
- package/src/runtime/model.ts +164 -19
- package/src/runtime/pose.ts +558 -16
package/docs/AUTHORING.md
CHANGED
|
@@ -12647,7 +12647,7 @@ verdict on those figures.
|
|
|
12647
12647
|
| <a id="CE_COMPILE_ALPHA_CLIP_CONFLICT"></a>`CE_COMPILE_ALPHA_CLIP_CONFLICT` | Names authored fields or convex/inverse flags conflicting with generated inside-clipping geometry. Use one authored/generated form.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦: alphaClip conflicts with [⟧<br>⟦vertexCount⟧<br>⟦vertices⟧<br>⟦weights⟧<br>⟦boneIndexing⟧<br>⟦, ⟧<br>⟦], convex=⟧<br>⟦, inverse=⟧<br>⟦; required ordinary inside clipping with generated geometry only. Remove authored geometry/flags.⟧ |
|
|
12648
12648
|
| <a id="CE_COMPILE_ALPHA_CLIP_DEFORM"></a>`CE_COMPILE_ALPHA_CLIP_DEFORM` | Names animation and source/clip key/frame counts that cannot share the existing projection contract. Give matching explicit supported transform keys in compatible actual frames, with constant world shares where needed, or author the corresponding clip geometry and motion; weighted binding does not copy source deform.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦ alphaClip from "⟧<br>⟦"⟧<br>⟦: animation "⟧<br>⟦" has ⟧<br>⟦ source and ⟧<br>⟦ clip keys; source frame=⟧<br>⟦no vertex geometry⟧<br>⟦clip frame=⟧<br>⟦, constant world influence shares=⟧<br>⟦Required matching explicit affine/depth-yaw/pitch transforms in compatible DeformGeometry frames, ⟧<br>⟦with constant shares for world-space transfer. Fix the named frame/keys or author corresponding clip motion.⟧ |
|
|
12649
12649
|
|
|
12650
|
-
## 15. Posing in place for a player — the runtime poser (stage
|
|
12650
|
+
## 15. Posing in place for a player — the runtime poser (stage 4)
|
|
12651
12651
|
|
|
12652
12652
|
rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
|
|
12653
12653
|
(`src/core/`) is the reference: each pose is a pure function of the document,
|
|
@@ -12682,13 +12682,13 @@ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeho
|
|
|
12682
12682
|
The display `name` may be shared by different placeholders; it is never a table
|
|
12683
12683
|
key to guess the source from. These load-time strings do not change the pose path.
|
|
12684
12684
|
|
|
12685
|
-
| | what stage
|
|
12685
|
+
| | what stage 4 poses |
|
|
12686
12686
|
| --- | --- |
|
|
12687
12687
|
| bones | local and world transforms, all five inherit modes, skin-required bones under the view's skin (an inactive bone is zeros, as the oracle poses it) |
|
|
12688
|
-
| timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, `rgba`, `rgb` and `alpha`; ik and
|
|
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 |
|
|
12688
|
+
| timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, `rgba`, `rgb` and `alpha`; ik, transform and path timelines; linear, stepped and Bézier keys, each read from a per-pose key cursor |
|
|
12689
|
+
| constraints | physics with its exact clock, reset and parameter timelines, and optional constraint-scoped external translate/rotate; value-mapped sliders with bone/slot/deform/physics composition; path constraints across all position, spacing and rotate modes on open or closed, constant-speed or arc-length, weighted or unweighted, deformed curves; one- and two-bone ik; world-space, absolute transform constraints; the document's order, each followed by the bones below what it moved posed again |
|
|
12690
12690
|
| vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
|
|
12691
|
-
| 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` |
|
|
12691
|
+
| deform | weighted and unweighted mesh and path timelines, linear/stepped/Bézier, per-pose key cursors and in-place buffers; linked meshes inherit their source's timelines only with `timelines: true` |
|
|
12692
12692
|
| skins and events | named/no-skin views with their skin-required bones and constraints; event delivery in key order over `(last, time]`, tail then head across a wrap |
|
|
12693
12693
|
| time | one animation at full weight from the setup pose, at a time (`apply`) or stepped as a looping track (`step`, the track time wrapped by the duration as `openTrack` wraps it) |
|
|
12694
12694
|
|
|
@@ -12740,8 +12740,12 @@ looping track; it takes the initial reset at animation zero if needed.
|
|
|
12740
12740
|
The physics clock is a running sum, never the animation time; constructor option
|
|
12741
12741
|
`{ clock: physicsClockStep }` can select the render clock without changing track
|
|
12742
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
|
|
12743
|
+
throws `RuntimeInputError` naming the call. `physicsTranslate(x, y, scope?)` and
|
|
12744
|
+
`physicsRotate(x, y, degrees, scope?)` shift the previous origins across every
|
|
12745
|
+
physics constraint when `scope` is omitted, or only across the named physics
|
|
12746
|
+
constraints (`scope: []` moves none; duplicate names apply once; an unknown or
|
|
12747
|
+
non-physics constraint name throws `RuntimeInputError` before mutating any
|
|
12748
|
+
state); `setEnvironment({
|
|
12745
12749
|
wind: [x, y], gravity: [x, y] })` supplies the skeleton directions (positive y
|
|
12746
12750
|
points down), all numbers finite. Offsets, velocities, lags and buffers belong
|
|
12747
12751
|
to each pose; a zero physics mix does not consume its constraint's clock.
|
|
@@ -12765,7 +12769,7 @@ so an unselected track cannot hide a construct the slider always applies.
|
|
|
12765
12769
|
|
|
12766
12770
|
**Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
|
|
12767
12771
|
every construct met, each `{ feature, detail, animation }`, and the message
|
|
12768
|
-
reads `not posed by the runtime poser (stage
|
|
12772
|
+
reads `not posed by the runtime poser (stage 4), N construct(s) missing:
|
|
12769
12773
|
<feature>: <detail>; …`. A model-level feature refuses the whole model; an
|
|
12770
12774
|
animation-level one refuses that animation alone (`model.refused`;
|
|
12771
12775
|
`model.animation(name)` and `setAnimation(name)` throw its refusal) and every
|
|
@@ -12774,13 +12778,12 @@ refused: nothing is posed approximately.
|
|
|
12774
12778
|
|
|
12775
12779
|
| feature | refuses | what it is |
|
|
12776
12780
|
| --- | --- | --- |
|
|
12777
|
-
| `path-constraint` | the model | a constraint of that kind |
|
|
12778
12781
|
| `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
|
|
12779
|
-
| `inactive-constrained-bone` | the model | an applied ik or
|
|
12782
|
+
| `inactive-constrained-bone` | the model | an applied ik, transform or path constraint naming a bone the view leaves inactive (what the oracle writes there depends on the pass before) |
|
|
12780
12783
|
| `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 |
|
|
12781
12784
|
| `clipping`, `null-atlas-region` | the model when shown at setup, else the animation that keys it | a clipping attachment; a region whose atlas rectangle the model states as `null` |
|
|
12782
12785
|
| `skin-conflict` | as above | a placeholder the skins fill differently |
|
|
12783
|
-
| `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`) |
|
|
12786
|
+
| `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`, `pathAnimationsWhy`, `steppedPreviousPassWhy`, `sliderDeformsWalkedPathWhy`) |
|
|
12784
12787
|
| `inherit-timeline`, `sequence-timeline`, `draw-order-timeline`, `dark-colour-timeline` | the animation | a timeline of that kind (`dark-colour` is `rgba2` / `rgb2`) |
|
|
12785
12788
|
|
|
12786
12789
|
A call it cannot answer throws `RuntimeInputError`, naming the call:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.77.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": {
|
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 4: 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, world-space absolute transforms, physics and value-mapped sliders with their
|
|
29
|
+
* two-bone ik, world-space absolute transforms, path constraints, physics (with optional movement scope) 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
|
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 4): 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,9 +12,9 @@
|
|
|
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
|
|
15
|
+
* when one is a property of the whole model — a transform constraint reading or
|
|
16
16
|
* writing local values or adding (`localSource`, `localTarget`,
|
|
17
|
-
* `additive`), an ik or
|
|
17
|
+
* `additive`), an ik, transform or path constraint over a bone the view leaves
|
|
18
18
|
* inactive, a world-space transform onto a `noScale` or
|
|
19
19
|
* `noScaleOrReflection` bone, a clipping attachment or a region with no
|
|
20
20
|
* atlas rectangle shown at setup, a placeholder the skins fill
|
|
@@ -26,8 +26,7 @@
|
|
|
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
|
-
* parameter timelines and per-pass writes. Mesh deforms, named/no-skin views
|
|
29
|
+
* `RUNTIME_FEATURES` is the closed list of what can be named. Stage 4 adds path constraints and scoped physics movement (`physicsTranslate`/`physicsRotate` with an optional constraint scope) on top of Stage 3's indexed physics and value-mapped sliders. Mesh and path deforms, named/no-skin views
|
|
31
30
|
* and immutable event deliveries remain covered. `ignore: ['events']` remains an explicit diagnostic opt-out; it is
|
|
32
31
|
* never the default. A merged view with conflicting attachments still refuses
|
|
33
32
|
* by name, as the oracle itself cannot resolve that view without file order.
|
|
@@ -47,19 +46,19 @@
|
|
|
47
46
|
*/
|
|
48
47
|
import type { ModelVertices } from '../model.ts';
|
|
49
48
|
import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, readBlend, readColour, shownAttachment, shownRow, sourceOfDoc, underSkin, type CompiledDocument, type CoreAnimation, type CoreBlendMode } from '../core/index.ts';
|
|
50
|
-
import { constraintInactiveWhy, constraintsAbsentWhy, TRANSFORM_PROPERTIES, type CoreIkRecord, type CoreTransformRecord } from '../core/constraints.ts';
|
|
49
|
+
import { constraintInactiveWhy, constraintsAbsentWhy, pathAnimationsWhy, TRANSFORM_PROPERTIES, type CoreIkRecord, type CoreTransformRecord } from '../core/constraints.ts';
|
|
50
|
+
import { slotBonePlan, type CorePathRecord, type SlotBoneEvent } from '../core/constraints_path.ts';
|
|
51
51
|
import { RUNTIME_PI } from '../core/world.ts';
|
|
52
52
|
import { heldArray, timelineIdentity } from '../core/deform.ts';
|
|
53
|
-
import type
|
|
53
|
+
import { steppedPreviousPassWhy, type CorePhysicsRecord, type PhysicsTimelineKind } from '../core/constraints_physics.ts';
|
|
54
54
|
import type { CoreSliderRecord } from '../core/constraints_slider.ts';
|
|
55
55
|
import { compileCurve, compileDeformCurve, type RuntimeCurve } from './curves.ts';
|
|
56
56
|
|
|
57
57
|
/** The stage of issue #1591 this poser implements. */
|
|
58
|
-
export const RUNTIME_STAGE =
|
|
58
|
+
export const RUNTIME_STAGE = 4;
|
|
59
59
|
|
|
60
60
|
/** Every construct the runtime poser can refuse, by the name its refusals carry. */
|
|
61
61
|
export const RUNTIME_FEATURES = [
|
|
62
|
-
'path-constraint',
|
|
63
62
|
'local-transform',
|
|
64
63
|
'additive-transform',
|
|
65
64
|
'inactive-constrained-bone',
|
|
@@ -125,11 +124,13 @@ export interface RuntimePlant {
|
|
|
125
124
|
readonly eventWrapOffByOne?: boolean;
|
|
126
125
|
/** The old restore behavior, which repeats keys up to the restored time. */
|
|
127
126
|
readonly restoreEventsFromStart?: boolean;
|
|
128
|
-
/** Omit deform / multiply offsets by weights too early: negative controls only. */
|
|
127
|
+
/** Omit deform / multiply offsets by weights too early / omit path / ignore physics scope: negative controls only. */
|
|
129
128
|
readonly omitPhysics?: boolean;
|
|
130
129
|
readonly sliderAsTrack?: boolean;
|
|
131
130
|
readonly omitDeform?: boolean;
|
|
132
131
|
readonly deformAfterWeights?: boolean;
|
|
132
|
+
readonly omitPath?: boolean;
|
|
133
|
+
readonly ignorePhysicsScope?: boolean;
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
// A bone's local values, seven per bone.
|
|
@@ -177,6 +178,17 @@ export const IK_BEND_POSITIVE = 1;
|
|
|
177
178
|
export const IK_COMPRESS = 2;
|
|
178
179
|
export const IK_STRETCH = 4;
|
|
179
180
|
|
|
181
|
+
// A path constraint's position, spacing and rotate modes.
|
|
182
|
+
export const PATH_POSITION_FIXED = 0;
|
|
183
|
+
export const PATH_POSITION_PERCENT = 1;
|
|
184
|
+
export const PATH_SPACING_LENGTH = 0;
|
|
185
|
+
export const PATH_SPACING_FIXED = 1;
|
|
186
|
+
export const PATH_SPACING_PERCENT = 2;
|
|
187
|
+
export const PATH_SPACING_PROPORTIONAL = 3;
|
|
188
|
+
export const PATH_ROTATE_TANGENT = 0;
|
|
189
|
+
export const PATH_ROTATE_CHAIN = 1;
|
|
190
|
+
export const PATH_ROTATE_CHAIN_SCALE = 2;
|
|
191
|
+
|
|
180
192
|
/** Degrees to radians, with the runtime's pi (`src/core/world.ts`). */
|
|
181
193
|
export const RAD = RUNTIME_PI / 180;
|
|
182
194
|
|
|
@@ -281,6 +293,36 @@ export interface RuntimeSlider {
|
|
|
281
293
|
animation: RuntimeAnimation;
|
|
282
294
|
}
|
|
283
295
|
|
|
296
|
+
export interface RuntimePath {
|
|
297
|
+
readonly kind: 'path';
|
|
298
|
+
readonly name: string;
|
|
299
|
+
readonly index: number;
|
|
300
|
+
readonly applied: boolean;
|
|
301
|
+
readonly bones: Int32Array;
|
|
302
|
+
readonly slot: number;
|
|
303
|
+
readonly slotBone: number;
|
|
304
|
+
/** Setup path attachment index into `model.attachments`, or −1 when the slot shows no path. */
|
|
305
|
+
attachment: number;
|
|
306
|
+
/** Number of vertex coordinates (`2 * vertexCount`) in the setup path attachment. */
|
|
307
|
+
pathVertices: number;
|
|
308
|
+
readonly closed: boolean;
|
|
309
|
+
readonly constantSpeed: boolean;
|
|
310
|
+
readonly lengths: Float64Array;
|
|
311
|
+
readonly positionMode: number;
|
|
312
|
+
readonly spacingMode: number;
|
|
313
|
+
readonly rotateMode: number;
|
|
314
|
+
readonly offsetRotation: number;
|
|
315
|
+
readonly position: number;
|
|
316
|
+
readonly spacing: number;
|
|
317
|
+
readonly mixRotate: number;
|
|
318
|
+
readonly mixX: number;
|
|
319
|
+
readonly mixY: number;
|
|
320
|
+
/** Constraint index whose `before` or `after` state sets the slot bone's determinant sign (`slotBonePlan`), or −1. */
|
|
321
|
+
readonly slotEventAt: number;
|
|
322
|
+
readonly slotEventAfter: boolean;
|
|
323
|
+
readonly setup: CorePathRecord;
|
|
324
|
+
}
|
|
325
|
+
|
|
284
326
|
export interface RuntimePhysicsTimeline {
|
|
285
327
|
readonly kind: PhysicsTimelineKind;
|
|
286
328
|
readonly targets: Int32Array;
|
|
@@ -293,7 +335,15 @@ export interface RuntimeSliderTimeline {
|
|
|
293
335
|
readonly timeCursor: number;
|
|
294
336
|
readonly mixCursor: number;
|
|
295
337
|
}
|
|
296
|
-
export
|
|
338
|
+
export interface RuntimePathTimeline {
|
|
339
|
+
readonly position: RuntimeCurve | null;
|
|
340
|
+
readonly spacing: RuntimeCurve | null;
|
|
341
|
+
readonly mix: RuntimeCurve | null;
|
|
342
|
+
readonly positionCursor: number;
|
|
343
|
+
readonly spacingCursor: number;
|
|
344
|
+
readonly mixCursor: number;
|
|
345
|
+
}
|
|
346
|
+
export type RuntimeConstraint = RuntimeIk | RuntimeTransform | RuntimePhysics | RuntimeSlider | RuntimePath;
|
|
297
347
|
|
|
298
348
|
/** 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. */
|
|
299
349
|
export interface RuntimeBoneTimeline {
|
|
@@ -351,6 +401,7 @@ export interface RuntimeAnimation {
|
|
|
351
401
|
readonly deforms: readonly RuntimeDeformTimeline[];
|
|
352
402
|
readonly physics: readonly RuntimePhysicsTimeline[];
|
|
353
403
|
readonly sliders: ReadonlyArray<RuntimeSliderTimeline | null>;
|
|
404
|
+
readonly paths: ReadonlyArray<RuntimePathTimeline | null>;
|
|
354
405
|
readonly slots: readonly RuntimeSlotTimeline[];
|
|
355
406
|
/** Per constraint of the plan, its timeline in this animation or `null`. */
|
|
356
407
|
readonly constraints: ReadonlyArray<RuntimeConstraintTimeline | null>;
|
|
@@ -368,6 +419,11 @@ export interface RuntimeModel {
|
|
|
368
419
|
readonly deformAfterWeights: boolean;
|
|
369
420
|
readonly omitPhysics: boolean;
|
|
370
421
|
readonly sliderAsTrack: boolean;
|
|
422
|
+
readonly omitPath: boolean;
|
|
423
|
+
readonly ignorePhysicsScope: boolean;
|
|
424
|
+
readonly hasPath: boolean;
|
|
425
|
+
readonly maxPathSpaces: number;
|
|
426
|
+
readonly maxPathVertices: number;
|
|
371
427
|
readonly deformCount: number;
|
|
372
428
|
readonly eventWrapOffByOne: boolean;
|
|
373
429
|
readonly restoreEventsFromStart: boolean;
|
|
@@ -443,7 +499,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
443
499
|
const refusals = new Refusals(new Set(plant.admit ?? []));
|
|
444
500
|
const ignoreEvents = (options.ignore ?? []).includes('events');
|
|
445
501
|
|
|
446
|
-
const absentWhy = constraintsAbsentWhy(doc);
|
|
502
|
+
const absentWhy = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
|
|
447
503
|
if (absentWhy !== null) refusals.note('oracle-absent', `the oracle leaves the bones out: ${absentWhy}`);
|
|
448
504
|
|
|
449
505
|
// --- bones ---
|
|
@@ -454,6 +510,12 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
454
510
|
if (i === undefined) throw new RuntimeInputError(`bone "${name}" is not a bone of this document (readModel refuses it first)`);
|
|
455
511
|
return i;
|
|
456
512
|
};
|
|
513
|
+
const slotIndex = new Map(doc.slots.map((s, i) => [s.name, i]));
|
|
514
|
+
const slotIndexOf = (name: string): number => {
|
|
515
|
+
const i = slotIndex.get(name);
|
|
516
|
+
if (i === undefined) throw new RuntimeInputError(`slot "${name}" is not a slot of this document (readModel refuses it first)`);
|
|
517
|
+
return i;
|
|
518
|
+
};
|
|
457
519
|
const active = activeBones(doc);
|
|
458
520
|
const parents = new Int32Array(nb);
|
|
459
521
|
const modes = new Uint8Array(nb);
|
|
@@ -491,9 +553,13 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
491
553
|
|
|
492
554
|
// --- constraints, in the document's order ---
|
|
493
555
|
const constraints: RuntimeConstraint[] = [];
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
556
|
+
const records = constraintRecords(doc);
|
|
557
|
+
const skipped = new Set<number>();
|
|
558
|
+
records.forEach((r, i) => {
|
|
559
|
+
if (constraintInactiveWhy(r, active) !== null) skipped.add(i);
|
|
560
|
+
});
|
|
561
|
+
const pathPlan = records.some((r) => r.kind === 'path') ? slotBonePlan(doc.bones, active, records, skipped) : new Map<number, SlotBoneEvent | null>();
|
|
562
|
+
for (const r of records) {
|
|
497
563
|
const applied = constraintInactiveWhy(r, active) === null;
|
|
498
564
|
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));
|
|
499
565
|
const index = constraints.length;
|
|
@@ -507,7 +573,9 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
507
573
|
}
|
|
508
574
|
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`);
|
|
509
575
|
if (r.kind === 'ik') constraints.push(compileIk(r, index, applied, bones, indexOf));
|
|
510
|
-
else {
|
|
576
|
+
else if (r.kind === 'path') {
|
|
577
|
+
constraints.push(compilePath(r, index, applied, bones, indexOf, slotIndexOf, pathPlan.get(index) ?? null));
|
|
578
|
+
} else {
|
|
511
579
|
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 ')}`);
|
|
512
580
|
if (r.additive) refusals.note('additive-transform', `transform constraint "${r.name}" is additive`);
|
|
513
581
|
const t = compileTransform(r, index, applied, bones, indexOf);
|
|
@@ -585,8 +653,10 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
585
653
|
const source = g.kind === 'mesh' ? g.vertices : sourceOf(g.skin, g.slot, g.source);
|
|
586
654
|
if (typeof source === 'string') throw new RuntimeInputError(`slot "${slot.name}": ${source} (readModel refuses it first)`);
|
|
587
655
|
made = meshAttachment(at, source, indexOf, plant.bindingIndex, nb);
|
|
656
|
+
} else if (g?.kind === 'path') {
|
|
657
|
+
made = pathAttachment(at, g.vertices, indexOf, plant.bindingIndex, nb);
|
|
588
658
|
}
|
|
589
|
-
// A bounding box, a
|
|
659
|
+
// A bounding box, a refused record admitted by a plant: the row's name and path, and nothing drawn.
|
|
590
660
|
if (made === null) made = vertexAttachment(at, DRAW_NONE, new Float64Array(0), 0);
|
|
591
661
|
attachments.push(made);
|
|
592
662
|
vertexCount += made.count;
|
|
@@ -607,7 +677,21 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
607
677
|
blend: readBlend(slot),
|
|
608
678
|
};
|
|
609
679
|
});
|
|
610
|
-
|
|
680
|
+
let hasPath = false;
|
|
681
|
+
let maxPathSpaces = 0;
|
|
682
|
+
let maxPathVertices = 0;
|
|
683
|
+
for (const c of constraints) {
|
|
684
|
+
if (c.kind !== 'path') continue;
|
|
685
|
+
hasPath = true;
|
|
686
|
+
c.attachment = c.setup.path === null ? -1 : slots[c.slot].setupAttachment;
|
|
687
|
+
const spaceCount = c.rotateMode === PATH_ROTATE_TANGENT ? c.bones.length : c.bones.length + 1;
|
|
688
|
+
if (spaceCount > maxPathSpaces) maxPathSpaces = spaceCount;
|
|
689
|
+
if (c.attachment >= 0) {
|
|
690
|
+
const at = attachments[c.attachment];
|
|
691
|
+
c.pathVertices = at.bindingCounts.length > 0 ? at.bindingCounts.length * 2 : at.local.length;
|
|
692
|
+
if (c.pathVertices > maxPathVertices) maxPathVertices = c.pathVertices;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
611
695
|
|
|
612
696
|
// --- animations ---
|
|
613
697
|
const animations = new Map<string, RuntimeAnimation>();
|
|
@@ -635,6 +719,11 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
635
719
|
sliderAsTrack: plant.sliderAsTrack === true,
|
|
636
720
|
omitDeform: plant.omitDeform === true,
|
|
637
721
|
deformAfterWeights: plant.deformAfterWeights === true,
|
|
722
|
+
omitPath: plant.omitPath === true,
|
|
723
|
+
ignorePhysicsScope: plant.ignorePhysicsScope === true,
|
|
724
|
+
hasPath,
|
|
725
|
+
maxPathSpaces,
|
|
726
|
+
maxPathVertices,
|
|
638
727
|
deformCount,
|
|
639
728
|
eventWrapOffByOne: plant.eventWrapOffByOne === true,
|
|
640
729
|
restoreEventsFromStart: plant.restoreEventsFromStart === true,
|
|
@@ -700,6 +789,12 @@ function meshAttachment(at: AttachmentHead, vertices: ModelVertices, indexOf: (n
|
|
|
700
789
|
return { ...at, deformCount: ids.length * 2, draw: DRAW_WEIGHTED, local: NO_NUMBERS, count: counts.length * 2, bindingCounts: Int32Array.from(counts), bindingBones: Int32Array.from(ids), bindingX: Float64Array.from(xs), bindingY: Float64Array.from(ys), bindingWeights: Float64Array.from(ws) };
|
|
701
790
|
}
|
|
702
791
|
|
|
792
|
+
/** A path attachment's vertices and bindings: stored and deformed like a mesh, never drawn into `RuntimePose.vertices`. */
|
|
793
|
+
function pathAttachment(at: AttachmentHead, vertices: ModelVertices, indexOf: (name: string) => number, rewrite: RuntimePlant['bindingIndex'], bones: number): RuntimeAttachment {
|
|
794
|
+
const m = meshAttachment(at, vertices, indexOf, rewrite, bones);
|
|
795
|
+
return { ...m, draw: DRAW_NONE, count: 0 };
|
|
796
|
+
}
|
|
797
|
+
|
|
703
798
|
function compileIk(r: CoreIkRecord, index: number, applied: boolean, bones: Int32Array, indexOf: (name: string) => number): RuntimeIk {
|
|
704
799
|
return {
|
|
705
800
|
kind: 'ik', name: r.name, index, applied, bones, target: indexOf(r.target),
|
|
@@ -712,6 +807,43 @@ function ikFlags(bendPositive: boolean, compress: boolean, stretch: boolean): nu
|
|
|
712
807
|
return (bendPositive ? IK_BEND_POSITIVE : 0) | (compress ? IK_COMPRESS : 0) | (stretch ? IK_STRETCH : 0);
|
|
713
808
|
}
|
|
714
809
|
|
|
810
|
+
function compilePath(
|
|
811
|
+
r: CorePathRecord,
|
|
812
|
+
index: number,
|
|
813
|
+
applied: boolean,
|
|
814
|
+
bones: Int32Array,
|
|
815
|
+
indexOf: (name: string) => number,
|
|
816
|
+
slotIndexOf: (name: string) => number,
|
|
817
|
+
ev: SlotBoneEvent | null,
|
|
818
|
+
): RuntimePath {
|
|
819
|
+
return {
|
|
820
|
+
kind: 'path',
|
|
821
|
+
name: r.name,
|
|
822
|
+
index,
|
|
823
|
+
applied,
|
|
824
|
+
bones,
|
|
825
|
+
slot: slotIndexOf(r.slot),
|
|
826
|
+
slotBone: indexOf(r.slotBone),
|
|
827
|
+
attachment: -1,
|
|
828
|
+
pathVertices: 0,
|
|
829
|
+
closed: r.path?.closed ?? false,
|
|
830
|
+
constantSpeed: r.path?.constantSpeed ?? true,
|
|
831
|
+
lengths: Float64Array.from(r.path?.lengths ?? []),
|
|
832
|
+
positionMode: r.positionMode === 'fixed' ? PATH_POSITION_FIXED : PATH_POSITION_PERCENT,
|
|
833
|
+
spacingMode: r.spacingMode === 'fixed' ? PATH_SPACING_FIXED : r.spacingMode === 'percent' ? PATH_SPACING_PERCENT : r.spacingMode === 'proportional' ? PATH_SPACING_PROPORTIONAL : PATH_SPACING_LENGTH,
|
|
834
|
+
rotateMode: r.rotateMode === 'chain' ? PATH_ROTATE_CHAIN : r.rotateMode === 'chainScale' ? PATH_ROTATE_CHAIN_SCALE : PATH_ROTATE_TANGENT,
|
|
835
|
+
offsetRotation: r.offsetRotation,
|
|
836
|
+
position: r.position,
|
|
837
|
+
spacing: r.spacing,
|
|
838
|
+
mixRotate: r.mixRotate,
|
|
839
|
+
mixX: r.mixX,
|
|
840
|
+
mixY: r.mixY,
|
|
841
|
+
slotEventAt: ev === null ? -1 : ev.at,
|
|
842
|
+
slotEventAfter: ev?.when === 'after',
|
|
843
|
+
setup: r,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
|
|
715
847
|
function compileTransform(r: CoreTransformRecord, index: number, applied: boolean, bones: Int32Array, indexOf: (name: string) => number): RuntimeTransform {
|
|
716
848
|
const fromProperty: number[] = [];
|
|
717
849
|
const fromOffset: number[] = [];
|
|
@@ -835,6 +967,19 @@ function compileAnimation(
|
|
|
835
967
|
if (keyed === undefined) return null;
|
|
836
968
|
return { time: keyed.time === null ? null : compileCurve(keyed.time, 1), mix: keyed.mix === null ? null : compileCurve(keyed.mix, 1), timeCursor: cursors++, mixCursor: cursors++ };
|
|
837
969
|
});
|
|
970
|
+
const paths = constraints.map((c): RuntimePathTimeline | null => {
|
|
971
|
+
if (c.kind !== 'path') return null;
|
|
972
|
+
const keyed = anim.constraints.path.find((t) => t.name === c.name);
|
|
973
|
+
if (keyed === undefined) return null;
|
|
974
|
+
return {
|
|
975
|
+
position: keyed.position === undefined ? null : compileCurve(keyed.position, 1),
|
|
976
|
+
spacing: keyed.spacing === undefined ? null : compileCurve(keyed.spacing, 1),
|
|
977
|
+
mix: keyed.mix === undefined ? null : compileCurve(keyed.mix, 3),
|
|
978
|
+
positionCursor: cursors++,
|
|
979
|
+
spacingCursor: cursors++,
|
|
980
|
+
mixCursor: cursors++,
|
|
981
|
+
};
|
|
982
|
+
});
|
|
838
983
|
const physics: RuntimePhysicsTimeline[] = (anim.constraints.physicsKeyed ?? []).map(t => ({
|
|
839
984
|
kind: t.kind, cursor: cursors++, curve: compileCurve(t.keys, t.kind === 'reset' ? 0 : 1),
|
|
840
985
|
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)),
|
|
@@ -846,5 +991,5 @@ function compileAnimation(
|
|
|
846
991
|
const arrays = keys.map(k => heldArray(g.vertices, k));
|
|
847
992
|
return { identity: `${a.skin}/${a.slot}/${a.attachment}`, cursor: cursors++, curve: compileDeformCurve(keys), count: arrays[0].length, held: Float64Array.from(arrays.flat()) };
|
|
848
993
|
});
|
|
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 }))) };
|
|
994
|
+
return { name, duration: tl.duration, physics, sliders, paths, 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 }))) };
|
|
850
995
|
}
|
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 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,10 +9,11 @@
|
|
|
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
|
|
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
|
|
15
|
-
*
|
|
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
|
|
16
17
|
* of one unstepped pose survives but key cursors. A stepped pose also keeps
|
|
17
18
|
* its physics clock, offsets, velocities and lags; sliders select values
|
|
18
19
|
* within this pose rather than advancing an independent track.
|
|
@@ -22,9 +23,9 @@
|
|
|
22
23
|
* Every formula below is the one the named function of `src/core/` computes,
|
|
23
24
|
* in its order of operations, on the same doubles: a bone's frame and its
|
|
24
25
|
* five inherit modes (`world.ts`), the timelines (`animation.ts`), the one-
|
|
25
|
-
* and two-bone ik, the world-space transform, the
|
|
26
|
-
* local values and the update order (`constraints.ts`),
|
|
27
|
-
* 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
|
|
28
29
|
* there by measurement against spine-core 4.3.13 to the last bit; the runtime
|
|
29
30
|
* does not re-measure them, it is held to the oracle at tolerance 0
|
|
30
31
|
* (`tools/core_gate.ts`, *The runtime poser*), so an order changed here is a
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
*
|
|
33
34
|
* ⚠️ The exactness hazards, each kept as the oracle has it: the setup value
|
|
34
35
|
* plus 0 before a timeline's first key (which turns a `-0` setup into `0`);
|
|
35
|
-
* a keyed ik or
|
|
36
|
+
* a keyed ik, transform or path value reaching the pose through the setup blend
|
|
36
37
|
* `setup + (value − setup)·1`; `Math.PI` for a folded uniform two-bone ik and
|
|
37
38
|
* the runtime's 3.1415927 everywhere else; the `(−180, 180]` wrap; a mix of
|
|
38
39
|
* exactly 0 touching nothing; an inactive bone posed as zeros by the
|
|
@@ -62,6 +63,13 @@ import {
|
|
|
62
63
|
MODE_NO_SCALE_OR_REFLECTION,
|
|
63
64
|
MODE_NORMAL,
|
|
64
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,
|
|
65
73
|
RAD,
|
|
66
74
|
RuntimeInputError,
|
|
67
75
|
SCALE_Y_UNIFORM,
|
|
@@ -74,9 +82,11 @@ import {
|
|
|
74
82
|
W_Y,
|
|
75
83
|
WORLDS,
|
|
76
84
|
type RuntimeAnimation,
|
|
85
|
+
type RuntimeConstraint,
|
|
77
86
|
type RuntimeEvent,
|
|
78
87
|
type RuntimeIk,
|
|
79
88
|
type RuntimeModel,
|
|
89
|
+
type RuntimePath,
|
|
80
90
|
type RuntimeTransform,
|
|
81
91
|
type RuntimeSlider,
|
|
82
92
|
type RuntimePhysicsTimeline,
|
|
@@ -84,6 +94,7 @@ import {
|
|
|
84
94
|
|
|
85
95
|
const DEG = RUNTIME_DEG;
|
|
86
96
|
const TWO_PI = 2 * RUNTIME_PI;
|
|
97
|
+
const PATH_EPSILON = 0.00001;
|
|
87
98
|
// The transform properties, by `TRANSFORM_PROPERTIES` index (src/core/constraints.ts).
|
|
88
99
|
const P_ROTATE = 0;
|
|
89
100
|
const P_X = 1;
|
|
@@ -134,11 +145,20 @@ export class RuntimePose {
|
|
|
134
145
|
|
|
135
146
|
private current: RuntimeAnimation | null = null;
|
|
136
147
|
private readonly cursors: Int32Array;
|
|
137
|
-
// 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.
|
|
138
149
|
private readonly ikMix: Float64Array;
|
|
139
150
|
private readonly ikSoftness: Float64Array;
|
|
140
151
|
private readonly ikFlags: Uint8Array;
|
|
141
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);
|
|
142
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.
|
|
143
163
|
private readonly changed: Int32Array;
|
|
144
164
|
private readonly inWorld: Int32Array;
|
|
@@ -156,6 +176,7 @@ export class RuntimePose {
|
|
|
156
176
|
private readonly hasPhysics: boolean;
|
|
157
177
|
private readonly physicsStates: PhysicsState[];
|
|
158
178
|
private readonly physicsRecords: Array<CorePhysicsRecord | null>;
|
|
179
|
+
private readonly physicsScope: Uint8Array;
|
|
159
180
|
private readonly environment = new Float64Array([1, 0, 0, 1]);
|
|
160
181
|
private readonly sliderTime: Float64Array;
|
|
161
182
|
private readonly sliderMix: Float64Array;
|
|
@@ -174,6 +195,7 @@ export class RuntimePose {
|
|
|
174
195
|
const nb = model.boneNames.length;
|
|
175
196
|
const ns = model.slots.length;
|
|
176
197
|
const nc = model.constraints.length;
|
|
198
|
+
this.physicsScope = new Uint8Array(nc);
|
|
177
199
|
this.locals = new Float64Array(nb * LOCALS);
|
|
178
200
|
this.world = new Float64Array(nb * WORLDS);
|
|
179
201
|
this.light = new Float64Array(ns * 4);
|
|
@@ -186,6 +208,16 @@ export class RuntimePose {
|
|
|
186
208
|
this.ikSoftness = new Float64Array(nc);
|
|
187
209
|
this.ikFlags = new Uint8Array(nc);
|
|
188
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);
|
|
189
221
|
// Sized by the longest target list (`RuntimeModel.scratch`): a transform may name one bone several times, and every entry is applied and reposed.
|
|
190
222
|
this.changed = new Int32Array(model.scratch);
|
|
191
223
|
this.inWorld = new Int32Array(model.scratch);
|
|
@@ -233,9 +265,11 @@ export class RuntimePose {
|
|
|
233
265
|
this.physicsValues(null, 0, false);
|
|
234
266
|
this.locals.set(this.model.setup);
|
|
235
267
|
this.hierarchy();
|
|
268
|
+
this.slotValues(null, 0);
|
|
269
|
+
this.deformValues(null, 0);
|
|
236
270
|
this.constraintValues(null, 0);
|
|
237
271
|
this.solveConstraints();
|
|
238
|
-
this.
|
|
272
|
+
this.sliderAttachments();
|
|
239
273
|
}
|
|
240
274
|
|
|
241
275
|
/** The animation posed at its time `t`, from the setup pose (the header). */
|
|
@@ -255,9 +289,11 @@ export class RuntimePose {
|
|
|
255
289
|
this.time = t;
|
|
256
290
|
this.boneTimelines(anim, t);
|
|
257
291
|
this.hierarchy();
|
|
292
|
+
this.slotValues(anim, t);
|
|
293
|
+
this.deformValues(anim, t);
|
|
258
294
|
this.constraintValues(anim, t);
|
|
259
295
|
this.solveConstraints();
|
|
260
|
-
this.
|
|
296
|
+
this.sliderAttachments();
|
|
261
297
|
}
|
|
262
298
|
|
|
263
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). */
|
|
@@ -496,6 +532,37 @@ export class RuntimePose {
|
|
|
496
532
|
}
|
|
497
533
|
this.sliderTime[k] = time;
|
|
498
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;
|
|
499
566
|
}
|
|
500
567
|
}
|
|
501
568
|
}
|
|
@@ -503,12 +570,15 @@ export class RuntimePose {
|
|
|
503
570
|
/** Every applied constraint in the document's order, each followed by the update order's repose (`applyConstraints`). */
|
|
504
571
|
private solveConstraints(): void {
|
|
505
572
|
const cs = this.model.constraints;
|
|
573
|
+
const hasPath = this.model.hasPath;
|
|
506
574
|
for (let k = 0; k < cs.length; k++) {
|
|
575
|
+
if (hasPath) this.snapSlotBones(k, false);
|
|
507
576
|
const c = cs[k];
|
|
508
577
|
if (!c.applied) continue;
|
|
509
578
|
if (c.kind === 'ik') this.solveIk(c, k);
|
|
510
579
|
else if (c.kind === 'transform') this.solveTransform(c, k);
|
|
511
580
|
else if (c.kind === 'slider') this.solveSlider(c, k);
|
|
581
|
+
else if (c.kind === 'path') this.solvePath(c, k);
|
|
512
582
|
else if (this.physicsPhase !== 'none' && !this.model.omitPhysics) {
|
|
513
583
|
const b = c.bones[0];
|
|
514
584
|
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 +586,18 @@ export class RuntimePose {
|
|
|
516
586
|
this.repose(1, 1);
|
|
517
587
|
}
|
|
518
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;
|
|
519
601
|
}
|
|
520
602
|
}
|
|
521
603
|
|
|
@@ -533,19 +615,54 @@ export class RuntimePose {
|
|
|
533
615
|
this.physicsReady = true;
|
|
534
616
|
}
|
|
535
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
|
+
|
|
536
649
|
/** External motion changes the previous origin, never the authored locals. */
|
|
537
|
-
physicsTranslate(x: number, y: number): void {
|
|
650
|
+
physicsTranslate(x: number, y: number, scope?: readonly string[]): void {
|
|
651
|
+
const selected = this.checkPhysicsScope(scope);
|
|
538
652
|
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new RuntimeInputError('physicsTranslate: x and y must be finite');
|
|
539
653
|
for (let k = 0; k < this.physicsStates.length; k++) {
|
|
654
|
+
if (selected !== null && selected[k] === 0) continue;
|
|
540
655
|
const s = this.physicsStates[k];
|
|
541
656
|
s.ux -= x; s.uy -= y; s.cx -= x; s.cy -= y;
|
|
542
657
|
}
|
|
543
658
|
}
|
|
544
659
|
|
|
545
|
-
physicsRotate(x: number, y: number, degrees: number): void {
|
|
660
|
+
physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void {
|
|
661
|
+
const selected = this.checkPhysicsScope(scope);
|
|
546
662
|
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(degrees)) throw new RuntimeInputError('physicsRotate: x, y and degrees must be finite');
|
|
547
663
|
const r = degrees * RAD, cos = Math.cos(r), sin = Math.sin(r);
|
|
548
664
|
for (let k = 0; k < this.physicsStates.length; k++) {
|
|
665
|
+
if (selected !== null && selected[k] === 0) continue;
|
|
549
666
|
const s = this.physicsStates[k];
|
|
550
667
|
const dx = s.cx - x, dy = s.cy - y;
|
|
551
668
|
const tx = dx * cos - dy * sin - dx, ty = dx * sin + dy * cos - dy;
|
|
@@ -662,9 +779,394 @@ export class RuntimePose {
|
|
|
662
779
|
if (c.bones.length > 0) this.repose(c.bones.length, 0);
|
|
663
780
|
}
|
|
664
781
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
this.
|
|
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 {
|
|
668
1170
|
for (let k = 0; k < this.model.constraints.length; k++) {
|
|
669
1171
|
this.sliderSlots(k);
|
|
670
1172
|
this.sliderDeforms(k);
|
|
@@ -1388,3 +1890,43 @@ function applyWorld(w: Float64Array, property: number, v: number, mix: number):
|
|
|
1388
1890
|
}
|
|
1389
1891
|
}
|
|
1390
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
|
+
}
|