spine-rigc 1.9.2 → 1.9.4
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 +28 -4397
- package/docs/AUTHORING.md +4 -4
- package/package.json +1 -1
- package/src/assertions/bodies/a06.ts +53 -3
- package/src/assertions/bodies/a13.ts +3 -3
- package/src/assertions/bodies/a14.ts +3 -3
- package/src/assertions/bodies/a15.ts +5 -5
- package/src/assertions/bodies/a19.ts +44 -2
- package/src/assertions/bodies/a22.ts +3 -3
- package/src/assertions/facts/animated_bones.ts +4 -2
- package/src/assertions/facts/mesh_attachments.ts +33 -4
- package/src/assertions/model/animated_bones.ts +13 -5
- package/src/assertions/model/bone_timelines.ts +5 -3
- package/src/assertions/model/declared.ts +127 -0
- package/src/assertions/model/index.ts +63 -40
- package/src/assertions/model/mesh_attachments.ts +31 -2
- package/src/assertions/model/parse.ts +9 -1
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/slider_composition.ts +10 -8
- package/src/assertions/model/stepped_poses.ts +3 -2
- package/src/bonedist.ts +4 -5
- package/src/check.ts +3 -2
- package/src/checkpics.ts +1 -1
- package/src/cli/core_commands.ts +1532 -0
- package/src/cli/shared.ts +1935 -0
- package/src/cli/spine_commands.ts +1141 -0
- package/src/core/additive.ts +52 -62
- package/src/core/constraints.ts +26 -3
- package/src/core/constraints_physics.ts +11 -1
- package/src/core/constraints_slider.ts +136 -6
- package/src/core/index.ts +27 -3
- package/src/core/skins.ts +28 -3
- package/src/core/walk.ts +9 -36
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformmeasure.ts +20 -48
- package/src/deformreport.ts +2 -2
- package/src/framing.ts +1 -1
- package/src/pose.ts +1 -1
- package/src/preview.ts +1 -1
- package/src/region_joins.ts +54 -0
- package/src/render.ts +206 -2836
- package/src/render_core.ts +19 -36
- package/src/render_shared.ts +2887 -0
- package/src/slots.ts +1 -1
- package/src/spine_side.ts +123 -0
- package/src/validate.ts +11 -91
- package/src/assertions/facts/skin_meshes.ts +0 -49
- package/src/assertions/model/skin_meshes.ts +0 -90
package/src/core/additive.ts
CHANGED
|
@@ -49,8 +49,9 @@
|
|
|
49
49
|
*
|
|
50
50
|
* So the class is a computation over the document, and the table below
|
|
51
51
|
* carries only what the kind contributes — what its application does when it
|
|
52
|
-
* writes
|
|
53
|
-
*
|
|
52
|
+
* writes. The rest is computed per cell, as the probe reads it. (What the
|
|
53
|
+
* runtime calls the timeline is the validator's table, `RUNTIME_TIMELINE`,
|
|
54
|
+
* since issue #1054.)
|
|
54
55
|
*
|
|
55
56
|
* ## The cells, per kind
|
|
56
57
|
*
|
|
@@ -97,63 +98,52 @@ import { slotTimelinesApply } from './skins.ts';
|
|
|
97
98
|
/** What a kind's application does when it writes (the header's *The cells, per kind*). */
|
|
98
99
|
export type AdditiveMode = 'adds' | 'writes' | 'writes nothing';
|
|
99
100
|
|
|
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
101
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* the
|
|
102
|
+
* What each timeline spelling's application does when it writes, keyed by the
|
|
103
|
+
* document's own words (`additiveSpelling`) — measured off the probe's cells
|
|
104
|
+
* over the seeded population (the header). What the runtime calls each
|
|
105
|
+
* spelling — the class it loads as and the properties it registers, which A40's
|
|
106
|
+
* sentence prints — is the validator's to know and stands beside it
|
|
107
|
+
* (`RUNTIME_TIMELINE`, `src/assertions/model/runtime_timelines.ts`, issue
|
|
108
|
+
* #1054): this module speaks the document's words only.
|
|
118
109
|
*/
|
|
119
|
-
export const
|
|
120
|
-
'bone rotate':
|
|
121
|
-
'bone translate':
|
|
122
|
-
'bone translatex':
|
|
123
|
-
'bone translatey':
|
|
124
|
-
'bone scale':
|
|
125
|
-
'bone scalex':
|
|
126
|
-
'bone scaley':
|
|
127
|
-
'bone shear':
|
|
128
|
-
'bone shearx':
|
|
129
|
-
'bone sheary':
|
|
130
|
-
'bone inherit':
|
|
131
|
-
'slot attachment':
|
|
132
|
-
'slot rgba':
|
|
133
|
-
'slot rgb':
|
|
134
|
-
'slot alpha':
|
|
135
|
-
'slot rgba2':
|
|
136
|
-
'slot rgb2':
|
|
137
|
-
'attachment deform':
|
|
138
|
-
'attachment sequence':
|
|
139
|
-
ik:
|
|
140
|
-
transform:
|
|
141
|
-
'path position':
|
|
142
|
-
'path spacing':
|
|
143
|
-
'path mix':
|
|
144
|
-
'physics inertia':
|
|
145
|
-
'physics strength':
|
|
146
|
-
'physics damping':
|
|
147
|
-
'physics mass':
|
|
148
|
-
'physics wind':
|
|
149
|
-
'physics gravity':
|
|
150
|
-
'physics mix':
|
|
151
|
-
|
|
152
|
-
'
|
|
153
|
-
'slider
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
events: row('EventTimeline', ['event'], 'none', 'writes nothing'),
|
|
110
|
+
export const ADDITIVE_MODE: Readonly<Record<string, AdditiveMode>> = {
|
|
111
|
+
'bone rotate': 'adds',
|
|
112
|
+
'bone translate': 'adds',
|
|
113
|
+
'bone translatex': 'adds',
|
|
114
|
+
'bone translatey': 'adds',
|
|
115
|
+
'bone scale': 'adds',
|
|
116
|
+
'bone scalex': 'adds',
|
|
117
|
+
'bone scaley': 'adds',
|
|
118
|
+
'bone shear': 'adds',
|
|
119
|
+
'bone shearx': 'adds',
|
|
120
|
+
'bone sheary': 'adds',
|
|
121
|
+
'bone inherit': 'writes',
|
|
122
|
+
'slot attachment': 'writes',
|
|
123
|
+
'slot rgba': 'writes',
|
|
124
|
+
'slot rgb': 'writes',
|
|
125
|
+
'slot alpha': 'writes',
|
|
126
|
+
'slot rgba2': 'writes',
|
|
127
|
+
'slot rgb2': 'writes',
|
|
128
|
+
'attachment deform': 'adds',
|
|
129
|
+
'attachment sequence': 'writes',
|
|
130
|
+
ik: 'writes',
|
|
131
|
+
transform: 'adds',
|
|
132
|
+
'path position': 'adds',
|
|
133
|
+
'path spacing': 'writes',
|
|
134
|
+
'path mix': 'adds',
|
|
135
|
+
'physics inertia': 'writes',
|
|
136
|
+
'physics strength': 'writes',
|
|
137
|
+
'physics damping': 'writes',
|
|
138
|
+
'physics mass': 'writes',
|
|
139
|
+
'physics wind': 'adds',
|
|
140
|
+
'physics gravity': 'adds',
|
|
141
|
+
'physics mix': 'writes',
|
|
142
|
+
'physics reset': 'writes nothing',
|
|
143
|
+
'slider time': 'adds',
|
|
144
|
+
'slider mix': 'adds',
|
|
145
|
+
drawOrder: 'writes',
|
|
146
|
+
events: 'writes nothing',
|
|
157
147
|
};
|
|
158
148
|
|
|
159
149
|
/** One timeline of a document's animation, as the core reads it — enough to pose the probe. */
|
|
@@ -208,7 +198,7 @@ export const ADDITIVE_DISPLACEMENT = 0.375;
|
|
|
208
198
|
|
|
209
199
|
/** A plant: rows of the table read otherwise (the core suite's `CO27`). */
|
|
210
200
|
export interface AdditivePlant {
|
|
211
|
-
|
|
201
|
+
modes?: Readonly<Record<string, AdditiveMode>>;
|
|
212
202
|
}
|
|
213
203
|
|
|
214
204
|
/** Every time the probe applies a timeline at: its keys' own times, the midpoints between them, and one past the last. */
|
|
@@ -453,11 +443,11 @@ function cellOf(view: AdditiveView, state: 'setup' | 'displaced', t: AdditiveTim
|
|
|
453
443
|
|
|
454
444
|
/** Every cell of the probe over `views` — each view under the setup state, then the displaced one. */
|
|
455
445
|
export function additiveCells(views: readonly AdditiveView[], t: AdditiveTimeline, plant: AdditivePlant = {}): AdditiveCell[] {
|
|
456
|
-
const
|
|
446
|
+
const modes = plant.modes ?? ADDITIVE_MODE;
|
|
457
447
|
const spelled = additiveSpelling(t);
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
460
|
-
return views.flatMap((view) => [cellOf(view, 'setup', t,
|
|
448
|
+
const mode = modes[spelled];
|
|
449
|
+
if (mode === undefined) throw new Error(`internal: "${spelled}" is no row of the additive table`);
|
|
450
|
+
return views.flatMap((view) => [cellOf(view, 'setup', t, mode), cellOf(view, 'displaced', t, mode)]);
|
|
461
451
|
}
|
|
462
452
|
|
|
463
453
|
/** The class the probe reads over its cells: `accumulates` where any cell does, `overwrites` where any cell only wrote, `inert` otherwise. */
|
package/src/core/constraints.ts
CHANGED
|
@@ -470,7 +470,7 @@ import { activeBones, type CompiledDocument, type CoreConstraintKind } from './i
|
|
|
470
470
|
import { fillingSkins } from './skins.ts';
|
|
471
471
|
import { readPathTimelines, slotBonePlan, solvePath, type CorePathRecord, type CorePathTimelines, type SlotBoneEvent } from './constraints_path.ts';
|
|
472
472
|
import { physicsTimelineCount, readPhysicsTimelines, stepPhysics, type CorePhysicsRecord, type CorePhysicsTimeline, type PhysicsStepContext } from './constraints_physics.ts';
|
|
473
|
-
import { applySlider, posedSlider, readSliderTimelines, sliderBonesWhy, type CoreSliderRecord, type CoreSliderTimeline, type SliderApplication } from './constraints_slider.ts';
|
|
473
|
+
import { applySlider, posedSlider, sliderPhysicsTarget, readSliderTimelines, sliderBonesWhy, type CoreSliderRecord, type CoreSliderTimeline, type SliderApplication } from './constraints_slider.ts';
|
|
474
474
|
|
|
475
475
|
const DEG = 180 / RUNTIME_PI;
|
|
476
476
|
const RAD = RUNTIME_PI / 180;
|
|
@@ -917,6 +917,20 @@ export interface SolverRules {
|
|
|
917
917
|
sliderAdditiveScaleProduct: boolean;
|
|
918
918
|
/** A slider's scale key at mix exactly 1 writes `setup·v` itself (issue #989), not `from + (setup·v − from)·1`. */
|
|
919
919
|
sliderScaleMixOneIsTarget: boolean;
|
|
920
|
+
/**
|
|
921
|
+
* Issue #1049, each the rule `./constraints_slider.ts` *Physics timelines* states — `false` (or the named reading)
|
|
922
|
+
* plants the reading it rejected: a slider's physics keys write the pass's physics records (`false`: write nothing);
|
|
923
|
+
* a non-additive one blends from the CURRENT value (`false`: from the setup value); only `wind` and `gravity` add
|
|
924
|
+
* (`false`: every value kind adds); a `mass` key blends the mass (`false`: the inverse); the write lasts one pass
|
|
925
|
+
* (`false`: what a slider wrote stands on the next step wherever the step's own animation does not key it); a
|
|
926
|
+
* `reset` key fires nothing (`false`: a key at or before the slider's time resets its constraints on every pass).
|
|
927
|
+
*/
|
|
928
|
+
sliderWritesPhysics: boolean;
|
|
929
|
+
sliderPhysicsFromCurrent: boolean;
|
|
930
|
+
sliderPhysicsAddsWindGravityOnly: boolean;
|
|
931
|
+
sliderPhysicsBlendsMass: boolean;
|
|
932
|
+
sliderPhysicsLastsOnePass: boolean;
|
|
933
|
+
sliderPhysicsResetIsDead: boolean;
|
|
920
934
|
}
|
|
921
935
|
|
|
922
936
|
/** The runtime's rules, as measured. */
|
|
@@ -939,6 +953,12 @@ export const RUNTIME_SOLVER_RULES: Readonly<SolverRules> = {
|
|
|
939
953
|
sliderReposesKeyedBones: true,
|
|
940
954
|
sliderAdditiveScaleProduct: true,
|
|
941
955
|
sliderScaleMixOneIsTarget: true,
|
|
956
|
+
sliderWritesPhysics: true,
|
|
957
|
+
sliderPhysicsFromCurrent: true,
|
|
958
|
+
sliderPhysicsAddsWindGravityOnly: true,
|
|
959
|
+
sliderPhysicsBlendsMass: true,
|
|
960
|
+
sliderPhysicsLastsOnePass: true,
|
|
961
|
+
sliderPhysicsResetIsDead: true,
|
|
942
962
|
};
|
|
943
963
|
|
|
944
964
|
/** The runtime's rules with a plant's over them. */
|
|
@@ -1508,6 +1528,8 @@ export function applyConstraints(bones: readonly ModelBone[], world: ReadonlyMap
|
|
|
1508
1528
|
// An ik over an inactive bone reads the frame above it as the pass left it (issue #979).
|
|
1509
1529
|
const updated = records.some((c) => c.kind === 'ik' && c.bones.some((b) => !active.has(b))) ? frameUpdatedBefore(bones, active, records, skipped) : new Map<number, boolean>();
|
|
1510
1530
|
const snapshots = new Map<number, CoreWorld>();
|
|
1531
|
+
// Issue #1049: under the step a slider's physics timelines write the pass's physics records, which a later physics constraint steps with.
|
|
1532
|
+
const physicsPose = physics === undefined ? undefined : sliderPhysicsTarget(records, active, physics, rules);
|
|
1511
1533
|
const snap = (i: number, when: 'before' | 'after'): void => {
|
|
1512
1534
|
for (const [k, e] of plan) if (e !== null && e.at === i && e.when === when && !(k === i && when === 'before')) snapshots.set(k, { ...(state.world.get((records[k] as CorePathRecord).slotBone) as CoreWorld) });
|
|
1513
1535
|
};
|
|
@@ -1521,14 +1543,15 @@ export function applyConstraints(bones: readonly ModelBone[], world: ReadonlyMap
|
|
|
1521
1543
|
// Under Physics.none a physics constraint applies nothing; stepped, it moves its bone in world space (`./constraints_physics.ts`).
|
|
1522
1544
|
if (physics !== undefined) {
|
|
1523
1545
|
const w = { ...(state.world.get(c.bone) as CoreWorld) };
|
|
1524
|
-
|
|
1546
|
+
const posed = physicsPose?.records.get(c.name) ?? c;
|
|
1547
|
+
if ((physics.step ?? stepPhysics)(posed, w, bone(state, c.bone).length ?? 0, physics)) {
|
|
1525
1548
|
state.world.set(c.bone, w);
|
|
1526
1549
|
changed = [c.bone];
|
|
1527
1550
|
inWorld = [c.bone];
|
|
1528
1551
|
}
|
|
1529
1552
|
}
|
|
1530
1553
|
} else if (c.kind === 'slider') {
|
|
1531
|
-
changed = applySlider(state, c, applied);
|
|
1554
|
+
changed = applySlider(state, c, applied, physicsPose);
|
|
1532
1555
|
} else if (c.kind === 'ik') {
|
|
1533
1556
|
if (c.mix !== 0) {
|
|
1534
1557
|
const frame = inactiveIkFrame(state, c, i, updated);
|
|
@@ -172,7 +172,9 @@
|
|
|
172
172
|
* no value, on each of the seven, read exact — the `mass` one NaN in both
|
|
173
173
|
* dumps). The timeline that
|
|
174
174
|
* names no constraint (`*`) writes every active constraint whose `…Global`
|
|
175
|
-
* flag for that value is on, and resets every active constraint.
|
|
175
|
+
* flag for that value is on, and resets every active constraint. A
|
|
176
|
+
* slider's animation keys them too, at the slider's place in the update
|
|
177
|
+
* order (issue #1049, `./constraints_slider.ts` *Its physics timelines*).
|
|
176
178
|
*
|
|
177
179
|
* **reset.** A `reset` key resets its constraints on the step whose apply
|
|
178
180
|
* crosses it — a key in `(last, t]`, `last` the time the walk last applied
|
|
@@ -569,6 +571,12 @@ export interface PhysicsStepContext {
|
|
|
569
571
|
* names for that. Absent on every walk but the looping one.
|
|
570
572
|
*/
|
|
571
573
|
loop?: boolean;
|
|
574
|
+
/** A plant only (issue #1049, `sliderPhysicsLastsOnePass: false`): what the sliders wrote on the step before, by constraint and field. */
|
|
575
|
+
carried?: Map<string, Partial<Record<'inertia' | 'strength' | 'damping' | 'massInverse' | 'wind' | 'gravity' | 'mix', number>>>;
|
|
576
|
+
/** Each `constraint/field` the step's own animation keys — what the planted carried write gives way to. */
|
|
577
|
+
keyedNow?: Set<string>;
|
|
578
|
+
/** A plant only (issue #1049, `sliderPhysicsFromCurrent: false`): each physics record's setup values, before the step's own animation. */
|
|
579
|
+
setupRecords?: Map<string, CorePhysicsRecord>;
|
|
572
580
|
}
|
|
573
581
|
|
|
574
582
|
/** A constraint's state in `ctx`, created fresh on its first update. */
|
|
@@ -813,6 +821,8 @@ export function stepPhysicsRecords(records: readonly CoreConstraintRecord[], key
|
|
|
813
821
|
const physics = records.filter((r): r is CorePhysicsRecord => r.kind === 'physics');
|
|
814
822
|
const posed = posedPhysics(physics, keyed, t, ctx.last, (r) => physicsActive(r, active), {}, ctx.loop === true && t < ctx.last);
|
|
815
823
|
ctx.last = t;
|
|
824
|
+
ctx.setupRecords = new Map(physics.map((r) => [r.name, r]));
|
|
825
|
+
ctx.keyedNow = new Set(keyed.flatMap((tl) => (tl.kind === 'reset' ? [] : (tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets(physics, tl.kind, (r) => physicsActive(r, active)).map((r) => r.name) : [tl.name]).map((n) => `${n}/${tl.kind === 'mass' ? 'massInverse' : tl.kind}`))));
|
|
816
826
|
for (const name of posed.reset) resetPhysicsState(physicsState(ctx, name), before);
|
|
817
827
|
return records.map((r) => (r.kind === 'physics' ? (posed.records.get(r.name) as CorePhysicsRecord) : r));
|
|
818
828
|
}
|
|
@@ -137,9 +137,52 @@
|
|
|
137
137
|
* animation deforms a curve a path constraint walks (unmeasured). A slider's
|
|
138
138
|
* deform, sequence and draw-order keys are posed since issue #955, after the
|
|
139
139
|
* sample's own (`./deform.ts`, `./draw_order.ts`; they were `sliderAttachmentsWhy`'s
|
|
140
|
-
* absence before).
|
|
141
|
-
*
|
|
142
|
-
*
|
|
140
|
+
* absence before). Its physics timelines are posed since issue #1049 (below),
|
|
141
|
+
* and events a slider does not fire, so neither leaves anything out.
|
|
142
|
+
*
|
|
143
|
+
* ## Its physics timelines (issue #1049)
|
|
144
|
+
*
|
|
145
|
+
* Under `Physics.none` a physics constraint applies nothing
|
|
146
|
+
* (`./constraints_physics.ts`), so neither do a slider's physics keys. Under
|
|
147
|
+
* the step they do, and until issue #1049 the core applied none of them: on
|
|
148
|
+
* the selftest's own builds where a dial's animation keys `wind` or
|
|
149
|
+
* `gravity` of a constraint declared after it, every bone the constraint
|
|
150
|
+
* drives left spine-core from the third step (`tip` worldX
|
|
151
|
+
* 14.87037037037037 against 12.092592592592593 at 0.025 s), and with a slot
|
|
152
|
+
* on that bone `render` drew wrong frames with no refusal. Measured against
|
|
153
|
+
* spine-core 4.3.13's output only — A10's looping walk, `render`'s recipe at
|
|
154
|
+
* 60 and 12 fps and the oracle's stepped grid under `--raw`, on 400 compiled
|
|
155
|
+
* rigs of one or two sliders and one or two physics constraints in random
|
|
156
|
+
* order, every kind keyed, at tolerance 0 — the rule is:
|
|
157
|
+
*
|
|
158
|
+
* - **A slider writes its physics keys into the pass's physics records, at
|
|
159
|
+
* its place in the update order**, at the time it applies its animation at
|
|
160
|
+
* (`at`), after the step's own animation posed them; a physics constraint
|
|
161
|
+
* after it steps with what it wrote, one before it has already stepped.
|
|
162
|
+
* Applying the writes to every constraint of the pass whatever its place
|
|
163
|
+
* read 239 of the 400 rigs off; not applying them, 205.
|
|
164
|
+
* - **A write lasts one pass.** The next step's records start again from the
|
|
165
|
+
* step's own animation: a write that stood where the step's animation does
|
|
166
|
+
* not key read 323 of 400 off.
|
|
167
|
+
* - **Before the timeline's first key it writes nothing**, as for bones.
|
|
168
|
+
* - **`wind`, `gravity` under `additive`: `current + v·mix`.** Every other
|
|
169
|
+
* value (`inertia`, `strength`, `damping`, `mass`, `mix`), and every value
|
|
170
|
+
* of a slider that is not additive: `current + (v − current)·mix`, with
|
|
171
|
+
* `current` the value as the pass holds it (the step's own animation and
|
|
172
|
+
* any slider before). Adding every kind read 104 of 400 off; blending from
|
|
173
|
+
* the setup value (before the step's own animation), 80; writing the key
|
|
174
|
+
* itself at mix exactly 1, 20 — in the last bit.
|
|
175
|
+
* - **A `mass` key blends the mass**, `1 / (m + (v − m)·mix)` with `m =
|
|
176
|
+
* 1 / massInverse`; blending the inverse read 28 of 400 off.
|
|
177
|
+
* - **A `reset` key fires nothing** — the slider applies its animation at one
|
|
178
|
+
* time, from that time, so no key is crossed (the path-slider suite's
|
|
179
|
+
* `PS159` measured the same on the pose); firing a key at or before `at` on
|
|
180
|
+
* every pass read 112 of 400 off.
|
|
181
|
+
* - The timeline naming no constraint (`*`) writes every active constraint
|
|
182
|
+
* whose `…Global` flag for that value is on, as a step's own does.
|
|
183
|
+
*
|
|
184
|
+
* `CO31` holds the rule on the `core_gate` probe rows and a seeded population
|
|
185
|
+
* on all three entries; `CO32` plants each rejected reading (`SolverRules`).
|
|
143
186
|
*
|
|
144
187
|
* ## Purity
|
|
145
188
|
*
|
|
@@ -148,8 +191,9 @@
|
|
|
148
191
|
*/
|
|
149
192
|
import type { ModelBone } from '../model.ts';
|
|
150
193
|
import { channelAt, keyIndexAt, type CoreAnimationTimelines, type CoreCurve, type CoreKey } from './animation.ts';
|
|
151
|
-
import { sourceValue, TRANSFORM_PROPERTIES, type SolverState, type TransformProperty } from './constraints.ts';
|
|
194
|
+
import { sourceValue, TRANSFORM_PROPERTIES, type CoreConstraintRecord, type SolverRules, type SolverState, type TransformProperty } from './constraints.ts';
|
|
152
195
|
import type { CompiledDocument, CoreAnimation } from './index.ts';
|
|
196
|
+
import { EVERY_GLOBAL_PHYSICS, physicsActive, physicsState, resetPhysicsState, unnamedPhysicsTargets, type CorePhysicsRecord, type CorePhysicsTimeline, type PhysicsStepContext } from './constraints_physics.ts';
|
|
153
197
|
|
|
154
198
|
/** The fields a slider's record may carry after `kind`, `name`, `declaredIn` (`buildRigConstraint` in `src/compile.ts`). */
|
|
155
199
|
export const SLIDER_FIELDS = ['animation', 'additive', 'loop', 'mix', 'bone', 'property', 'from', 'to', 'scale', 'max', 'local', 'time', 'skin'] as const;
|
|
@@ -160,6 +204,8 @@ export interface CoreSliderRecord {
|
|
|
160
204
|
name: string;
|
|
161
205
|
animation: string;
|
|
162
206
|
timelines: CoreAnimationTimelines;
|
|
207
|
+
/** Its animation's physics timelines, in the animation's order (issue #1049) — set by `readModel` once the animations' constraint timelines are read, which is after the records. */
|
|
208
|
+
physics: readonly CorePhysicsTimeline[];
|
|
163
209
|
/** The setup bones its animation keys, by name — what a non-additive key's value is added to. */
|
|
164
210
|
setup: ReadonlyMap<string, ModelBone>;
|
|
165
211
|
additive: boolean;
|
|
@@ -252,7 +298,7 @@ export function readSliderRecord(raw: Record<string, unknown>, name: string, whe
|
|
|
252
298
|
if (b !== undefined) setup.set(b.name, b);
|
|
253
299
|
}
|
|
254
300
|
const record: CoreSliderRecord = {
|
|
255
|
-
kind: 'slider', name, animation: anim?.name ?? '', timelines: anim?.timelines ?? { declared: 0, duration: 0, bones: [], slots: [], later: [], attachments: [], drawOrder: [], events: [] }, setup,
|
|
301
|
+
kind: 'slider', name, animation: anim?.name ?? '', timelines: anim?.timelines ?? { declared: 0, duration: 0, bones: [], slots: [], later: [], attachments: [], drawOrder: [], events: [] }, physics: [], setup,
|
|
256
302
|
additive: flag('additive'), loop, mix: num('mix', 1), time: num('time', 0), bone, property,
|
|
257
303
|
from: num('from', 0), to: num('to', 0), scale: num('scale', 1), local: flag('local'), skin: flag('skin'), listedBySkin: false,
|
|
258
304
|
};
|
|
@@ -380,13 +426,14 @@ function valuesAt(keys: readonly CoreKey[], t: number): number[] | null {
|
|
|
380
426
|
* loop to pose again with everything below them. The application is pushed
|
|
381
427
|
* onto `applied` for the slots.
|
|
382
428
|
*/
|
|
383
|
-
export function applySlider(state: SolverState, r: CoreSliderRecord, applied?: SliderApplication[]): string[] {
|
|
429
|
+
export function applySlider(state: SolverState, r: CoreSliderRecord, applied?: SliderApplication[], physics?: SliderPhysicsTarget): string[] {
|
|
384
430
|
if (r.mix === 0) return [];
|
|
385
431
|
const time = sliderTime(state, r);
|
|
386
432
|
const d = r.timelines.duration;
|
|
387
433
|
const at = r.loop && d !== 0 ? time % d : time;
|
|
388
434
|
applied?.push({ name: r.name, timelines: r.timelines, at, alpha: r.mix, additive: r.additive, time });
|
|
389
435
|
const alpha = r.mix;
|
|
436
|
+
if (physics !== undefined && state.rules.sliderWritesPhysics) applySliderPhysics(r, at, alpha, physics);
|
|
390
437
|
const changed: string[] = [];
|
|
391
438
|
for (const target of r.timelines.bones) {
|
|
392
439
|
const index = state.index.get(target.name);
|
|
@@ -445,6 +492,89 @@ export function applySlider(state: SolverState, r: CoreSliderRecord, applied?: S
|
|
|
445
492
|
return changed;
|
|
446
493
|
}
|
|
447
494
|
|
|
495
|
+
/** The physics records a stepped pass carries, by name — what a slider's physics timelines write and a later physics constraint steps with (issue #1049, the header's *Its physics timelines*). */
|
|
496
|
+
export interface SliderPhysicsTarget {
|
|
497
|
+
records: Map<string, CorePhysicsRecord>;
|
|
498
|
+
/** Each record's setup values, before the step's own animation — what a planted blend from the setup reads. */
|
|
499
|
+
setup: ReadonlyMap<string, CorePhysicsRecord>;
|
|
500
|
+
active: (r: CorePhysicsRecord) => boolean;
|
|
501
|
+
rules: Readonly<SolverRules>;
|
|
502
|
+
ctx: PhysicsStepContext;
|
|
503
|
+
/** What the sliders wrote this pass, by constraint and field — kept only under the planted reading that a write outlasts its pass. */
|
|
504
|
+
written: Map<string, Partial<Record<PhysicsField, number>>>;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** The fields of a physics record a timeline writes. */
|
|
508
|
+
type PhysicsField = 'inertia' | 'strength' | 'damping' | 'massInverse' | 'wind' | 'gravity' | 'mix';
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The physics records of one stepped pass, ready for the sliders to write
|
|
512
|
+
* (issue #1049): every physics record as the step's own animation posed it.
|
|
513
|
+
* Under the planted reading that a slider's write outlasts its pass, what
|
|
514
|
+
* the sliders wrote on the step before stands wherever the step's own
|
|
515
|
+
* animation does not key it (`PhysicsStepContext.keyedNow`).
|
|
516
|
+
*/
|
|
517
|
+
export function sliderPhysicsTarget(records: readonly CoreConstraintRecord[], active: ReadonlySet<string>, ctx: PhysicsStepContext, rules: Readonly<SolverRules>): SliderPhysicsTarget {
|
|
518
|
+
const posed = new Map(records.flatMap((r): Array<[string, CorePhysicsRecord]> => (r.kind === 'physics' ? [[r.name, r]] : [])));
|
|
519
|
+
const out = new Map(posed);
|
|
520
|
+
const written = new Map<string, Partial<Record<PhysicsField, number>>>();
|
|
521
|
+
if (!rules.sliderPhysicsLastsOnePass) {
|
|
522
|
+
for (const [name, fields] of ctx.carried ?? new Map<string, Partial<Record<PhysicsField, number>>>()) {
|
|
523
|
+
const r = out.get(name);
|
|
524
|
+
if (r === undefined) continue;
|
|
525
|
+
const next = { ...r };
|
|
526
|
+
const kept: Partial<Record<PhysicsField, number>> = {};
|
|
527
|
+
for (const [f, v] of Object.entries(fields) as Array<[PhysicsField, number]>) {
|
|
528
|
+
if (ctx.keyedNow?.has(`${name}/${f}`)) continue;
|
|
529
|
+
next[f] = v;
|
|
530
|
+
kept[f] = v;
|
|
531
|
+
}
|
|
532
|
+
out.set(name, next);
|
|
533
|
+
written.set(name, kept);
|
|
534
|
+
}
|
|
535
|
+
ctx.carried = written;
|
|
536
|
+
}
|
|
537
|
+
return { records: out, setup: ctx.setupRecords ?? posed, active: (r) => physicsActive(r, active), rules, ctx, written };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** The value kinds a slider's physics timeline adds under `additive` (`./additive.ts`'s table); the rest write. */
|
|
541
|
+
const PHYSICS_ADDS: ReadonlySet<string> = new Set(['wind', 'gravity']);
|
|
542
|
+
|
|
543
|
+
/** A slider's physics timelines applied at `at` with alpha `alpha` onto the pass's physics records (the header's *Its physics timelines*). */
|
|
544
|
+
function applySliderPhysics(r: CoreSliderRecord, at: number, alpha: number, target: SliderPhysicsTarget): void {
|
|
545
|
+
const rules = target.rules;
|
|
546
|
+
for (const tl of r.physics) {
|
|
547
|
+
const named = target.records.get(tl.name);
|
|
548
|
+
const targets = tl.name === EVERY_GLOBAL_PHYSICS ? unnamedPhysicsTargets([...target.records.values()], tl.kind, target.active) : named !== undefined && target.active(named) ? [named] : [];
|
|
549
|
+
if (tl.kind === 'reset') {
|
|
550
|
+
// A slider applies its animation at one time, from that time: no key is crossed, so a reset key fires nothing (PS159).
|
|
551
|
+
if (!rules.sliderPhysicsResetIsDead && tl.keys.some((k) => k.time <= at)) for (const p of targets) resetPhysicsState(physicsState(target.ctx, p.name), target.ctx.time);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const i = keyIndexAt(tl.keys, at);
|
|
555
|
+
if (i < 0) continue;
|
|
556
|
+
const v = channelAt(tl.keys, i, 0, at);
|
|
557
|
+
const adds = r.additive && (PHYSICS_ADDS.has(tl.kind) || !rules.sliderPhysicsAddsWindGravityOnly);
|
|
558
|
+
for (const p of targets) {
|
|
559
|
+
const from = rules.sliderPhysicsFromCurrent ? p : (target.setup.get(p.name) as CorePhysicsRecord);
|
|
560
|
+
const next = { ...p };
|
|
561
|
+
let field: PhysicsField;
|
|
562
|
+
if (tl.kind === 'mass') {
|
|
563
|
+
field = 'massInverse';
|
|
564
|
+
if (rules.sliderPhysicsBlendsMass) {
|
|
565
|
+
const m = 1 / from.massInverse;
|
|
566
|
+
next.massInverse = 1 / (adds ? 1 / p.massInverse + v * alpha : m + (v - m) * alpha);
|
|
567
|
+
} else next.massInverse = adds ? p.massInverse + (1 / v) * alpha : from.massInverse + (1 / v - from.massInverse) * alpha;
|
|
568
|
+
} else {
|
|
569
|
+
field = tl.kind;
|
|
570
|
+
next[field] = adds ? p[field] + v * alpha : from[field] + (v - from[field]) * alpha;
|
|
571
|
+
}
|
|
572
|
+
target.records.set(p.name, next);
|
|
573
|
+
if (!rules.sliderPhysicsLastsOnePass) target.written.set(p.name, { ...target.written.get(p.name), [field]: next[field] });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
448
578
|
/** A slot's pose as the sliders move it: what it shows, its light colour and its dark colour (`null` when it states none). */
|
|
449
579
|
export interface SlotPoseState {
|
|
450
580
|
placeholder: string | null;
|
package/src/core/index.ts
CHANGED
|
@@ -352,9 +352,10 @@ export interface CompiledDocument {
|
|
|
352
352
|
/**
|
|
353
353
|
* The skin the document is posed under (issue #932, `./skins.ts`): `all` —
|
|
354
354
|
* every skin merged, the oracle's `--skin all` and `readModel`'s reading —
|
|
355
|
-
*
|
|
355
|
+
* one skin's name, `underSkin`'s, or `null` — no skin set, the runtime's
|
|
356
|
+
* `Skeleton.skin` before `setSkin` is called (`underNoSkin`, issue #1051).
|
|
356
357
|
*/
|
|
357
|
-
skin: string;
|
|
358
|
+
skin: string | null;
|
|
358
359
|
/** The skeleton's reference scale (issue #958), which every physics record carries too (`CorePhysicsRecord.referenceScale`). */
|
|
359
360
|
referenceScale: number;
|
|
360
361
|
bones: ModelBone[];
|
|
@@ -390,7 +391,7 @@ export interface CoreStated {
|
|
|
390
391
|
/**
|
|
391
392
|
* The `pages` section, checked: a list of pages, each holding exactly
|
|
392
393
|
* `CORE_PAGE_FIELDS` — a non-empty name, a size of two positive finite
|
|
393
|
-
* numbers, a list of
|
|
394
|
+
* numbers, a list of at least one region (issue #1054) — and each region exactly
|
|
394
395
|
* `CORE_PAGE_REGION_FIELDS`, a non-empty name and nine finite numbers. Every
|
|
395
396
|
* fault is named by its path. Nothing is defaulted: a field the writer always
|
|
396
397
|
* writes is required here.
|
|
@@ -422,6 +423,10 @@ function readPages(value: unknown, problems: string[], flags: boolean): ModelPag
|
|
|
422
423
|
}
|
|
423
424
|
const regions: ModelPageRegion[] = [];
|
|
424
425
|
if (!Array.isArray(raw.regions)) problems.push(`${label}: regions is not a list`);
|
|
426
|
+
// Issue #1054: a page holding no region is one no build writes — the atlas text's block for it would end before any region line, which
|
|
427
|
+
// the round trip refuses (A07_ATLAS_TEXT_SHAPE: "the last page block declares no region") — so a document stating one is refused here by
|
|
428
|
+
// the page's name rather than read, where nothing on the model side would name it.
|
|
429
|
+
else if (raw.regions.length === 0) problems.push(`${label}: regions is an empty list — a page holds at least one region; the atlas text cannot state a page with none (its block would end before a region line, which A07_ATLAS_TEXT_SHAPE refuses), so no build writes one`);
|
|
425
430
|
else {
|
|
426
431
|
raw.regions.forEach((region, j) => {
|
|
427
432
|
const at = `${label}.regions[${j}]`;
|
|
@@ -880,6 +885,11 @@ export function readModel(text: string, where = 'the model document'): CompiledD
|
|
|
880
885
|
if (isRecord(raw) && animations[i] !== undefined) animations[i].constraints = readConstraintTimelines(raw.constraints, `animations[${i}] "${animations[i].name}"`, constraints, problems);
|
|
881
886
|
});
|
|
882
887
|
}
|
|
888
|
+
// A slider's physics timelines are its animation's (issue #1049), read only now.
|
|
889
|
+
for (const c of constraints) {
|
|
890
|
+
const r = c.record;
|
|
891
|
+
if (r?.kind === 'slider') r.physics = animations.find((a) => a.name === r.animation)?.constraints.physicsKeyed ?? [];
|
|
892
|
+
}
|
|
883
893
|
const editorOrder = spec === CORE_DOCUMENT_SPEC && 'editorOrder' in value ? readEditorOrder(value.editorOrder, skins, animations, problems) : null;
|
|
884
894
|
if (problems.length > 0) throw new CoreInputError(`${where}: ${problems.length} problem(s): ${problems.join('; ')}`);
|
|
885
895
|
const stated: CoreStated | null = editorOrder === null ? null : { stage, editorOrder };
|
|
@@ -904,6 +914,20 @@ export function underSkin(doc: CompiledDocument, name: string): CompiledDocument
|
|
|
904
914
|
return view;
|
|
905
915
|
}
|
|
906
916
|
|
|
917
|
+
/**
|
|
918
|
+
* The document posed with no skin set (issue #1051): no skin's `bones` or
|
|
919
|
+
* constraint lists applied — the default skin's included — and every slot
|
|
920
|
+
* resolved through the default skin alone, or through nothing where the
|
|
921
|
+
* document declares none (`./skins.ts`, *No skin set*, for the measurement).
|
|
922
|
+
* The view a fresh skeleton is in: `render` without `--skin`, A10's walk and
|
|
923
|
+
* `validate()`'s poses (`noSkinView` in `src/render_core.ts`).
|
|
924
|
+
*/
|
|
925
|
+
export function underNoSkin(doc: CompiledDocument): CompiledDocument {
|
|
926
|
+
const view: CompiledDocument = { ...doc, skin: null, constraints: doc.constraints.map((c) => (c.record === undefined ? c : { ...c, record: { ...c.record } as CoreConstraintRecord })) };
|
|
927
|
+
resolveSkinView(view);
|
|
928
|
+
return view;
|
|
929
|
+
}
|
|
930
|
+
|
|
907
931
|
/**
|
|
908
932
|
* What a skin view decides on the constraint records: whether an applied
|
|
909
933
|
* skin's list names each (`listedBySkin`, `./skins.ts`), and what each path
|
package/src/core/skins.ts
CHANGED
|
@@ -54,6 +54,30 @@
|
|
|
54
54
|
* not what the slot shows; under `all` every bone some skin names is
|
|
55
55
|
* active, so a one-skin rig is unchanged.
|
|
56
56
|
*
|
|
57
|
+
* - **No skin set** (issue #1051): a fresh skeleton whose `setSkin` was never
|
|
58
|
+
* called — `underNoSkin`, the state `render` without `--skin`, A10's walk
|
|
59
|
+
* and `validate()` pose in. No skin's `bones` or constraint lists are
|
|
60
|
+
* applied, the default skin's included, and a slot shows the default
|
|
61
|
+
* skin's record for its placeholder, else nothing — so over a document
|
|
62
|
+
* with skins and no `default` one every slot shows nothing. Measured with
|
|
63
|
+
* `tools/pose_oracle.ts dump --skin none` on 93 skeletons rebuilt through
|
|
64
|
+
* `ingest` and `compile` (31 with no default skin, 31 whose default names
|
|
65
|
+
* skin-required members, 31 with a plain default): `Skeleton.skin` read
|
|
66
|
+
* `null` on all 93; all 311 skin-required bones read inactive — the 51 a
|
|
67
|
+
* default skin names among them, and the 66 a constraint writes — and the
|
|
68
|
+
* 125 bones that are not skin-required under one read active and unposed;
|
|
69
|
+
* all 73 skin-required constraints read inactive and moved nothing when
|
|
70
|
+
* removed — the 12 a default skin lists among them — and the 81 that are
|
|
71
|
+
* not read active; 199 slots whose placeholder the default skin fills
|
|
72
|
+
* showed it and 359 only named skins fill showed nothing; a colour key on
|
|
73
|
+
* a slot whose bone is inactive held its setup colour (50 of 50) and moved
|
|
74
|
+
* it on an active one (46 of 46), and a draw-order key applied either way
|
|
75
|
+
* (93 of 93). The core suite's `CO29` holds the same population against
|
|
76
|
+
* the core under `--raw` at tolerance 0, and `CO30` plants each rejected
|
|
77
|
+
* reading (the old one among them: no skin set read as the default skin,
|
|
78
|
+
* which differs exactly where the default skin names a skin-required
|
|
79
|
+
* member).
|
|
80
|
+
*
|
|
57
81
|
* Under `all` every skin is applied at once: `bones` and the constraint
|
|
58
82
|
* lists of every skin count, and a placeholder several skins fill shows the
|
|
59
83
|
* LAST of them in the Spine file's order — the case `./index.ts` leaves out
|
|
@@ -68,15 +92,16 @@ export const CORE_ALL_SKINS = 'all';
|
|
|
68
92
|
/** The skin a document names its default skin with — Spine's `SkeletonData.defaultSkin` is the skin of this name. */
|
|
69
93
|
export const CORE_DEFAULT_SKIN = 'default';
|
|
70
94
|
|
|
71
|
-
/** The skins whose `bones` and constraint lists are applied: every skin under `all`, the named skin alone otherwise (the header's
|
|
95
|
+
/** The skins whose `bones` and constraint lists are applied: every skin under `all`, none with no skin set, the named skin alone otherwise (the header's measurements). */
|
|
72
96
|
export function appliedSkins(doc: CompiledDocument): CoreSkin[] {
|
|
97
|
+
if (doc.skin === null) return [];
|
|
73
98
|
return doc.skin === CORE_ALL_SKINS ? doc.skins : doc.skins.filter((k) => k.name === doc.skin);
|
|
74
99
|
}
|
|
75
100
|
|
|
76
|
-
/** Under a named skin, the skins a placeholder is looked up in, in precedence order: the named skin, then the default skin (the header's
|
|
101
|
+
/** Under a named skin or none, the skins a placeholder is looked up in, in precedence order: the named skin, then the default skin (the header's measurements). */
|
|
77
102
|
export function lookupSkins(doc: CompiledDocument): CoreSkin[] {
|
|
78
103
|
if (doc.skin === CORE_ALL_SKINS) return doc.skins;
|
|
79
|
-
const named = doc.skins.find((k) => k.name === doc.skin);
|
|
104
|
+
const named = doc.skin === null ? undefined : doc.skins.find((k) => k.name === doc.skin);
|
|
80
105
|
const fallback = doc.skin === CORE_DEFAULT_SKIN ? undefined : doc.skins.find((k) => k.name === CORE_DEFAULT_SKIN);
|
|
81
106
|
return [named, fallback].filter((k): k is CoreSkin => k !== undefined);
|
|
82
107
|
}
|