spine-rigc 1.9.1 → 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 +39 -17
- package/docs/AUTHORING.md +1 -1
- package/package.json +1 -1
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/index.ts +21 -2
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/bonedist.ts +18 -6
- 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/nonfinite.ts +54 -0
- package/src/render.ts +151 -86
- package/src/render_core.ts +5 -0
- package/src/validate.ts +432 -457
|
@@ -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
|
/**
|
|
@@ -431,6 +431,16 @@ export interface PhysicsTimelinePlant {
|
|
|
431
431
|
search?: (keys: readonly CoreKey[], t: number) => number;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
/**
|
|
435
|
+
* The records a physics timeline that names no constraint (`*`) writes: every
|
|
436
|
+
* active one whose `…Global` flag for `kind` is on, and for `reset` every
|
|
437
|
+
* active one (the header's *The timelines*), in the order given. `posedPhysics`
|
|
438
|
+
* asks it, and so does the additive probe (`./additive.ts`, issue #1025).
|
|
439
|
+
*/
|
|
440
|
+
export function unnamedPhysicsTargets<R extends CorePhysicsRecord>(records: readonly R[], kind: PhysicsTimelineKind, active: (r: CorePhysicsRecord) => boolean): R[] {
|
|
441
|
+
return records.filter((r) => active(r) && (kind === 'reset' || r.global[kind]));
|
|
442
|
+
}
|
|
443
|
+
|
|
434
444
|
/**
|
|
435
445
|
* Every physics record posed by the animation's physics timelines at `t`, in
|
|
436
446
|
* the timelines' order (the header's *The timelines*), and the names of the
|
|
@@ -438,18 +448,16 @@ export interface PhysicsTimelinePlant {
|
|
|
438
448
|
* `last` to `t` (`resetCrossed`). `active`
|
|
439
449
|
* says whether a constraint is applied at all (its skin and its bone).
|
|
440
450
|
*/
|
|
441
|
-
export function posedPhysics(records: readonly CorePhysicsRecord[], timelines: readonly CorePhysicsTimeline[], t: number, last: number, active: (r: CorePhysicsRecord) => boolean, plant: PhysicsTimelinePlant = {}): { records: Map<string, CorePhysicsRecord>; reset: Set<string> } {
|
|
451
|
+
export function posedPhysics(records: readonly CorePhysicsRecord[], timelines: readonly CorePhysicsTimeline[], t: number, last: number, active: (r: CorePhysicsRecord) => boolean, plant: PhysicsTimelinePlant = {}, wrapped = false): { records: Map<string, CorePhysicsRecord>; reset: Set<string> } {
|
|
442
452
|
const search = plant.search ?? keyIndexAt;
|
|
443
453
|
const channel = plant.channel ?? channelAt;
|
|
444
454
|
const setupOf = new Map(records.map((r) => [r.name, r]));
|
|
445
455
|
const posed = new Map(records.map((r) => [r.name, { ...r }]));
|
|
446
456
|
const reset = new Set<string>();
|
|
447
457
|
for (const tl of timelines) {
|
|
448
|
-
const targets = tl.name === EVERY_GLOBAL_PHYSICS
|
|
449
|
-
? [...posed.values()].filter((r) => active(r) && (tl.kind === 'reset' || r.global[tl.kind]))
|
|
450
|
-
: [posed.get(tl.name) as CorePhysicsRecord].filter((r) => active(r));
|
|
458
|
+
const targets = tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets([...posed.values()], tl.kind, active) : [posed.get(tl.name) as CorePhysicsRecord].filter((r) => active(r));
|
|
451
459
|
if (tl.kind === 'reset') {
|
|
452
|
-
if (resetCrossed(tl.keys, last, t)) for (const r of targets) reset.add(r.name);
|
|
460
|
+
if (resetCrossed(tl.keys, last, t, wrapped)) for (const r of targets) reset.add(r.name);
|
|
453
461
|
continue;
|
|
454
462
|
}
|
|
455
463
|
const i = search(tl.keys, t);
|
|
@@ -473,9 +481,18 @@ export function posedPhysics(records: readonly CorePhysicsRecord[], timelines: r
|
|
|
473
481
|
* issue #960): exactly when a key lies in `(last, t]` — so a key is crossed
|
|
474
482
|
* once, by the step that reaches it, and `last` is −1 before the walk's
|
|
475
483
|
* first apply, where a key at 0 is crossed.
|
|
484
|
+
*
|
|
485
|
+
* `wrapped` is a looping track's step whose animation time went DOWN (issue
|
|
486
|
+
* #1025, `./walk.ts`): the key crossed is then one after `last` or one at or
|
|
487
|
+
* before `t` — `(last, ∞)` and `(−1, t]` — measured against spine-core's
|
|
488
|
+
* `PhysicsConstraint.reset` calls on A10's walk. A step that wraps the track
|
|
489
|
+
* one or more times while its animation time still rises is not `wrapped`:
|
|
490
|
+
* the runtime fired only `(last, t]` there (a key at 0.25 of a 0.3 s
|
|
491
|
+
* animation stepped by 0.7 s, crossed by the track twice between two applies
|
|
492
|
+
* at 0.1 and 0.2, fired on neither).
|
|
476
493
|
*/
|
|
477
|
-
export function resetCrossed(keys: readonly CoreKey[], last: number, t: number): boolean {
|
|
478
|
-
return keys.some((k) => last < k.time && k.time <= t);
|
|
494
|
+
export function resetCrossed(keys: readonly CoreKey[], last: number, t: number, wrapped = false): boolean {
|
|
495
|
+
return wrapped ? keys.some((k) => k.time > last || k.time <= t) : keys.some((k) => last < k.time && k.time <= t);
|
|
479
496
|
}
|
|
480
497
|
|
|
481
498
|
// ---------------------------------------------------------------------------
|
|
@@ -546,6 +563,12 @@ export interface PhysicsStepContext {
|
|
|
546
563
|
* to the step's time (`resetCrossed`).
|
|
547
564
|
*/
|
|
548
565
|
last: number;
|
|
566
|
+
/**
|
|
567
|
+
* The walk's track loops (`./walk.ts`, issue #1025): a step whose animation
|
|
568
|
+
* time went down from `last` wrapped, and crosses the keys `resetCrossed`
|
|
569
|
+
* names for that. Absent on every walk but the looping one.
|
|
570
|
+
*/
|
|
571
|
+
loop?: boolean;
|
|
549
572
|
}
|
|
550
573
|
|
|
551
574
|
/** A constraint's state in `ctx`, created fresh on its first update. */
|
|
@@ -788,7 +811,7 @@ export function physicsRows(records: readonly CorePhysicsRecord[], round: (v: nu
|
|
|
788
811
|
*/
|
|
789
812
|
export function stepPhysicsRecords(records: readonly CoreConstraintRecord[], keyed: readonly CorePhysicsTimeline[], t: number, active: ReadonlySet<string>, ctx: PhysicsStepContext, before: number): CoreConstraintRecord[] {
|
|
790
813
|
const physics = records.filter((r): r is CorePhysicsRecord => r.kind === 'physics');
|
|
791
|
-
const posed = posedPhysics(physics, keyed, t, ctx.last, (r) => physicsActive(r, active));
|
|
814
|
+
const posed = posedPhysics(physics, keyed, t, ctx.last, (r) => physicsActive(r, active), {}, ctx.loop === true && t < ctx.last);
|
|
792
815
|
ctx.last = t;
|
|
793
816
|
for (const name of posed.reset) resetPhysicsState(physicsState(ctx, name), before);
|
|
794
817
|
return records.map((r) => (r.kind === 'physics' ? (posed.records.get(r.name) as CorePhysicsRecord) : r));
|