spine-rigc 1.9.0 → 1.9.2
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/cli.ts +48 -17
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/bone_timelines.ts +56 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +34 -16
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/index.ts +83 -2
- package/src/assertions/model/linked_meshes.ts +2 -2
- package/src/assertions/model/mesh_attachments.ts +1 -1
- package/src/assertions/model/region_joins.ts +20 -12
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +18 -8
- package/src/assertions/model/skin_members.ts +5 -3
- package/src/assertions/model/skin_meshes.ts +1 -1
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/slot_colour.ts +5 -4
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +9 -8
- package/src/bonedist.ts +18 -6
- package/src/compile.ts +114 -14
- package/src/core/additive.ts +468 -0
- package/src/core/constraints.ts +12 -2
- package/src/core/constraints_physics.ts +31 -8
- package/src/core/raw.ts +77 -16
- package/src/core/walk.ts +202 -0
- package/src/deformmeasure.ts +35 -2270
- package/src/deformstructure.ts +60 -18
- package/src/deformsurvey.ts +2295 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +200 -59
- package/src/render_core.ts +5 -0
- package/src/validate.ts +791 -1558
package/src/compile.ts
CHANGED
|
@@ -318,9 +318,24 @@ const FRAME = 1 / 60;
|
|
|
318
318
|
* returns, so the refusal below is raised there, inside `compile`, in these
|
|
319
319
|
* words. Exported so the selftest hands it to the emitter the way the assembly
|
|
320
320
|
* does (`MA03`, `MA08`).
|
|
321
|
+
*
|
|
322
|
+
* 🔢 **It returns the order the FILE keys, which is the comparator's order
|
|
323
|
+
* with every integer-like name first** (issue #1034). `animations` is a JSON
|
|
324
|
+
* object, and an object lists a key that is an array index (`5`, `10` — not
|
|
325
|
+
* `01`, `-a` or `2b`) before every other key, in ascending order, whatever
|
|
326
|
+
* order it was filled in. Measured on a probe keying `5, -a, 10, 2b, 01`: the
|
|
327
|
+
* comparator orders `-a, 01, 2b, 5, 10`, and `skeleton.json`, spine-core's
|
|
328
|
+
* `animations` and the `/3` document's `editorOrder` all list
|
|
329
|
+
* `5, 10, -a, 01, 2b`. The emitter keys the object either way, so no byte
|
|
330
|
+
* moves; what moves is every READER of a `rigc-compiled/2` or `/1` document,
|
|
331
|
+
* which derives the file's order by calling this function and was handed the
|
|
332
|
+
* comparator's list (`fileAnimationOrder` below). The comparator's own list is
|
|
333
|
+
* `editorNamesInOrder(names, 'animations')`. Skins are an array and keep the
|
|
334
|
+
* comparator's order (`editorSkinOrder`), and a skin's slot keys come back as
|
|
335
|
+
* an object's keys (`editorSlotKeyOrder`), which are the file's already.
|
|
321
336
|
*/
|
|
322
337
|
export function editorAnimationOrder(names: readonly string[]): string[] {
|
|
323
|
-
return editorNamesInOrder(names, 'animations');
|
|
338
|
+
return keyedOrder(editorNamesInOrder(names, 'animations'));
|
|
324
339
|
}
|
|
325
340
|
|
|
326
341
|
/**
|
|
@@ -3514,17 +3529,12 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3514
3529
|
// applies them, so a name pair the editor could key two ways is refused here
|
|
3515
3530
|
// in the order it was refused at emission before this entry existed.
|
|
3516
3531
|
//
|
|
3517
|
-
// ⚠️ A name list the file KEYS is
|
|
3518
|
-
//
|
|
3519
|
-
//
|
|
3520
|
-
//
|
|
3521
|
-
//
|
|
3522
|
-
|
|
3523
|
-
// sort's.
|
|
3524
|
-
const editorOrder: ModelEditorOrder = {
|
|
3525
|
-
skins: EDITOR_ORDERS.skins(skins).map((skin) => ({ name: skin.name, slots: Object.keys(EDITOR_ORDERS.slotKeys(skin.attachments)) })),
|
|
3526
|
-
animations: keyedOrder(EDITOR_ORDERS.animations([...animations.keys()])),
|
|
3527
|
-
};
|
|
3532
|
+
// ⚠️ A name list the file KEYS is the keys of an object filled in that
|
|
3533
|
+
// order: an object lists an integer-like key ("2", "10") before every other
|
|
3534
|
+
// key. Since issue #1034 that is `editorAnimationOrder`'s own result rather
|
|
3535
|
+
// than a step applied here, so a reader deriving the order of a `/2` document
|
|
3536
|
+
// (`fileAnimationOrder`) is handed the same list this states.
|
|
3537
|
+
const editorOrder: ModelEditorOrder = editorOrderOver(skins, [...animations.keys()]);
|
|
3528
3538
|
|
|
3529
3539
|
for (const slot of slots) {
|
|
3530
3540
|
if (!boneNames.has(slot.bone)) throw new CompileError(`slot "${slot.name}" has no bone`);
|
|
@@ -3559,13 +3569,103 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3559
3569
|
*/
|
|
3560
3570
|
const EDITOR_ORDERS = { skins: editorSkinOrder, slotKeys: editorSlotKeyOrder, animations: editorAnimationOrder } as const;
|
|
3561
3571
|
|
|
3562
|
-
/**
|
|
3563
|
-
|
|
3572
|
+
/**
|
|
3573
|
+
* The order an object filled with `names`, in that order, lists its keys in —
|
|
3574
|
+
* the order a JSON object keyed by them is spelled in, and the order
|
|
3575
|
+
* spine-core reads it in: every array-index name (`5`, `10`; not `01`, `-a`,
|
|
3576
|
+
* `2b`) first, ascending, then the rest as filled (issue #1034).
|
|
3577
|
+
*
|
|
3578
|
+
* ⭐ It is the rule for EVERY object the Spine file keys by a name, not only
|
|
3579
|
+
* `animations`: the emitter fills each such object in the model's order — an
|
|
3580
|
+
* animation's `slots`, `bones`, `ik`, `transform`, `path`, `physics` and
|
|
3581
|
+
* `slider` groups and its `attachments` at all three levels (skin, slot,
|
|
3582
|
+
* attachment) — so a reader walking one of them in the file's order walks
|
|
3583
|
+
* this over the model's order (`src/deformstructure.ts`'s deform timelines,
|
|
3584
|
+
* `src/assertions/model/constraints.ts`'s constraint timelines). Measured on a
|
|
3585
|
+
* build whose `ik` group keys `leg_f_ik` then `5`: the file and spine-core list
|
|
3586
|
+
* `5, leg_f_ik`.
|
|
3587
|
+
*/
|
|
3588
|
+
export function keyedOrder(names: readonly string[]): string[] {
|
|
3564
3589
|
const keyed: Record<string, true> = {};
|
|
3565
3590
|
for (const name of names) keyed[name] = true;
|
|
3566
3591
|
return Object.keys(keyed);
|
|
3567
3592
|
}
|
|
3568
3593
|
|
|
3594
|
+
/** A skin as far as its order reads it: its name and its table, keyed by slot. */
|
|
3595
|
+
interface OrderedSkin {
|
|
3596
|
+
name: string;
|
|
3597
|
+
attachments: Readonly<Record<string, unknown>>;
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
/**
|
|
3601
|
+
* The orders the Spine file lists `skins`, each skin's slot keys and
|
|
3602
|
+
* `animations` in, over a model's skins and animation names: `EDITOR_ORDERS`,
|
|
3603
|
+
* the functions `compile` hands the emitter, so this is the emitted order by
|
|
3604
|
+
* construction rather than a second statement of it (issue #1034). `compileModel`
|
|
3605
|
+
* states it in the model (`editorOrder`); `fileAnimationOrder` and `fileSkinOrder` derive it for a
|
|
3606
|
+
* document that does not state it. A name set the editor could key two ways is
|
|
3607
|
+
* refused here by name, as at emission.
|
|
3608
|
+
*/
|
|
3609
|
+
export function editorOrderOver(skins: readonly OrderedSkin[], animations: readonly string[]): ModelEditorOrder {
|
|
3610
|
+
return { skins: editorSkinsOver(skins), animations: EDITOR_ORDERS.animations(animations) };
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
/** `editorOrderOver`'s skins alone: each skin's name and slot keys, in the file's order. */
|
|
3614
|
+
function editorSkinsOver(skins: readonly OrderedSkin[]): ModelEditorOrder['skins'] {
|
|
3615
|
+
return EDITOR_ORDERS.skins(skins).map((skin) => ({ name: skin.name, slots: Object.keys(EDITOR_ORDERS.slotKeys(skin.attachments)) }));
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
/** What a reader of a model document holds that the file's orders are read or derived from: `CompiledDocument`'s fields. */
|
|
3619
|
+
export interface OrderedDocument {
|
|
3620
|
+
skins: readonly OrderedSkin[];
|
|
3621
|
+
animations: readonly { name: string }[];
|
|
3622
|
+
stated: { editorOrder: ModelEditorOrder } | null;
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
/**
|
|
3626
|
+
* The order the Spine file lists a model document's animations in — the one
|
|
3627
|
+
* place a reader of the document gets it (issue #1034), with
|
|
3628
|
+
* `fileSkinOrder` its sibling for the skins and their slot keys.
|
|
3629
|
+
*
|
|
3630
|
+
* - A `rigc-compiled/3` document STATES it (`editorOrder`, issue #1026), and
|
|
3631
|
+
* that statement is the one source: nothing derives it beside the document.
|
|
3632
|
+
* - A `/2` or `/1` document does not, and it is derived by the emitter's own
|
|
3633
|
+
* rule over the document's names (`EDITOR_ORDERS`, as `editorOrderOver`
|
|
3634
|
+
* runs it) — the file's order for every name, integer-like ones included
|
|
3635
|
+
* (`editorAnimationOrder`'s 🔢). A name set the editor could key two ways is
|
|
3636
|
+
* refused by name (`CompileError`), as at emission.
|
|
3637
|
+
*/
|
|
3638
|
+
export function fileAnimationOrder(doc: OrderedDocument): readonly string[] {
|
|
3639
|
+
return doc.stated !== null ? doc.stated.editorOrder.animations : EDITOR_ORDERS.animations(doc.animations.map((a) => a.name));
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
/** `fileAnimationOrder`'s sibling: the skins, each with its slot keys, in the order the Spine file lists them — stated by a `/3` document, derived for a `/2` or `/1` one. */
|
|
3643
|
+
export function fileSkinOrder(doc: OrderedDocument): ModelEditorOrder['skins'] {
|
|
3644
|
+
return doc.stated !== null ? doc.stated.editorOrder.skins : editorSkinsOver(doc.skins);
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
/**
|
|
3648
|
+
* `skins` in the order `order` lists them (`fileSkinOrder`), each
|
|
3649
|
+
* with its slot keys in that order. The k-th entry of a name is the k-th skin
|
|
3650
|
+
* of that name, so two skins answering to one name — a defect another check
|
|
3651
|
+
* names — are neither merged nor dropped. A skin `order` does not list, or an
|
|
3652
|
+
* entry naming no skin left, is this caller's defect (`readModel` refuses an
|
|
3653
|
+
* `editorOrder` that is not a permutation of the document's skins), so it
|
|
3654
|
+
* throws rather than walking a guess.
|
|
3655
|
+
*/
|
|
3656
|
+
export function skinsInFileOrder<T extends { name: string }>(skins: readonly T[], order: ModelEditorOrder['skins']): Array<{ skin: T; slots: readonly string[] }> {
|
|
3657
|
+
const left = [...skins];
|
|
3658
|
+
const out: Array<{ skin: T; slots: readonly string[] }> = [];
|
|
3659
|
+
for (const entry of order) {
|
|
3660
|
+
const at = left.findIndex((skin) => skin.name === entry.name);
|
|
3661
|
+
if (at < 0) throw new Error(`internal: the file's skin order names skin "${entry.name}", which no skin left in the walk answers to`);
|
|
3662
|
+
out.push({ skin: left[at], slots: entry.slots });
|
|
3663
|
+
left.splice(at, 1);
|
|
3664
|
+
}
|
|
3665
|
+
if (left.length > 0) throw new Error(`internal: the file's skin order leaves out skin(s) ${left.map((skin) => `"${skin.name}"`).join(', ')}`);
|
|
3666
|
+
return out;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3569
3669
|
/**
|
|
3570
3670
|
* The fields `CompileResult` carries from the model, in the order it has
|
|
3571
3671
|
* always carried them (`CarriedFromCompileResult`), by reference.
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one timeline does when it is applied additively — the question A40
|
|
3
|
+
* asks of the timeline a later slider shares with an earlier one (issue
|
|
4
|
+
* #1025, cut 4c-5 of step 4c of #380). The validator asks the runtime by
|
|
5
|
+
* posing (`timelineAddBehaviour` in `src/validate.ts`): the timeline applied
|
|
6
|
+
* with `add` set, at alpha 1, `MixFrom.current`, no events, TWICE, at every
|
|
7
|
+
* time its own keys name, the midpoints between them and one past the last,
|
|
8
|
+
* from two start states — the setup pose, and the setup pose with every number
|
|
9
|
+
* a pose holds moved by `0.375`, every slot showing nothing and the draw order
|
|
10
|
+
* turned by one — under no skin and under every skin. A second application
|
|
11
|
+
* that moves the pose again is `accumulates`; one that moved it the first time
|
|
12
|
+
* and not the second is `overwrites`; one that never moves it is `inert`; the
|
|
13
|
+
* strongest any cell reads wins. This module answers the same question from
|
|
14
|
+
* the model document, with the core's own readings.
|
|
15
|
+
*
|
|
16
|
+
* ## The class is not a function of the timeline's kind alone
|
|
17
|
+
*
|
|
18
|
+
* Measured first, by posing the runtime's probe over a seeded population that
|
|
19
|
+
* reaches every timeline the catalogue lists (`src/timelines.ts`), each in
|
|
20
|
+
* three value variants — keys away from the setup, keys stating the setup
|
|
21
|
+
* values, keys stating the identity (0, scale 1, white, no draw-order move) —
|
|
22
|
+
* under no skin, the default skin and a second one, with targets active under
|
|
23
|
+
* one skin only (234 timelines, the core suite's `CO27`): of the 36
|
|
24
|
+
* spellings, 18 read one class on every timeline and 18 did not — every
|
|
25
|
+
* adding kind read `accumulates` on some timelines and `inert` on others, and
|
|
26
|
+
* a deform read all three. The kind decides what an application CAN do; three
|
|
27
|
+
* facts of the rig decide what it does:
|
|
28
|
+
*
|
|
29
|
+
* - **Whether its target is active under some skin.** A timeline whose bone,
|
|
30
|
+
* slot bone or constraint is inactive under every view writes nothing:
|
|
31
|
+
* `inert`, whatever its kind. The rules are the core's own — `activeBones`,
|
|
32
|
+
* `slotTimelinesApply`, `constraintInactiveWhy` (an ik is gated on its
|
|
33
|
+
* target, a transform on its source, a path on its slot's bone, a physics
|
|
34
|
+
* constraint on its bone, a slider on its dial) — measured equal here on an
|
|
35
|
+
* ik over a skin-required bone (applied) and onto one (not applied).
|
|
36
|
+
* - **Whether a deform or sequence timeline's attachment is shown.** Both
|
|
37
|
+
* are gated on the slot showing the record they are keyed on, or a linked
|
|
38
|
+
* mesh that plays its timelines (`timelineIdentity`); from the displaced
|
|
39
|
+
* state the slot shows nothing, so only the setup state can be written. A
|
|
40
|
+
* deform keyed on a record no view shows at setup is `inert`; a deform
|
|
41
|
+
* whose every offset is 0 fills the empty deform array once and adds 0 the
|
|
42
|
+
* second time — `overwrites`, not `accumulates`.
|
|
43
|
+
* - **Whether the value it adds is 0.** The ten bone value timelines, the
|
|
44
|
+
* transform mixes, a path's position and mix, physics wind and gravity and
|
|
45
|
+
* a slider's time and mix ADD: `current + v` (a scale `current + (v·setup −
|
|
46
|
+
* setup)`, the setup's and not the current's — a displaced `scaleX` 1.875
|
|
47
|
+
* over setup 1.5 keyed 3 read 4.875). Keyed to 0 (a scale to 1) they write
|
|
48
|
+
* nothing at all: `inert`.
|
|
49
|
+
*
|
|
50
|
+
* So the class is a computation over the document, and the table below
|
|
51
|
+
* carries only what the kind contributes — what its application does when it
|
|
52
|
+
* writes, the runtime class the timeline loads as and the properties it
|
|
53
|
+
* registers. The rest is computed per cell, as the probe reads it.
|
|
54
|
+
*
|
|
55
|
+
* ## The cells, per kind
|
|
56
|
+
*
|
|
57
|
+
* - `adds`: each field `once = current + d`, `twice = once + d`, `d` the
|
|
58
|
+
* keyed value at the time (`channelAt`, the runtime's curve as the core
|
|
59
|
+
* reproduces it), a scale's `v·setup − setup`; `current` the setup value,
|
|
60
|
+
* or the setup value plus `0.375`. A deform: the setup state holds no
|
|
61
|
+
* deform, `once` is the keyed array (`deformAt`) and `twice` the core's
|
|
62
|
+
* additive blend of it over itself at alpha 1 (`blendDeform`).
|
|
63
|
+
* - `writes`: the keyed value replaces the current one — a number through the
|
|
64
|
+
* setup blend at alpha 1, `current + (v − current)`, a mass as its inverse; a
|
|
65
|
+
* colour channel the keyed value clamped to [0, 1]; an ik's flags as keyed;
|
|
66
|
+
* an inherit mode, an attachment by the name it resolves to under the view
|
|
67
|
+
* (`shownAttachment`), a draw order (`drawOrderAt`) against the setup order
|
|
68
|
+
* or the order turned by one; a sequence writes its frame wherever its
|
|
69
|
+
* record is shown (the setup pose holds no frame index).
|
|
70
|
+
* - `writes nothing`: an event timeline (a slider fires none) and a physics
|
|
71
|
+
* `reset` (no time passes between the two applications).
|
|
72
|
+
*
|
|
73
|
+
* A physics timeline naming no constraint (`*`) writes every active record
|
|
74
|
+
* whose `…Global` flag for its value is on (`unnamedPhysicsTargets`).
|
|
75
|
+
*
|
|
76
|
+
* ## Held
|
|
77
|
+
*
|
|
78
|
+
* The core suite's `CO27` poses the runtime's probe over the seeded
|
|
79
|
+
* population and holds every cell's letter, and every added field's three
|
|
80
|
+
* values at tolerance 0, to this module's; one row of the table flipped per
|
|
81
|
+
* class turns exactly the timelines using it red.
|
|
82
|
+
*
|
|
83
|
+
* ## Purity
|
|
84
|
+
*
|
|
85
|
+
* As the rest of the core: nothing from the Spine runtime package, nothing
|
|
86
|
+
* from `src/transform.ts`, no clock, no randomness, no I/O.
|
|
87
|
+
*/
|
|
88
|
+
import type { ModelBone, ModelVertices } from '../model.ts';
|
|
89
|
+
import { channelAt, keyIndexAt, SLOT_COLOUR_TIMELINES, type BoneTimelineKind, type CoreKey, type SlotTimelineKind } from './animation.ts';
|
|
90
|
+
import { constraintInactiveWhy, TRANSFORM_MIXES, TRANSFORM_PROPERTIES, type CoreConstraintRecord, type CoreIkKey } from './constraints.ts';
|
|
91
|
+
import { EVERY_GLOBAL_PHYSICS, physicsActive, unnamedPhysicsTargets, type CorePhysicsRecord, type PhysicsTimelineKind } from './constraints_physics.ts';
|
|
92
|
+
import { blendDeform, deformAt, timelineIdentity, type CoreDeformKey, type CoreSequenceKey } from './deform.ts';
|
|
93
|
+
import { drawOrderAt, type CoreDrawOrderKey } from './draw_order.ts';
|
|
94
|
+
import { activeBones, foldInheritMode, readColour, shownAttachment, shownRow, type CompiledDocument, type CoreAttachment } from './index.ts';
|
|
95
|
+
import { slotTimelinesApply } from './skins.ts';
|
|
96
|
+
|
|
97
|
+
/** What a kind's application does when it writes (the header's *The cells, per kind*). */
|
|
98
|
+
export type AdditiveMode = 'adds' | 'writes' | 'writes nothing';
|
|
99
|
+
|
|
100
|
+
/** What a timeline's property ids are followed by: its target's index (and, for an attachment's, the attachment), or nothing. */
|
|
101
|
+
export type AdditiveAddress = 'bone' | 'slot' | 'attachment' | 'constraint' | 'none';
|
|
102
|
+
|
|
103
|
+
/** One timeline spelling: the runtime class it loads as, the properties it registers in order, what its ids carry, and its mode. */
|
|
104
|
+
export interface AdditiveRow {
|
|
105
|
+
runtimeClass: string;
|
|
106
|
+
properties: readonly string[];
|
|
107
|
+
address: AdditiveAddress;
|
|
108
|
+
mode: AdditiveMode;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const row = (runtimeClass: string, properties: readonly string[], address: AdditiveAddress, mode: AdditiveMode): AdditiveRow => ({ runtimeClass, properties, address, mode });
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Every timeline spelling a document's animation can hold, as measured (the
|
|
115
|
+
* header): the class name each loads as and the property ids it registers were
|
|
116
|
+
* read off the loaded timeline objects, and the mode off the probe's cells over
|
|
117
|
+
* the seeded population.
|
|
118
|
+
*/
|
|
119
|
+
export const ADDITIVE_APPLY: Readonly<Record<string, AdditiveRow>> = {
|
|
120
|
+
'bone rotate': row('RotateTimeline', ['rotate'], 'bone', 'adds'),
|
|
121
|
+
'bone translate': row('TranslateTimeline', ['x', 'y'], 'bone', 'adds'),
|
|
122
|
+
'bone translatex': row('TranslateXTimeline', ['x'], 'bone', 'adds'),
|
|
123
|
+
'bone translatey': row('TranslateYTimeline', ['y'], 'bone', 'adds'),
|
|
124
|
+
'bone scale': row('ScaleTimeline', ['scaleX', 'scaleY'], 'bone', 'adds'),
|
|
125
|
+
'bone scalex': row('ScaleXTimeline', ['scaleX'], 'bone', 'adds'),
|
|
126
|
+
'bone scaley': row('ScaleYTimeline', ['scaleY'], 'bone', 'adds'),
|
|
127
|
+
'bone shear': row('ShearTimeline', ['shearX', 'shearY'], 'bone', 'adds'),
|
|
128
|
+
'bone shearx': row('ShearXTimeline', ['shearX'], 'bone', 'adds'),
|
|
129
|
+
'bone sheary': row('ShearYTimeline', ['shearY'], 'bone', 'adds'),
|
|
130
|
+
'bone inherit': row('InheritTimeline', ['inherit'], 'bone', 'writes'),
|
|
131
|
+
'slot attachment': row('AttachmentTimeline', ['attachment'], 'slot', 'writes'),
|
|
132
|
+
'slot rgba': row('RGBATimeline', ['rgb', 'alpha'], 'slot', 'writes'),
|
|
133
|
+
'slot rgb': row('RGBTimeline', ['rgb'], 'slot', 'writes'),
|
|
134
|
+
'slot alpha': row('AlphaTimeline', ['alpha'], 'slot', 'writes'),
|
|
135
|
+
'slot rgba2': row('RGBA2Timeline', ['rgb', 'alpha', 'rgb2'], 'slot', 'writes'),
|
|
136
|
+
'slot rgb2': row('RGB2Timeline', ['rgb', 'rgb2'], 'slot', 'writes'),
|
|
137
|
+
'attachment deform': row('DeformTimeline', ['deform'], 'attachment', 'adds'),
|
|
138
|
+
'attachment sequence': row('SequenceTimeline', ['sequence'], 'attachment', 'writes'),
|
|
139
|
+
ik: row('IkConstraintTimeline', ['ikConstraint'], 'constraint', 'writes'),
|
|
140
|
+
transform: row('TransformConstraintTimeline', ['transformConstraint'], 'constraint', 'adds'),
|
|
141
|
+
'path position': row('PathConstraintPositionTimeline', ['pathConstraintPosition'], 'constraint', 'adds'),
|
|
142
|
+
'path spacing': row('PathConstraintSpacingTimeline', ['pathConstraintSpacing'], 'constraint', 'writes'),
|
|
143
|
+
'path mix': row('PathConstraintMixTimeline', ['pathConstraintMix'], 'constraint', 'adds'),
|
|
144
|
+
'physics inertia': row('PhysicsConstraintInertiaTimeline', ['physicsConstraintInertia'], 'constraint', 'writes'),
|
|
145
|
+
'physics strength': row('PhysicsConstraintStrengthTimeline', ['physicsConstraintStrength'], 'constraint', 'writes'),
|
|
146
|
+
'physics damping': row('PhysicsConstraintDampingTimeline', ['physicsConstraintDamping'], 'constraint', 'writes'),
|
|
147
|
+
'physics mass': row('PhysicsConstraintMassTimeline', ['physicsConstraintMass'], 'constraint', 'writes'),
|
|
148
|
+
'physics wind': row('PhysicsConstraintWindTimeline', ['physicsConstraintWind'], 'constraint', 'adds'),
|
|
149
|
+
'physics gravity': row('PhysicsConstraintGravityTimeline', ['physicsConstraintGravity'], 'constraint', 'adds'),
|
|
150
|
+
'physics mix': row('PhysicsConstraintMixTimeline', ['physicsConstraintMix'], 'constraint', 'writes'),
|
|
151
|
+
// A reset registers its property with no constraint index, named or not (measured: both read `physicsConstraintReset`).
|
|
152
|
+
'physics reset': row('PhysicsConstraintResetTimeline', ['physicsConstraintReset'], 'none', 'writes nothing'),
|
|
153
|
+
'slider time': row('SliderTimeline', ['sliderTime'], 'constraint', 'adds'),
|
|
154
|
+
'slider mix': row('SliderMixTimeline', ['sliderMix'], 'constraint', 'adds'),
|
|
155
|
+
drawOrder: row('DrawOrderTimeline', ['drawOrder'], 'none', 'writes'),
|
|
156
|
+
events: row('EventTimeline', ['event'], 'none', 'writes nothing'),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** One timeline of a document's animation, as the core reads it — enough to pose the probe. */
|
|
160
|
+
export type AdditiveTimeline =
|
|
161
|
+
| { group: 'bone'; word: BoneTimelineKind; bone: string; keys: readonly CoreKey[] }
|
|
162
|
+
| { group: 'slot'; word: SlotTimelineKind; slot: string; keys: readonly CoreKey[] }
|
|
163
|
+
| { group: 'ik'; word: 'ik'; constraint: string; keys: readonly CoreIkKey[] }
|
|
164
|
+
| { group: 'transform'; word: 'transform'; constraint: string; keys: readonly CoreKey[] }
|
|
165
|
+
| { group: 'path'; word: 'position' | 'spacing' | 'mix'; constraint: string; keys: readonly CoreKey[] }
|
|
166
|
+
| { group: 'physics'; word: PhysicsTimelineKind; constraint: string; keys: readonly CoreKey[] }
|
|
167
|
+
| { group: 'slider'; word: 'time' | 'mix'; constraint: string; keys: readonly CoreKey[] }
|
|
168
|
+
| { group: 'attachment'; word: 'deform'; skin: string; slot: string; attachment: string; keys: readonly CoreDeformKey[] }
|
|
169
|
+
| { group: 'attachment'; word: 'sequence'; skin: string; slot: string; attachment: string; keys: readonly CoreSequenceKey[] }
|
|
170
|
+
| { group: 'drawOrder'; word: 'drawOrder'; keys: readonly CoreDrawOrderKey[] }
|
|
171
|
+
| { group: 'events'; word: 'events'; times: readonly number[] };
|
|
172
|
+
|
|
173
|
+
/** The spelling a timeline is a row of. */
|
|
174
|
+
export function additiveSpelling(t: AdditiveTimeline): string {
|
|
175
|
+
if (t.group === 'ik' || t.group === 'transform' || t.group === 'drawOrder' || t.group === 'events') return t.group;
|
|
176
|
+
return `${t.group} ${t.word}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** One view the probe poses under: the name the runtime's skin carries (`(none)` for no skin set) and the document under it. */
|
|
180
|
+
export interface AdditiveView {
|
|
181
|
+
name: string;
|
|
182
|
+
doc: CompiledDocument;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** What the probe moved one field to: before either application, after one, after two — each as the pose prints it. */
|
|
186
|
+
export interface AdditiveValue {
|
|
187
|
+
/** `bone:<name>.<field>`, `constraint:<name>.<field>` or `slot:<name>.deform`, the pose field's own name. */
|
|
188
|
+
field: string;
|
|
189
|
+
t: number;
|
|
190
|
+
before: string;
|
|
191
|
+
once: string;
|
|
192
|
+
twice: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** One view under one start state: the letter the probe reads (`A` a second application moved it, `W` only the first did, `-` nothing), and the values an adding kind moved. */
|
|
196
|
+
export interface AdditiveCell {
|
|
197
|
+
view: string;
|
|
198
|
+
state: 'setup' | 'displaced';
|
|
199
|
+
letter: 'A' | 'W' | '-';
|
|
200
|
+
values: AdditiveValue[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The class the probe reads: the strongest any cell reads. */
|
|
204
|
+
export type AdditiveBehaviour = 'accumulates' | 'overwrites' | 'inert';
|
|
205
|
+
|
|
206
|
+
/** The probe's displacement of every number a pose holds — the validator's `PROBE_DISPLACEMENT`, exact in binary. */
|
|
207
|
+
export const ADDITIVE_DISPLACEMENT = 0.375;
|
|
208
|
+
|
|
209
|
+
/** A plant: rows of the table read otherwise (the core suite's `CO27`). */
|
|
210
|
+
export interface AdditivePlant {
|
|
211
|
+
rows?: Readonly<Record<string, AdditiveRow>>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Every time the probe applies a timeline at: its keys' own times, the midpoints between them, and one past the last. */
|
|
215
|
+
export function additiveTimes(times: readonly number[]): number[] {
|
|
216
|
+
const between: number[] = [];
|
|
217
|
+
for (let i = 1; i < times.length; i++) between.push((times[i - 1] + times[i]) / 2);
|
|
218
|
+
return [...times, ...between, (times.length ? times[times.length - 1] : 0) + 1];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const spell = (v: number): string => String(v);
|
|
222
|
+
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
223
|
+
|
|
224
|
+
/** One field moved by an adding kind: its two sums from `current`. */
|
|
225
|
+
function added(values: AdditiveValue[], field: string, t: number, current: number, d: number): { wrote: boolean; again: boolean } {
|
|
226
|
+
const once = current + d;
|
|
227
|
+
const twice = once + d;
|
|
228
|
+
values.push({ field, t, before: spell(current), once: spell(once), twice: spell(twice) });
|
|
229
|
+
return { wrote: spell(once) !== spell(current), again: spell(twice) !== spell(once) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The keyed channels at `t`, the probe's times all at or after the first key. */
|
|
233
|
+
function channelsAt(keys: readonly CoreKey[], t: number): number[] {
|
|
234
|
+
const i = keyIndexAt(keys, t);
|
|
235
|
+
if (i < 0) return [];
|
|
236
|
+
return keys[i].values.map((_v, c) => channelAt(keys, i, c, t));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** A number written through the setup blend at alpha 1: whether it moves `current`. */
|
|
240
|
+
const blendMoves = (current: number, v: number): boolean => spell(current + (v - current)) !== spell(current);
|
|
241
|
+
|
|
242
|
+
/** The setup value of a bone field. */
|
|
243
|
+
function boneSetup(b: ModelBone, field: 'x' | 'y' | 'rotation' | 'scaleX' | 'scaleY' | 'shearX' | 'shearY'): number {
|
|
244
|
+
return b[field] ?? (field === 'scaleX' || field === 'scaleY' ? 1 : 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const BONE_FIELDS: Readonly<Record<Exclude<BoneTimelineKind, 'inherit'>, ReadonlyArray<'x' | 'y' | 'rotation' | 'scaleX' | 'scaleY' | 'shearX' | 'shearY'>>> = {
|
|
248
|
+
rotate: ['rotation'],
|
|
249
|
+
translate: ['x', 'y'],
|
|
250
|
+
translatex: ['x'],
|
|
251
|
+
translatey: ['y'],
|
|
252
|
+
scale: ['scaleX', 'scaleY'],
|
|
253
|
+
scalex: ['scaleX'],
|
|
254
|
+
scaley: ['scaleY'],
|
|
255
|
+
shear: ['shearX', 'shearY'],
|
|
256
|
+
shearx: ['shearX'],
|
|
257
|
+
sheary: ['shearY'],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/** A constraint's record under a view, by kind and name. */
|
|
261
|
+
function recordOf(view: CompiledDocument, kind: CoreConstraintRecord['kind'], name: string): CoreConstraintRecord | undefined {
|
|
262
|
+
return view.constraints.find((c) => c.kind === kind && c.name === name)?.record;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Whether a constraint's timelines write into its pose under a view whose
|
|
267
|
+
* active bones are `active` — the solver's rule (`constraintInactiveWhy`,
|
|
268
|
+
* `physicsActive`) except for a slider, whose timelines write whatever its dial
|
|
269
|
+
* bone is: measured, a slider dialled by a skin-required bone keyed `mix`
|
|
270
|
+
* accumulated under no skin and the default skin, where the bone is inactive,
|
|
271
|
+
* as under the skin that lists the bone. The solver leaves such a slider
|
|
272
|
+
* unapplied; whether the runtime applies it was not measured here (the
|
|
273
|
+
* header's ⚠️).
|
|
274
|
+
*/
|
|
275
|
+
function applies(r: CoreConstraintRecord, active: ReadonlySet<string>): boolean {
|
|
276
|
+
if (r.kind === 'physics') return physicsActive(r, active);
|
|
277
|
+
const why = constraintInactiveWhy(r, active);
|
|
278
|
+
return why === null || (r.kind === 'slider' && why !== 'skin');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The fields an adding constraint timeline moves, with their setup values. */
|
|
282
|
+
function constraintFields(t: AdditiveTimeline, r: CoreConstraintRecord): Array<{ field: string; setup: number; channel: number }> {
|
|
283
|
+
if (t.group === 'transform' && r.kind === 'transform') return TRANSFORM_PROPERTIES.map((p, channel) => ({ field: TRANSFORM_MIXES[p], setup: r.mixes[p], channel }));
|
|
284
|
+
if (t.group === 'path' && r.kind === 'path') {
|
|
285
|
+
if (t.word === 'position') return [{ field: 'position', setup: r.position, channel: 0 }];
|
|
286
|
+
if (t.word === 'mix') return [{ field: 'mixRotate', setup: r.mixRotate, channel: 0 }, { field: 'mixX', setup: r.mixX, channel: 1 }, { field: 'mixY', setup: r.mixY, channel: 2 }];
|
|
287
|
+
return [{ field: 'spacing', setup: r.spacing, channel: 0 }];
|
|
288
|
+
}
|
|
289
|
+
if (t.group === 'physics' && r.kind === 'physics' && t.word !== 'reset') {
|
|
290
|
+
if (t.word === 'mass') return [{ field: 'massInverse', setup: r.massInverse, channel: 0 }];
|
|
291
|
+
return [{ field: t.word, setup: r[t.word], channel: 0 }];
|
|
292
|
+
}
|
|
293
|
+
if (t.group === 'slider' && r.kind === 'slider') return [{ field: t.word, setup: r[t.word], channel: 0 }];
|
|
294
|
+
if (t.group === 'ik' && r.kind === 'ik') return [{ field: 'mix', setup: r.mix, channel: 0 }, { field: 'softness', setup: r.softness, channel: 1 }];
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The vertices a shown record's deform array is computed on: its own, or a linked mesh's source's. */
|
|
299
|
+
function deformVertices(view: CompiledDocument, record: CoreAttachment): ModelVertices | null {
|
|
300
|
+
const g = record.geometry;
|
|
301
|
+
if (g === undefined) return null;
|
|
302
|
+
const source = g.kind === 'linkedmesh' ? view.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry : g;
|
|
303
|
+
return source !== undefined && source.kind !== 'region' && source.kind !== 'linkedmesh' ? source.vertices : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** One view, one start state: the letter and the values (the header's cells). */
|
|
307
|
+
function cellOf(view: AdditiveView, state: 'setup' | 'displaced', t: AdditiveTimeline, mode: AdditiveMode): AdditiveCell {
|
|
308
|
+
const doc = view.doc;
|
|
309
|
+
const active = activeBones(doc);
|
|
310
|
+
const shift = state === 'setup' ? 0 : ADDITIVE_DISPLACEMENT;
|
|
311
|
+
const values: AdditiveValue[] = [];
|
|
312
|
+
let wrote = false;
|
|
313
|
+
let again = false;
|
|
314
|
+
const note = (r: { wrote: boolean; again: boolean }): void => {
|
|
315
|
+
wrote ||= r.wrote;
|
|
316
|
+
again ||= r.again;
|
|
317
|
+
};
|
|
318
|
+
const times = additiveTimes(t.group === 'events' ? t.times : t.keys.map((k) => k.time));
|
|
319
|
+
const done = (): AdditiveCell => ({ view: view.name, state, letter: again ? 'A' : wrote ? 'W' : '-', values });
|
|
320
|
+
if (mode === 'writes nothing') return done();
|
|
321
|
+
switch (t.group) {
|
|
322
|
+
case 'bone': {
|
|
323
|
+
const b = doc.bones.find((x) => x.name === t.bone);
|
|
324
|
+
if (b === undefined || !active.has(b.name)) return done();
|
|
325
|
+
if (t.word === 'inherit') {
|
|
326
|
+
// The displaced pose holds the mode's number plus 0.375, which no mode is; the setup pose holds the setup mode.
|
|
327
|
+
const setup = foldInheritMode(b.inheritMode ?? 'normal') ?? 'normal';
|
|
328
|
+
for (const time of times) {
|
|
329
|
+
const i = keyIndexAt(t.keys, time);
|
|
330
|
+
if (i < 0) continue;
|
|
331
|
+
if (state === 'displaced' || t.keys[i].mode !== setup) wrote = true;
|
|
332
|
+
}
|
|
333
|
+
return done();
|
|
334
|
+
}
|
|
335
|
+
const fields = BONE_FIELDS[t.word];
|
|
336
|
+
for (const time of times) {
|
|
337
|
+
const v = channelsAt(t.keys, time);
|
|
338
|
+
fields.forEach((field, c) => {
|
|
339
|
+
const setup = boneSetup(b, field);
|
|
340
|
+
const d = mode === 'adds' ? (field === 'scaleX' || field === 'scaleY' ? v[c] * setup - setup : v[c]) : Number.NaN;
|
|
341
|
+
if (mode === 'adds') note(added(values, `bone:${b.name}.${field}`, time, setup + shift, d));
|
|
342
|
+
else note({ wrote: blendMoves(setup + shift, v[c]), again: false });
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return done();
|
|
346
|
+
}
|
|
347
|
+
case 'slot': {
|
|
348
|
+
const slot = doc.slots.find((s) => s.name === t.slot);
|
|
349
|
+
if (slot === undefined || !slotTimelinesApply(doc, slot, active)) return done();
|
|
350
|
+
if (t.word === 'attachment') {
|
|
351
|
+
const nameShown = (placeholder: string | null): string | null => {
|
|
352
|
+
if (placeholder === null) return null;
|
|
353
|
+
const s = shownAttachment(doc, { ...slot, setup: placeholder });
|
|
354
|
+
return s === null || 'conflict' in s ? null : shownRow(s).name;
|
|
355
|
+
};
|
|
356
|
+
// The setup pose shows the setup attachment; the displaced pose shows nothing.
|
|
357
|
+
const current = state === 'setup' ? nameShown(slot.setup) : null;
|
|
358
|
+
for (const time of times) {
|
|
359
|
+
const i = keyIndexAt(t.keys, time);
|
|
360
|
+
if (i >= 0 && nameShown(t.keys[i].name ?? null) !== current) wrote = true;
|
|
361
|
+
}
|
|
362
|
+
return done();
|
|
363
|
+
}
|
|
364
|
+
const shape = SLOT_COLOUR_TIMELINES[t.word];
|
|
365
|
+
const light = readColour(slot.color ?? 'ffffffff');
|
|
366
|
+
const dark = slot.dark === undefined ? null : readColour(slot.dark);
|
|
367
|
+
for (const time of times) {
|
|
368
|
+
const v = channelsAt(t.keys, time);
|
|
369
|
+
shape.channels.forEach((channel: string, c: number) => {
|
|
370
|
+
const second = channel.endsWith('2');
|
|
371
|
+
const at = ['r', 'g', 'b', 'a'].indexOf(second ? channel.slice(0, 1) : channel);
|
|
372
|
+
const setup = second ? (dark === null ? Number.NaN : dark[at]) : light[at];
|
|
373
|
+
if (spell(clamp01(v[c])) !== spell(setup + shift)) wrote = true;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return done();
|
|
377
|
+
}
|
|
378
|
+
case 'attachment': {
|
|
379
|
+
// The displaced pose shows nothing on any slot, and both timelines are gated on what a slot shows.
|
|
380
|
+
if (state === 'displaced') return done();
|
|
381
|
+
const identity = `${t.skin}/${t.slot}/${t.attachment}`;
|
|
382
|
+
for (const slot of doc.slots) {
|
|
383
|
+
if (!slotTimelinesApply(doc, slot, active)) continue;
|
|
384
|
+
const shown = shownAttachment(doc, slot);
|
|
385
|
+
if (shown === null || 'conflict' in shown || timelineIdentity(slot.name, shown) !== identity) continue;
|
|
386
|
+
if (t.word === 'sequence') {
|
|
387
|
+
if (shown.record.sequenceCount !== undefined && t.keys.length > 0) wrote = true;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const vertices = deformVertices(doc, shown.record);
|
|
391
|
+
if (vertices === null) continue;
|
|
392
|
+
for (const time of times) {
|
|
393
|
+
const target = deformAt(vertices, t.keys, time);
|
|
394
|
+
if (target === null) continue;
|
|
395
|
+
// From no deform, the additive blend at alpha 1 is the target (`blendDeform`); a second one adds `target − setup` to it.
|
|
396
|
+
const once = blendDeform(vertices, null, target, 1, true);
|
|
397
|
+
const twice = blendDeform(vertices, once, target, 1, true);
|
|
398
|
+
values.push({ field: `slot:${slot.name}.deform`, t: time, before: '[]', once: `[${once.join(',')}]`, twice: `[${twice.join(',')}]` });
|
|
399
|
+
wrote = true;
|
|
400
|
+
if (once.join(',') !== twice.join(',')) again = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return done();
|
|
404
|
+
}
|
|
405
|
+
case 'drawOrder': {
|
|
406
|
+
const count = doc.slots.length;
|
|
407
|
+
const setup = Array.from({ length: count }, (_v, i) => i);
|
|
408
|
+
const current = state === 'setup' ? setup : [...setup.slice(1), ...setup.slice(0, 1)];
|
|
409
|
+
for (const time of times) {
|
|
410
|
+
const order = drawOrderAt(count, t.keys, time);
|
|
411
|
+
if (order !== null && order.join(',') !== current.join(',')) wrote = true;
|
|
412
|
+
}
|
|
413
|
+
return done();
|
|
414
|
+
}
|
|
415
|
+
case 'events':
|
|
416
|
+
// A kind that writes moves the pose; an event timeline under a slider fires nothing (the table's mode), so only a plant reaches this.
|
|
417
|
+
if (mode === 'writes') wrote = true;
|
|
418
|
+
return done();
|
|
419
|
+
default: {
|
|
420
|
+
const records: CoreConstraintRecord[] =
|
|
421
|
+
t.group === 'physics' && t.constraint === EVERY_GLOBAL_PHYSICS
|
|
422
|
+
? unnamedPhysicsTargets(
|
|
423
|
+
doc.constraints.flatMap((c): CorePhysicsRecord[] => (c.record?.kind === 'physics' ? [c.record] : [])),
|
|
424
|
+
t.word,
|
|
425
|
+
(r) => physicsActive(r, active),
|
|
426
|
+
)
|
|
427
|
+
: [recordOf(doc, t.group, t.constraint)].filter((r): r is CoreConstraintRecord => r !== undefined && applies(r, active));
|
|
428
|
+
for (const r of records) {
|
|
429
|
+
// A kind that writes with no field of its own to compare — only a plant makes a `reset` one — moves the pose wherever it applies.
|
|
430
|
+
if (t.group === 'physics' && t.word === 'reset' && mode === 'writes') wrote = true;
|
|
431
|
+
const fields = constraintFields(t, r);
|
|
432
|
+
for (const time of times) {
|
|
433
|
+
const v = channelsAt(t.keys, time);
|
|
434
|
+
for (const { field, setup, channel } of fields) {
|
|
435
|
+
const current = setup + shift;
|
|
436
|
+
if (mode === 'adds') note(added(values, `constraint:${r.name}.${field}`, time, current, v[channel]));
|
|
437
|
+
// A mass is keyed as a mass and posed as its inverse.
|
|
438
|
+
else if (field === 'massInverse') note({ wrote: spell(1 / (1 / current + (v[channel] - 1 / current))) !== spell(current), again: false });
|
|
439
|
+
else note({ wrote: blendMoves(current, v[channel]), again: false });
|
|
440
|
+
}
|
|
441
|
+
// An ik key's flags: the displaced pose moved the bend direction (a number); the setup pose holds the setup flags.
|
|
442
|
+
if (t.group === 'ik' && r.kind === 'ik') {
|
|
443
|
+
const i = keyIndexAt(t.keys, time);
|
|
444
|
+
const flags = i < 0 ? undefined : t.keys[i].flags;
|
|
445
|
+
if (flags !== undefined && (state === 'displaced' || flags.bendPositive !== r.bendPositive || flags.compress !== r.compress || flags.stretch !== r.stretch)) wrote = true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return done();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Every cell of the probe over `views` — each view under the setup state, then the displaced one. */
|
|
455
|
+
export function additiveCells(views: readonly AdditiveView[], t: AdditiveTimeline, plant: AdditivePlant = {}): AdditiveCell[] {
|
|
456
|
+
const rows = plant.rows ?? ADDITIVE_APPLY;
|
|
457
|
+
const spelled = additiveSpelling(t);
|
|
458
|
+
const r = rows[spelled];
|
|
459
|
+
if (r === undefined) throw new Error(`internal: "${spelled}" is no row of the additive table`);
|
|
460
|
+
return views.flatMap((view) => [cellOf(view, 'setup', t, r.mode), cellOf(view, 'displaced', t, r.mode)]);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** The class the probe reads over its cells: `accumulates` where any cell does, `overwrites` where any cell only wrote, `inert` otherwise. */
|
|
464
|
+
export function additiveBehaviour(cells: readonly AdditiveCell[]): AdditiveBehaviour {
|
|
465
|
+
if (cells.some((c) => c.letter === 'A')) return 'accumulates';
|
|
466
|
+
return cells.some((c) => c.letter === 'W') ? 'overwrites' : 'inert';
|
|
467
|
+
}
|
|
468
|
+
|
package/src/core/constraints.ts
CHANGED
|
@@ -1402,12 +1402,22 @@ function solveTransform(state: SolverState, c: CoreTransformRecord): { changed:
|
|
|
1402
1402
|
|
|
1403
1403
|
/** Why a constraint is not applied under the skin view posed (the header's measured rule), or null when it is. */
|
|
1404
1404
|
function inactiveWhy(state: SolverState, c: CoreConstraintRecord): string | null {
|
|
1405
|
+
return constraintInactiveWhy(c, state.active, state.rules);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Why a constraint is not applied under a skin view whose active bones are
|
|
1410
|
+
* `active` (the header's measured rule), or null when it is — `inactiveWhy`'s
|
|
1411
|
+
* reading, exported so the additive probe (`./additive.ts`, issue #1025) asks
|
|
1412
|
+
* the rule the solver applies rather than a copy of it.
|
|
1413
|
+
*/
|
|
1414
|
+
export function constraintInactiveWhy(c: CoreConstraintRecord, active: ReadonlySet<string>, rules: Readonly<SolverRules> = RUNTIME_SOLVER_RULES): string | null {
|
|
1405
1415
|
// A skin-required constraint of any kind is applied when an applied skin's list for its kind names it (issue #932, card #961; physics first by issue #956).
|
|
1406
1416
|
if (c.skin && !c.listedBySkin) return 'skin';
|
|
1407
1417
|
// A path constraint is active when its slot's bone is (`./constraints_path.ts`, *Which constraints run*); every other kind when every bone it names is.
|
|
1408
1418
|
// A transform constraint is applied when its source is, to every bone it names, inactive ones included (issue #979: an inactive bone among them did not stop it moving the others, and it wrote into the inactive one). An ik naming an inactive bone is left unapplied: the runtime applies it from ancestors it has not brought up to date (the header's *Unposed bones*).
|
|
1409
|
-
const named = c.kind === 'path' ? [c.slotBone] : c.kind === 'ik' ? (
|
|
1410
|
-
return named.every((n) =>
|
|
1419
|
+
const named = c.kind === 'path' ? [c.slotBone] : c.kind === 'ik' ? (rules.ikOverInactiveFresh ? [c.target] : [...c.bones, c.target]) : c.kind === 'transform' ? (rules.transformIgnoresBoneActivity ? [c.source] : [...c.bones, c.source]) : c.kind === 'slider' ? (c.bone === null ? [] : [c.bone]) : [c.bone];
|
|
1420
|
+
return named.every((n) => active.has(n)) ? null : 'inactive bone';
|
|
1411
1421
|
}
|
|
1412
1422
|
|
|
1413
1423
|
/**
|