spine-rigc 1.5.3 → 1.6.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/README.md +2 -1
- package/cli.ts +131 -403
- package/docs/AUTHORING.md +63 -14
- package/docs/INGEST.md +1 -0
- package/docs/MOTION.md +1 -0
- package/package.json +1 -1
- package/src/atlas.ts +25 -0
- package/src/check.ts +113 -24
- package/src/checkpics.ts +2 -0
- package/src/compile.ts +641 -506
- package/src/core/animation.ts +982 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1721 -0
- package/src/core/constraints_path.ts +942 -0
- package/src/core/constraints_physics.ts +830 -0
- package/src/core/constraints_slider.ts +505 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1103 -0
- package/src/core/raw.ts +347 -0
- package/src/core/skins.ts +104 -0
- package/src/core/uvs.ts +448 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/world.ts +233 -0
- package/src/deformmeasure.ts +337 -73
- package/src/deformreport.ts +370 -0
- package/src/depth.ts +101 -2
- package/src/emit_spine.ts +860 -0
- package/src/mesh.ts +24 -15
- package/src/model.ts +958 -0
- package/src/render.ts +649 -143
- package/src/render_core.ts +582 -0
- package/src/rig.ts +34 -6
- package/src/transform.ts +26 -4
- package/src/types.ts +18 -0
- package/src/validate.ts +30 -2
package/src/compile.ts
CHANGED
|
@@ -41,7 +41,6 @@ import { parseJsonWithPosition } from './json-position.ts';
|
|
|
41
41
|
import { dottedPath, nearMisses, refuseNumbersTheFileCannotCarry, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
42
42
|
import type { ShapeVisit } from './keys.ts';
|
|
43
43
|
import { MANIFEST_ENUMS, MANIFEST_TYPES } from './types.ts';
|
|
44
|
-
import { inEditorKeyOrder, withoutParserDefaults } from './keyorder.ts';
|
|
45
44
|
import { EVERY_GLOBAL_PHYSICS, parseMotionSpec } from './motion.ts';
|
|
46
45
|
import {
|
|
47
46
|
BONE_INHERIT_KNOWN,
|
|
@@ -79,7 +78,7 @@ import {
|
|
|
79
78
|
buildRibbonMesh,
|
|
80
79
|
buildRingMesh,
|
|
81
80
|
checkHullOrder,
|
|
82
|
-
|
|
81
|
+
bindWeightedVertices,
|
|
83
82
|
formatWalk,
|
|
84
83
|
measureAuthoredMeshFit,
|
|
85
84
|
meshEdges,
|
|
@@ -128,6 +127,34 @@ import {
|
|
|
128
127
|
TransformError,
|
|
129
128
|
type BoneTransform,
|
|
130
129
|
} from './transform.ts';
|
|
130
|
+
import { emitSkeleton, PHYSICS_PARAMS, UNSTATED_REFERENCE_SCALE, type SkeletonHeader } from './emit_spine.ts';
|
|
131
|
+
import {
|
|
132
|
+
isModelVertexAttachment,
|
|
133
|
+
type CarriedFromCompileResult,
|
|
134
|
+
type CompiledAnimation,
|
|
135
|
+
type ModelAttachmentTimelines,
|
|
136
|
+
type ModelBinding,
|
|
137
|
+
type ModelBone,
|
|
138
|
+
type ModelBoundingBoxAttachment,
|
|
139
|
+
type ModelClippingAttachment,
|
|
140
|
+
type ModelConstraint,
|
|
141
|
+
type ModelConstraintKind,
|
|
142
|
+
type ModelEvent,
|
|
143
|
+
type ModelKey,
|
|
144
|
+
type ModelLinkedMeshAttachment,
|
|
145
|
+
type ModelMeshAttachment,
|
|
146
|
+
type ModelPathAttachment,
|
|
147
|
+
type ModelAtlasRect,
|
|
148
|
+
type ModelRegionAttachment,
|
|
149
|
+
type ModelRegionSequence,
|
|
150
|
+
type ModelSequence,
|
|
151
|
+
type ModelSkin,
|
|
152
|
+
type ModelSlot,
|
|
153
|
+
type ModelTimelines,
|
|
154
|
+
type ModelVertices,
|
|
155
|
+
type SkinTable,
|
|
156
|
+
type SkinTableEntry,
|
|
157
|
+
} from './model.ts';
|
|
131
158
|
import type {
|
|
132
159
|
CompileResult,
|
|
133
160
|
CompiledImage,
|
|
@@ -147,20 +174,6 @@ import type {
|
|
|
147
174
|
MotionValueKey,
|
|
148
175
|
MotionValueTrack,
|
|
149
176
|
RigInfo,
|
|
150
|
-
SpineAttachment,
|
|
151
|
-
SpineBone,
|
|
152
|
-
SpineBoundingBoxAttachment,
|
|
153
|
-
SpineClippingAttachment,
|
|
154
|
-
SpineConstraint,
|
|
155
|
-
SpineEvent,
|
|
156
|
-
SpineLinkedMeshAttachment,
|
|
157
|
-
SpineMeshAttachment,
|
|
158
|
-
SpinePathAttachment,
|
|
159
|
-
SpineRegionAttachment,
|
|
160
|
-
SpineSequence,
|
|
161
|
-
SpineSkeletonJson,
|
|
162
|
-
SpineSlot,
|
|
163
|
-
SpineTimelineKey,
|
|
164
177
|
} from './types.ts';
|
|
165
178
|
|
|
166
179
|
export { CompileError, NotImplementedError };
|
|
@@ -292,11 +305,15 @@ const FRAME = 1 / 60;
|
|
|
292
305
|
*
|
|
293
306
|
* The comparator is hand-rolled and locale-independent, which `A18` requires:
|
|
294
307
|
* `localeCompare` would make the emitted bytes a property of the machine.
|
|
308
|
+
*
|
|
309
|
+
* Handed to the Spine emitter as its `AnimationOrder` (issue #921): it takes
|
|
310
|
+
* the model's names and `emitAnimations` keys the object in the order this
|
|
311
|
+
* returns, so the refusal below is raised there, inside `compile`, in these
|
|
312
|
+
* words. Exported so the selftest hands it to the emitter the way the assembly
|
|
313
|
+
* does (`MA03`, `MA08`).
|
|
295
314
|
*/
|
|
296
|
-
function editorAnimationOrder
|
|
297
|
-
|
|
298
|
-
for (const name of editorNamesInOrder(Object.keys(animations), 'animations')) ordered[name] = animations[name];
|
|
299
|
-
return ordered;
|
|
315
|
+
export function editorAnimationOrder(names: readonly string[]): string[] {
|
|
316
|
+
return editorNamesInOrder(names, 'animations');
|
|
300
317
|
}
|
|
301
318
|
|
|
302
319
|
/**
|
|
@@ -374,8 +391,12 @@ const DEFAULT_SKIN = 'default';
|
|
|
374
391
|
* ⇒ Every skin name in this tree is lower-case ASCII without digits, so no
|
|
375
392
|
* emitted byte moves and no rig is refused. What moves is the claim, and what
|
|
376
393
|
* stops being refused is a rig with capitals, folders or digits in its skins.
|
|
394
|
+
*
|
|
395
|
+
* Exported, with `editorSlotKeyOrder`, because the Spine emitter applies both
|
|
396
|
+
* (`emitSkins` takes them as its `EditorOrder`, issue #919) and the selftest
|
|
397
|
+
* hands them to it the way the assembly does (`MS07`, `MS08`).
|
|
377
398
|
*/
|
|
378
|
-
function editorSkinOrder<T extends { name: string }>(skins: readonly T[]): T[] {
|
|
399
|
+
export function editorSkinOrder<T extends { name: string }>(skins: readonly T[]): T[] {
|
|
379
400
|
const pinned = skins.filter((skin) => skin.name === DEFAULT_SKIN);
|
|
380
401
|
const rest = skins.filter((skin) => skin.name !== DEFAULT_SKIN);
|
|
381
402
|
// Ranked rather than looked up by name: two skins answering to one name are a
|
|
@@ -421,7 +442,7 @@ function editorSkinOrder<T extends { name: string }>(skins: readonly T[]): T[] {
|
|
|
421
442
|
* attachment name holding `/` is the common case the folder question would
|
|
422
443
|
* have to answer first.
|
|
423
444
|
*/
|
|
424
|
-
function editorSlotKeyOrder<T>(attachments: Record<string, T>): Record<string, T> {
|
|
445
|
+
export function editorSlotKeyOrder<T>(attachments: Record<string, T>): Record<string, T> {
|
|
425
446
|
const names = Object.keys(attachments);
|
|
426
447
|
if (names.some((name) => name.includes(NAME_FOLDER))) return attachments;
|
|
427
448
|
for (let i = 0; i < names.length; i++) {
|
|
@@ -839,8 +860,14 @@ const FLOAT32_BITS = new Uint32Array(FLOAT32_SCRATCH.buffer);
|
|
|
839
860
|
* `137.5`. What this does NOT do is snap a double's residue to zero: `r6`'s grid
|
|
840
861
|
* was absolute and swallowed anything under 5e-7, a float's is relative, so a
|
|
841
862
|
* residue of 1e-15 is a float in its own right and is emitted as one.
|
|
863
|
+
*
|
|
864
|
+
* 🔒 Exported for one reader: `MX01` in `selftest.ts` defines "on the float32
|
|
865
|
+
* grid" as a fixed point of this function, `f32(x) === x`, and holds every
|
|
866
|
+
* number the model document spells to it or to the six-decimal grid (issue
|
|
867
|
+
* #942). `Math.fround(x) === x` is not that test: this writes `0.2`, whose
|
|
868
|
+
* float is 0.20000000298023224, so it would refuse the emitter's own output.
|
|
842
869
|
*/
|
|
843
|
-
function f32(n: number): number {
|
|
870
|
+
export function f32(n: number): number {
|
|
844
871
|
const f = Math.fround(n);
|
|
845
872
|
if (f === 0 || !Number.isFinite(f)) return f === 0 ? 0 : f;
|
|
846
873
|
const magnitude = Math.abs(f);
|
|
@@ -1187,7 +1214,7 @@ const BONE_TRACKS: Record<string, ValueTrackShape> = {
|
|
|
1187
1214
|
* `:1062` and only `mix` reassigns it (`:1090`), so an `inertia` key that omits
|
|
1188
1215
|
* `value` reads **0** — not the 0.5 that `:306` gives a constraint that states
|
|
1189
1216
|
* no `inertia`. Two different tables of defaults sit forty lines apart in one
|
|
1190
|
-
* file (`PHYSICS_PARAMS`
|
|
1217
|
+
* file (`PHYSICS_PARAMS` in `src/emit_spine.ts` holds the other one), and reading the setup
|
|
1191
1218
|
* column into this one would emit a `damping` timeline whose omitted keys mean
|
|
1192
1219
|
* 0.85 to the author and 0 to the runtime. Nothing here depends on the number,
|
|
1193
1220
|
* because `compileValueTrack` writes every field explicitly — it is recorded
|
|
@@ -1418,24 +1445,15 @@ const CONSTRAINT_TIMELINES: Record<'ik' | 'transform', ConstraintTimelineShape>
|
|
|
1418
1445
|
};
|
|
1419
1446
|
|
|
1420
1447
|
/**
|
|
1421
|
-
*
|
|
1448
|
+
* The five components a physics constraint drives. The parameters and their
|
|
1449
|
+
* parser defaults are `PHYSICS_PARAMS`, which moved to `src/emit_spine.ts` with
|
|
1450
|
+
* the omission it decides (issue #919).
|
|
1422
1451
|
*
|
|
1423
1452
|
* `PHYSICS_COMPONENTS` is exported for `ingest.ts`, which omits a constraint that
|
|
1424
1453
|
* drives none of them (issue #731) and must read "drives" off the same five
|
|
1425
1454
|
* names this module refuses an empty constraint with, not off a copy.
|
|
1426
1455
|
*/
|
|
1427
1456
|
export const PHYSICS_COMPONENTS = ['x', 'y', 'rotate', 'scaleX', 'shearX'] as const;
|
|
1428
|
-
const PHYSICS_PARAMS: Array<[string, number]> = [
|
|
1429
|
-
['inertia', 0.5],
|
|
1430
|
-
['strength', 100],
|
|
1431
|
-
['damping', 0.85],
|
|
1432
|
-
['mass', 1],
|
|
1433
|
-
['wind', 0],
|
|
1434
|
-
['gravity', 0],
|
|
1435
|
-
['mix', 1],
|
|
1436
|
-
['fps', 60],
|
|
1437
|
-
['limit', 5000],
|
|
1438
|
-
];
|
|
1439
1457
|
|
|
1440
1458
|
// ---------------------------------------------------------------------------
|
|
1441
1459
|
// curves
|
|
@@ -2537,7 +2555,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2537
2555
|
const axisSpineDeg = manifest?.axis ? screenToSpineDegrees(manifest.axis.deg) : null;
|
|
2538
2556
|
const partBySlot = new Map(parts.map((part) => [part.slot, part]));
|
|
2539
2557
|
|
|
2540
|
-
const bones:
|
|
2558
|
+
const bones: ModelBone[] = [];
|
|
2541
2559
|
for (const spec of rig.bones) {
|
|
2542
2560
|
bones.push(buildBone(spec, bones, { rig, manifest, cropH, axisSpineDeg, partBySlot }));
|
|
2543
2561
|
}
|
|
@@ -2563,9 +2581,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2563
2581
|
// -- 4. slots + skins ------------------------------------------------------
|
|
2564
2582
|
// Draw order IS the slots array order. No separate field,
|
|
2565
2583
|
// and the rig's array is that order.
|
|
2566
|
-
const slots:
|
|
2567
|
-
|
|
2568
|
-
|
|
2584
|
+
const slots: ModelSlot[] = [];
|
|
2585
|
+
// Every attachment is a model record (`SkinTableEntry`); the emitter turns the
|
|
2586
|
+
// tables into Spine's at assembly, and `CompiledModel.skins` holds them.
|
|
2587
|
+
const skinTables = new Map<string, SkinTable>();
|
|
2588
|
+
const tableFor = (skinName: string): SkinTable => {
|
|
2569
2589
|
let table = skinTables.get(skinName);
|
|
2570
2590
|
if (!table) {
|
|
2571
2591
|
table = {};
|
|
@@ -2703,11 +2723,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2703
2723
|
if (setup?.color && rigSlot.color !== undefined) {
|
|
2704
2724
|
throw new CompileError(`slot "${rigSlot.name}" has a setup colour in the rig spec AND in the motion spec`);
|
|
2705
2725
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2726
|
+
// The setup attachment is held as the value it is, `null` included; leaving
|
|
2727
|
+
// the key out for `null` is the emitter's (`emitSlots`).
|
|
2728
|
+
const slot: ModelSlot = { name: rigSlot.name, bone: rigSlot.bone, setup: setupAttachment };
|
|
2708
2729
|
if (setup?.color) slot.color = rgbaHex(setup.color);
|
|
2709
2730
|
else if (rigSlot.color !== undefined) slot.color = rigSlot.color;
|
|
2710
2731
|
if (rigSlot.dark !== undefined) slot.dark = rigSlot.dark;
|
|
2732
|
+
// As stated: `parseRigSpec` refused every spelling the runtime would read as no mode (issue #946).
|
|
2711
2733
|
if (rigSlot.blend !== undefined) slot.blend = rigSlot.blend;
|
|
2712
2734
|
slots.push(slot);
|
|
2713
2735
|
// ...and nothing below this line has anything to build. An empty entry in a
|
|
@@ -2717,7 +2739,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2717
2739
|
if (empty) continue;
|
|
2718
2740
|
|
|
2719
2741
|
if (part) {
|
|
2720
|
-
const perSlot: Record<string,
|
|
2742
|
+
const perSlot: Record<string, SkinTableEntry> = {};
|
|
2721
2743
|
const mesh = part.mesh ? buildMesh(part, manifest!, bones, transforms, rigSlot.bone) : null;
|
|
2722
2744
|
for (const name of names) {
|
|
2723
2745
|
const img = images.find((im) => im.region === name);
|
|
@@ -2751,7 +2773,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2751
2773
|
for (const skinName of skinNames) {
|
|
2752
2774
|
const placeholders = skinParts.get(skinName)!.attachments[rigSlot.name];
|
|
2753
2775
|
if (!placeholders) continue;
|
|
2754
|
-
const perSlot: Record<string,
|
|
2776
|
+
const perSlot: Record<string, SkinTableEntry> = {};
|
|
2755
2777
|
for (const [placeholder, att] of Object.entries(placeholders)) {
|
|
2756
2778
|
const where = `skin "${skinName}" slot "${rigSlot.name}" attachment "${placeholder}"`;
|
|
2757
2779
|
const built = buildRigAttachment(att, placeholder, where, {
|
|
@@ -2766,6 +2788,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2766
2788
|
imagesDir,
|
|
2767
2789
|
depths: attachmentDepths,
|
|
2768
2790
|
slotNames: new Set(rig.slots.map((s) => s.name)),
|
|
2791
|
+
atlasRegions: atlasIn === null ? null : atlasIn.byName,
|
|
2769
2792
|
links: pendingLinks,
|
|
2770
2793
|
});
|
|
2771
2794
|
// The name is put on AFTER the builder rather than inside it: six
|
|
@@ -2815,7 +2838,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2815
2838
|
// (structure), then the motion spec's physics table (tuning). A name in both is
|
|
2816
2839
|
// refused: `mix` timelines resolve by name, and two constraints answering to
|
|
2817
2840
|
// one name is a timeline driving something nobody chose.
|
|
2818
|
-
const constraints:
|
|
2841
|
+
const constraints: ModelConstraint[] = [];
|
|
2819
2842
|
const physicsReport: CompileResult['physics'] = [];
|
|
2820
2843
|
// Deform keys that stated a model instead of a run (issue #294). Declared here
|
|
2821
2844
|
// rather than inside the animation loop because `explain` reports it across
|
|
@@ -2851,8 +2874,8 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2851
2874
|
constraintNamesOfKind.set(type, [...(constraintNamesOfKind.get(type) ?? []), name]);
|
|
2852
2875
|
};
|
|
2853
2876
|
/**
|
|
2854
|
-
* ik constraint ->
|
|
2855
|
-
*
|
|
2877
|
+
* ik constraint -> its three booleans IN EFFECT: the value it declares, or
|
|
2878
|
+
* the parser's default where it declares none. Issue #273.
|
|
2856
2879
|
*
|
|
2857
2880
|
* 🚨 `SkeletonJson` reads `bendPositive`, `compress` and `stretch` in TWO
|
|
2858
2881
|
* places with the same defaults: once on the constraint (`:155`) and once on
|
|
@@ -2861,19 +2884,23 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2861
2884
|
* `bendPositive: false` and an `ik` timeline that keys only `mix` produce a
|
|
2862
2885
|
* constraint that bends the other way for the whole animation, with the field
|
|
2863
2886
|
* still in the file and inert: four builds differing only in these flags posed
|
|
2864
|
-
* one pose.
|
|
2865
|
-
*
|
|
2866
|
-
*
|
|
2887
|
+
* one pose. So a key that states no flag takes the constraint's from this
|
|
2888
|
+
* map, and the model's ik key holds all three flags in effect (`ModelKey`).
|
|
2889
|
+
* Which of them the file carries is the Spine emitter's: only those that
|
|
2890
|
+
* differ from the per-key default (`emitAnimations`, issue #921), which is
|
|
2891
|
+
* what this map held before — a rig that says nothing and a key that says
|
|
2892
|
+
* nothing already agree, so nothing is written for them.
|
|
2867
2893
|
*/
|
|
2868
|
-
const
|
|
2894
|
+
const ikFlagsInEffect = new Map<string, Record<string, boolean>>();
|
|
2869
2895
|
// Which slots can actually show a path, for the path constraint's own check.
|
|
2870
|
-
// Read off the
|
|
2871
|
-
//
|
|
2896
|
+
// Read off the skin tables — the model records the file is emitted from —
|
|
2897
|
+
// rather than the spec, so it answers the question the runtime asks: is
|
|
2898
|
+
// there an attachment of that kind on that slot? (By `kind` since issue #919.)
|
|
2872
2899
|
const pathSlots = new Map<string, string[]>();
|
|
2873
2900
|
for (const [skinName, table] of skinTables) {
|
|
2874
2901
|
for (const [slotName, perSlot] of Object.entries(table)) {
|
|
2875
2902
|
for (const att of Object.values(perSlot)) {
|
|
2876
|
-
if (
|
|
2903
|
+
if (att.kind !== 'path') continue;
|
|
2877
2904
|
pathSlots.set(slotName, [...(pathSlots.get(slotName) ?? []), skinName]);
|
|
2878
2905
|
break;
|
|
2879
2906
|
}
|
|
@@ -2893,12 +2920,15 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2893
2920
|
constraints.push(buildRigConstraint(spec, constraintCtx));
|
|
2894
2921
|
declareConstraint(spec.name, spec.type);
|
|
2895
2922
|
if (spec.type === 'ik') {
|
|
2896
|
-
|
|
2923
|
+
// `flag.dflt` is the per-key default, and the constraint's own parser
|
|
2924
|
+
// default is the same value for each of the three
|
|
2925
|
+
// (`PARSER_DEFAULTS['ik constraint']` and `['ik key']` in keyorder.ts).
|
|
2926
|
+
const inEffect: Record<string, boolean> = {};
|
|
2897
2927
|
for (const flag of CONSTRAINT_TIMELINES.ik.flags) {
|
|
2898
2928
|
const declared = spec[flag.field];
|
|
2899
|
-
|
|
2929
|
+
inEffect[flag.field] = typeof declared === 'boolean' ? declared : flag.dflt;
|
|
2900
2930
|
}
|
|
2901
|
-
|
|
2931
|
+
ikFlagsInEffect.set(spec.name, inEffect);
|
|
2902
2932
|
}
|
|
2903
2933
|
}
|
|
2904
2934
|
withMotionSource(() => {
|
|
@@ -2916,17 +2946,17 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2916
2946
|
if (!boneNames.has(spec.bone)) {
|
|
2917
2947
|
throw new CompileError(`physics constraint "${name}" targets unknown bone "${spec.bone}"`);
|
|
2918
2948
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
};
|
|
2949
|
+
// Every component and parameter the table states is held, 0 and the
|
|
2950
|
+
// parser's default included; which of them Spine leaves out is the
|
|
2951
|
+
// emitter's (`emitConstraint`). `components` is the report's list of the
|
|
2952
|
+
// ones that DRIVE, as it always was.
|
|
2953
|
+
const entry: ModelConstraint = { kind: 'physics', name, declaredIn: 'motion', bone: spec.bone };
|
|
2924
2954
|
const components: string[] = [];
|
|
2925
2955
|
for (const comp of PHYSICS_COMPONENTS) {
|
|
2926
2956
|
const v = spec[comp];
|
|
2927
|
-
if (v === undefined
|
|
2957
|
+
if (v === undefined) continue;
|
|
2928
2958
|
entry[comp] = f32(v);
|
|
2929
|
-
components.push(comp);
|
|
2959
|
+
if (v !== 0) components.push(comp);
|
|
2930
2960
|
}
|
|
2931
2961
|
if (!components.length) {
|
|
2932
2962
|
// The parser is happy with this and the constraint does nothing at all.
|
|
@@ -2935,9 +2965,9 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2935
2965
|
`physics constraint "${name}" drives no component — set at least one of ${PHYSICS_COMPONENTS.join('/')}`,
|
|
2936
2966
|
);
|
|
2937
2967
|
}
|
|
2938
|
-
for (const [param
|
|
2968
|
+
for (const [param] of PHYSICS_PARAMS) {
|
|
2939
2969
|
const v = spec[param as keyof typeof spec] as number | undefined;
|
|
2940
|
-
if (v === undefined
|
|
2970
|
+
if (v === undefined) continue;
|
|
2941
2971
|
entry[param] = f32(v);
|
|
2942
2972
|
}
|
|
2943
2973
|
constraints.push(entry);
|
|
@@ -2983,12 +3013,15 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2983
3013
|
// the refusal below reports as the skins it looked in, so it is read from the
|
|
2984
3014
|
// map rather than restated.
|
|
2985
3015
|
const attachmentIndex: SlotAttachmentIndex = { bySlot: attachmentsBySlot, searched: [...skinTables.keys()] };
|
|
2986
|
-
|
|
3016
|
+
// The model's animations, in the motion spec's order; `emitAnimations` keys
|
|
3017
|
+
// them in the editor's at assembly.
|
|
3018
|
+
const animations = new Map<string, CompiledAnimation>();
|
|
2987
3019
|
const declaredDurations: Record<string, number> = {};
|
|
2988
3020
|
const slotNames = new Set(slots.map((s) => s.name));
|
|
2989
|
-
// Which slots an `rgba2` timeline may be keyed on: the
|
|
2990
|
-
//
|
|
2991
|
-
//
|
|
3021
|
+
// Which slots an `rgba2` timeline may be keyed on: the model slots with a
|
|
3022
|
+
// `dark` — the slots the file is emitted from, which `emitSlots` writes each
|
|
3023
|
+
// `dark` of (issue #919) — for the same reason `attachmentIndex` is built off
|
|
3024
|
+
// the skin tables: what the runtime does with a timeline depends on the file,
|
|
2992
3025
|
// not on the spec that produced it. A slot whose `dark` never reached the
|
|
2993
3026
|
// artifact is a slot the runtime allocates no dark colour for (issue #690).
|
|
2994
3027
|
const darkSlots = new Set(slots.filter((s) => s.dark !== undefined).map((s) => s.name));
|
|
@@ -2997,13 +3030,24 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2997
3030
|
checkMotionGroups(motion);
|
|
2998
3031
|
for (const [animName, anim] of Object.entries(motion.animations)) {
|
|
2999
3032
|
declaredDurations[animName] = anim.duration;
|
|
3000
|
-
const slotTimelines
|
|
3001
|
-
const boneTimelines
|
|
3002
|
-
/**
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3033
|
+
const slotTimelines = new Map<string, ModelTimelines>();
|
|
3034
|
+
const boneTimelines = new Map<string, ModelTimelines>();
|
|
3035
|
+
/**
|
|
3036
|
+
* One table per constraint family, target -> timelines. The physics
|
|
3037
|
+
* timeline that names no constraint is filed under its own target,
|
|
3038
|
+
* `EVERY_GLOBAL_PHYSICS`, in the position its track holds among the
|
|
3039
|
+
* named ones (`CompiledAnimation`).
|
|
3040
|
+
*/
|
|
3041
|
+
const familyTimelines: Record<ConstraintTrackFamily, Map<string, ModelTimelines>> = {
|
|
3042
|
+
physics: new Map(),
|
|
3043
|
+
path: new Map(),
|
|
3044
|
+
slider: new Map(),
|
|
3045
|
+
};
|
|
3046
|
+
/** `targets.get(target)`, made on first use — the map keeps the order targets are first keyed in. */
|
|
3047
|
+
const timelinesOf = (targets: Map<string, ModelTimelines>, target: string): ModelTimelines => {
|
|
3048
|
+
let timelines = targets.get(target);
|
|
3049
|
+
if (timelines === undefined) targets.set(target, (timelines = new Map()));
|
|
3050
|
+
return timelines;
|
|
3007
3051
|
};
|
|
3008
3052
|
const claimed = new Set<string>();
|
|
3009
3053
|
let compiledDuration = 0;
|
|
@@ -3031,7 +3075,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3031
3075
|
);
|
|
3032
3076
|
}
|
|
3033
3077
|
if (everyGlobal) {
|
|
3034
|
-
const physics = constraints.filter((one) => one.
|
|
3078
|
+
const physics = constraints.filter((one) => one.kind === 'physics');
|
|
3035
3079
|
const flag = `${track.property}Global`;
|
|
3036
3080
|
const reached =
|
|
3037
3081
|
track.property === 'reset'
|
|
@@ -3103,7 +3147,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3103
3147
|
// the fault is the pair, not a key.
|
|
3104
3148
|
if (family === null && !isBoneTrack) {
|
|
3105
3149
|
const mine = SLOT_COLOR_CHANNELS[track.property] ?? [];
|
|
3106
|
-
for (const other of
|
|
3150
|
+
for (const other of slotTimelines.get(target)?.keys() ?? []) {
|
|
3107
3151
|
const shared = (SLOT_COLOR_CHANNELS[other] ?? []).filter((channel) => mine.includes(channel));
|
|
3108
3152
|
if (shared.length === 0) continue;
|
|
3109
3153
|
throw new CompileError(
|
|
@@ -3133,11 +3177,12 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3133
3177
|
: isBoneTrack
|
|
3134
3178
|
? compileValueTrack(resolved, motion, animName, anim.duration, target, shift, BONE_TRACKS, 'bone')
|
|
3135
3179
|
: compileTrack(resolved, motion, animName, anim.duration, target, shift, attachmentIndex, darkSlots);
|
|
3136
|
-
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time
|
|
3137
|
-
//
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
else (
|
|
3180
|
+
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3181
|
+
// The every-global physics timeline under its own target, `*`
|
|
3182
|
+
// (`everyGlobal`); the empty name is the Spine emitter's spelling.
|
|
3183
|
+
if (family !== null) timelinesOf(familyTimelines[family], target).set(track.property, keys);
|
|
3184
|
+
else if (isBoneTrack) timelinesOf(boneTimelines, target).set(track.property, keys);
|
|
3185
|
+
else timelinesOf(slotTimelines, target).set(track.property, keys);
|
|
3141
3186
|
});
|
|
3142
3187
|
}
|
|
3143
3188
|
|
|
@@ -3148,9 +3193,9 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3148
3193
|
// The target is resolved by name AND by type: `findConstraint(name,
|
|
3149
3194
|
// IkConstraintData)` misses a transform constraint of the same name and the
|
|
3150
3195
|
// parser throws in the consumer's process, so the mismatch is named here.
|
|
3151
|
-
const constraintTimelines: Record<'ik' | 'transform',
|
|
3152
|
-
ik:
|
|
3153
|
-
transform:
|
|
3196
|
+
const constraintTimelines: Record<'ik' | 'transform', Map<string, ModelKey[]>> = {
|
|
3197
|
+
ik: new Map(),
|
|
3198
|
+
transform: new Map(),
|
|
3154
3199
|
};
|
|
3155
3200
|
for (const group of ['ik', 'transform'] as const) {
|
|
3156
3201
|
// The array, the entries and their `constraint` names are shapes, so
|
|
@@ -3175,7 +3220,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3175
3220
|
: `the rig declares no ${group} constraint at all`),
|
|
3176
3221
|
);
|
|
3177
3222
|
}
|
|
3178
|
-
if (constraintTimelines[group]
|
|
3223
|
+
if (constraintTimelines[group].has(name)) {
|
|
3179
3224
|
throw new CompileError(
|
|
3180
3225
|
`animation "${animName}" has two ${group} timelines on constraint "${name}"; ` +
|
|
3181
3226
|
'the group holds one timeline per constraint, so merge them into one',
|
|
@@ -3190,18 +3235,25 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3190
3235
|
// The ik table, asked only by the ik group: `leg` may also be a
|
|
3191
3236
|
// transform constraint, and a transform key set carries no flag for
|
|
3192
3237
|
// the rig's booleans to be stamped onto (issue #692).
|
|
3193
|
-
group === 'ik' ? (
|
|
3238
|
+
group === 'ik' ? (ikFlagsInEffect.get(name) ?? {}) : {},
|
|
3194
3239
|
);
|
|
3195
|
-
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time
|
|
3196
|
-
constraintTimelines[group]
|
|
3240
|
+
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3241
|
+
constraintTimelines[group].set(name, keys);
|
|
3197
3242
|
}
|
|
3198
3243
|
}
|
|
3199
3244
|
|
|
3200
3245
|
// -- deform timelines: keyed on a skin/slot/attachment triple ----------
|
|
3201
|
-
//
|
|
3202
|
-
//
|
|
3203
|
-
|
|
3204
|
-
|
|
3246
|
+
// skin -> slot -> attachment -> its `deform` and `sequence` keys: the two
|
|
3247
|
+
// timelines an attachment can carry, which is why the last level exists.
|
|
3248
|
+
const deformTimelines = new Map<string, Map<string, Map<string, ModelAttachmentTimelines>>>();
|
|
3249
|
+
/** The triple's entry, made on first use in the order triples are first keyed. */
|
|
3250
|
+
const attachmentTimelinesOf = (skin: string, slot: string): Map<string, ModelAttachmentTimelines> => {
|
|
3251
|
+
let bySlot = deformTimelines.get(skin);
|
|
3252
|
+
if (bySlot === undefined) deformTimelines.set(skin, (bySlot = new Map()));
|
|
3253
|
+
let byAttachment = bySlot.get(slot);
|
|
3254
|
+
if (byAttachment === undefined) bySlot.set(slot, (byAttachment = new Map()));
|
|
3255
|
+
return byAttachment;
|
|
3256
|
+
};
|
|
3205
3257
|
const deformTracks: MotionDeformTrack[] = anim.deform ?? [];
|
|
3206
3258
|
for (const track of deformTracks) {
|
|
3207
3259
|
const skinName = track.skin ?? 'default';
|
|
@@ -3226,7 +3278,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3226
3278
|
`(it has: ${Object.keys(perSlot).join(', ')})`,
|
|
3227
3279
|
);
|
|
3228
3280
|
}
|
|
3229
|
-
if (deformTimelines
|
|
3281
|
+
if (deformTimelines.get(skinName)?.get(track.slot)?.has(track.attachment)) {
|
|
3230
3282
|
throw new CompileError(`${at}: two deform timelines on one attachment; merge them into one`);
|
|
3231
3283
|
}
|
|
3232
3284
|
const keys = compileDeformTrack(
|
|
@@ -3235,13 +3287,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3235
3287
|
animName,
|
|
3236
3288
|
anim.duration,
|
|
3237
3289
|
{
|
|
3238
|
-
...deformGeometryOf(attachment, at,
|
|
3290
|
+
...deformGeometryOf(attachment, at, transforms),
|
|
3239
3291
|
depth: attachmentDepths.get(`${skinName}/${track.slot}/${track.attachment}`) ?? null,
|
|
3240
3292
|
},
|
|
3241
3293
|
deformTransforms,
|
|
3242
3294
|
);
|
|
3243
|
-
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time
|
|
3244
|
-
(
|
|
3295
|
+
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3296
|
+
attachmentTimelinesOf(skinName, track.slot).set(track.attachment, { deform: keys });
|
|
3245
3297
|
}
|
|
3246
3298
|
|
|
3247
3299
|
// -- sequence timelines: the other attachment timeline (issue #729) -----
|
|
@@ -3273,22 +3325,22 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3273
3325
|
);
|
|
3274
3326
|
}
|
|
3275
3327
|
const keys = compileSequenceTrack(track, anim.duration, attachment, at);
|
|
3276
|
-
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time
|
|
3277
|
-
const slot = (
|
|
3278
|
-
if (slot
|
|
3328
|
+
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3329
|
+
const slot = attachmentTimelinesOf(skinName, track.slot);
|
|
3330
|
+
if (slot.get(track.attachment)?.sequence) {
|
|
3279
3331
|
throw new CompileError(`${at}: two sequence timelines on one attachment; merge them into one`);
|
|
3280
3332
|
}
|
|
3281
|
-
slot
|
|
3333
|
+
slot.set(track.attachment, { ...(slot.get(track.attachment) ?? {}), sequence: keys });
|
|
3282
3334
|
}
|
|
3283
3335
|
|
|
3284
|
-
const drawOrder = anim.drawOrder ? compileDrawOrder(anim.drawOrder, animName, anim.duration, slots) :
|
|
3285
|
-
|
|
3336
|
+
const drawOrder = anim.drawOrder ? compileDrawOrder(anim.drawOrder, animName, anim.duration, slots) : [];
|
|
3337
|
+
for (const key of drawOrder) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3286
3338
|
|
|
3287
|
-
const eventKeys = anim.events ? compileEvents(anim.events, animName, anim.duration, rig.events ?? {}) :
|
|
3339
|
+
const eventKeys = anim.events ? compileEvents(anim.events, animName, anim.duration, rig.events ?? {}) : [];
|
|
3288
3340
|
// An event timeline counts towards the animation's length the same as any
|
|
3289
3341
|
// other: `readAnimation` takes the duration from the longest timeline it
|
|
3290
3342
|
// built, and `EventTimeline.getDuration()` is its last frame like the rest.
|
|
3291
|
-
|
|
3343
|
+
for (const key of eventKeys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3292
3344
|
|
|
3293
3345
|
// Rule 4: the declared duration is verified, because skeleton JSON does not
|
|
3294
3346
|
// carry one — the loader takes the max key time.
|
|
@@ -3304,133 +3356,101 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3304
3356
|
`animation "${animName}" declares duration ${anim.duration}s but its last key is at ${compiledDuration}s`,
|
|
3305
3357
|
);
|
|
3306
3358
|
}
|
|
3307
|
-
//
|
|
3308
|
-
//
|
|
3309
|
-
//
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3359
|
+
// The model's record of the animation. Which groups the file carries, in
|
|
3360
|
+
// what order and under what spelling is the Spine emitter's
|
|
3361
|
+
// (`emitAnimation`: `readAnimation`'s reading order, each group only when
|
|
3362
|
+
// non-empty).
|
|
3363
|
+
animations.set(animName, {
|
|
3364
|
+
duration: anim.duration,
|
|
3365
|
+
bones: boneTimelines,
|
|
3366
|
+
slots: slotTimelines,
|
|
3367
|
+
constraints: {
|
|
3368
|
+
ik: constraintTimelines.ik,
|
|
3369
|
+
transform: constraintTimelines.transform,
|
|
3370
|
+
path: familyTimelines.path,
|
|
3371
|
+
physics: familyTimelines.physics,
|
|
3372
|
+
slider: familyTimelines.slider,
|
|
3373
|
+
},
|
|
3374
|
+
attachments: deformTimelines,
|
|
3375
|
+
drawOrder,
|
|
3376
|
+
events: eventKeys,
|
|
3377
|
+
});
|
|
3323
3378
|
}
|
|
3324
3379
|
});
|
|
3325
3380
|
|
|
3326
3381
|
// -- 6. assemble -----------------------------------------------------------
|
|
3327
3382
|
//
|
|
3328
|
-
//
|
|
3383
|
+
// What the skeleton carries that the model does not hold: the header. The
|
|
3384
|
+
// stage is four fields or none of them — `x`/`y` are the origin of the box
|
|
3329
3385
|
// `width`/`height` give an extent to, so a header carrying an origin for a box
|
|
3330
|
-
// it does not declare would be a shape no export has
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
// Event definitions. Emitted in the order the rig spec declares them — object
|
|
3347
|
-
// key order is the spec's, not a set's, so A18 stays a contract.
|
|
3348
|
-
const events: Record<string, SpineEvent> = {};
|
|
3386
|
+
// it does not declare would be a shape no export has (issue #578).
|
|
3387
|
+
const header: SkeletonHeader = {
|
|
3388
|
+
spine: SPINE_VERSION,
|
|
3389
|
+
stage:
|
|
3390
|
+
stageWidth !== undefined && stageHeight !== undefined
|
|
3391
|
+
? { x: rig.skeleton?.x ?? 0, y: rig.skeleton?.y ?? 0, width: stageWidth, height: stageHeight }
|
|
3392
|
+
: null,
|
|
3393
|
+
fps: rig.skeleton?.fps,
|
|
3394
|
+
images: skeletonImagesPath(rig.skeleton?.images, opts, outDir, partDirs),
|
|
3395
|
+
audio: rig.skeleton?.audio,
|
|
3396
|
+
};
|
|
3397
|
+
|
|
3398
|
+
// Event definitions, in the order the rig spec declares them — the map's
|
|
3399
|
+
// insertion order is the spec's, not a set's, so A18 stays a contract.
|
|
3400
|
+
const events = new Map<string, ModelEvent>();
|
|
3349
3401
|
for (const [name, def] of Object.entries(rig.events ?? {})) {
|
|
3350
|
-
const entry:
|
|
3402
|
+
const entry: ModelEvent = {};
|
|
3351
3403
|
if (def.int !== undefined) entry.int = def.int;
|
|
3352
3404
|
if (def.float !== undefined) entry.float = f32(def.float);
|
|
3353
3405
|
if (def.string !== undefined) entry.string = def.string;
|
|
3354
3406
|
if (def.audio !== undefined) entry.audio = def.audio;
|
|
3355
3407
|
if (def.volume !== undefined) entry.volume = f32(def.volume);
|
|
3356
3408
|
if (def.balance !== undefined) entry.balance = f32(def.balance);
|
|
3357
|
-
events
|
|
3409
|
+
events.set(name, entry);
|
|
3358
3410
|
}
|
|
3411
|
+
// Every skin with what it activates and its table, in the spec's order: the
|
|
3412
|
+
// model's record of it. `splitRigSkin`'s lists, or none for a skin the spec
|
|
3413
|
+
// does not declare (a manifest's `default`).
|
|
3414
|
+
const skins: ModelSkin[] = [...skinTables].map(([name, attachments]) => {
|
|
3415
|
+
const parts = skinParts.get(name);
|
|
3416
|
+
return {
|
|
3417
|
+
name,
|
|
3418
|
+
bones: parts?.bones ?? [],
|
|
3419
|
+
constraints: Object.fromEntries(RIG_SKIN_CONSTRAINT_KEYS.map((key) => [key, parts?.constraints[key] ?? []])) as ModelSkin['constraints'],
|
|
3420
|
+
attachments,
|
|
3421
|
+
};
|
|
3422
|
+
});
|
|
3359
3423
|
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
skins
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
name,
|
|
3379
|
-
...(parts?.bones.length ? { bones: parts.bones } : {}),
|
|
3380
|
-
...Object.fromEntries(
|
|
3381
|
-
RIG_SKIN_CONSTRAINT_KEYS.filter((key) => parts?.constraints[key].length).map((key) => [
|
|
3382
|
-
key,
|
|
3383
|
-
parts!.constraints[key],
|
|
3384
|
-
]),
|
|
3385
|
-
),
|
|
3386
|
-
attachments: editorSlotKeyOrder(attachments),
|
|
3387
|
-
};
|
|
3388
|
-
}),
|
|
3389
|
-
),
|
|
3390
|
-
// Between `skins` and `animations`, which is where the editor writes it. A
|
|
3391
|
-
// conditional spread rather than an assignment after the literal, so the key
|
|
3392
|
-
// lands in that position instead of at the end.
|
|
3393
|
-
...(Object.keys(events).length ? { events } : {}),
|
|
3394
|
-
// Keyed in the editor's own order rather than the motion spec's, because a
|
|
3395
|
-
// slider's reference to an animation is an ordinal in the format and the
|
|
3396
|
-
// editor re-sorts this object — see `editorAnimationOrder`. The sort is
|
|
3397
|
-
// applied HERE and not to the loop above, so what the compiler reads, the
|
|
3398
|
-
// order it reports durations in, and which animation a CompileError names
|
|
3399
|
-
// first are all still the spec's own; only the emitted key order moves.
|
|
3400
|
-
animations: editorAnimationOrder(animations),
|
|
3401
|
-
};
|
|
3402
|
-
if (constraints.length) skeleton.constraints = constraints;
|
|
3424
|
+
// The skeleton: the Spine emitter's one entry (issue #922), which writes every
|
|
3425
|
+
// section from the model, in the order and under the spellings the file has,
|
|
3426
|
+
// and runs the parser-default and key-order passes on the finished object.
|
|
3427
|
+
// What it is handed besides the model is what the model does not hold: the
|
|
3428
|
+
// header above, and the editor's orders, which stay here (see `editorSkinOrder` and
|
|
3429
|
+
// `editorAnimationOrder`): the sorts are applied at emission, so everything
|
|
3430
|
+
// this function reads, and the model's `skins` and `animations`, keep the
|
|
3431
|
+
// spec's own order.
|
|
3432
|
+
// The skeleton's reference scale, which wind and gravity act over (issue
|
|
3433
|
+
// #958): the rig's stated number as it is, or the value the parser reads a
|
|
3434
|
+
// header stating none as. The model holds it; the emitter writes it into the
|
|
3435
|
+
// header, where the parser-default pass drops it at 100 as before.
|
|
3436
|
+
const referenceScale = rig.skeleton?.referenceScale ?? UNSTATED_REFERENCE_SCALE;
|
|
3437
|
+
const skeleton = emitSkeleton(
|
|
3438
|
+
{ referenceScale, bones, slots, skins, constraints, events, animations },
|
|
3439
|
+
header,
|
|
3440
|
+
{ skins: editorSkinOrder, slotKeys: editorSlotKeyOrder, animations: editorAnimationOrder },
|
|
3441
|
+
);
|
|
3403
3442
|
|
|
3404
3443
|
for (const slot of slots) {
|
|
3405
3444
|
if (!boneNames.has(slot.bone)) throw new CompileError(`slot "${slot.name}" has no bone`);
|
|
3406
3445
|
}
|
|
3407
3446
|
|
|
3408
|
-
//
|
|
3409
|
-
// one
|
|
3410
|
-
|
|
3411
|
-
// are free to build in whatever order reads best and none of them states the
|
|
3412
|
-
// order a second time. It moves positions and nothing else: no key is added,
|
|
3413
|
-
// dropped or re-valued, and what the gate and `A18` read is this object's text.
|
|
3414
|
-
//
|
|
3415
|
-
// Before it, and on the same finished object: every key whose value is the
|
|
3416
|
-
// one the 4.3 parser reads in its absence is left out, the way the editor
|
|
3417
|
-
// leaves it out (issue #716 tranche 3). One pass beside the other rather than
|
|
3418
|
-
// folded into it, because each has its own table and the selftest plants a
|
|
3419
|
-
// wrong row into each on its own; both walk the object through
|
|
3420
|
-
// `forEachKindedObject`, so what a kind is stays said in one place. The spec
|
|
3421
|
-
// is untouched — an author may still write `x: 0`, and it is still the record
|
|
3422
|
-
// of what was written — and what the parser loads does not move (`S103`).
|
|
3423
|
-
inEditorKeyOrder(withoutParserDefaults(skeleton));
|
|
3424
|
-
|
|
3425
|
-
return {
|
|
3426
|
-
skeleton,
|
|
3427
|
-
skeletonText: `${JSON.stringify(skeleton, null, 2)}\n`,
|
|
3428
|
-
atlasText,
|
|
3447
|
+
// The fields the model carries as `CompileResult` carries them, by reference:
|
|
3448
|
+
// one object, so the result and its model cannot say two different things.
|
|
3449
|
+
const carried: CarriedFromCompileResult = {
|
|
3429
3450
|
images,
|
|
3430
3451
|
pageGrids: atlasIn === null ? [] : [...atlasIn.grids].map(([page, grid]) => ({ page, said: grid.said })),
|
|
3431
3452
|
droppedStates,
|
|
3432
3453
|
absentParts,
|
|
3433
|
-
declaredDurations,
|
|
3434
3454
|
meshBones: [...meshBones],
|
|
3435
3455
|
meshes,
|
|
3436
3456
|
physics: physicsReport,
|
|
@@ -3438,6 +3458,14 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3438
3458
|
trackDerivations,
|
|
3439
3459
|
rig: buildRigInfo(rig, bones, meshes, manifest, images),
|
|
3440
3460
|
};
|
|
3461
|
+
return {
|
|
3462
|
+
skeleton,
|
|
3463
|
+
skeletonText: `${JSON.stringify(skeleton, null, 2)}\n`,
|
|
3464
|
+
atlasText,
|
|
3465
|
+
declaredDurations,
|
|
3466
|
+
...carried,
|
|
3467
|
+
model: { referenceScale, bones, setupWorld: transforms, slots, skins, constraints, events, animations, ...carried },
|
|
3468
|
+
};
|
|
3441
3469
|
}
|
|
3442
3470
|
|
|
3443
3471
|
// ---------------------------------------------------------------------------
|
|
@@ -3453,7 +3481,7 @@ interface BoneContext {
|
|
|
3453
3481
|
}
|
|
3454
3482
|
|
|
3455
3483
|
/**
|
|
3456
|
-
* One rig bone -> one
|
|
3484
|
+
* One rig bone -> one model bone (`ModelBone`); `emitBones` writes it as Spine's.
|
|
3457
3485
|
*
|
|
3458
3486
|
* 🔑 A field is emitted exactly when the spec declared it. That is not Spine's
|
|
3459
3487
|
* own exporter convention (it omits anything equal to the default) and the
|
|
@@ -3461,8 +3489,8 @@ interface BoneContext {
|
|
|
3461
3489
|
* deciding emission from the arithmetic rather than from the author's text makes
|
|
3462
3490
|
* the file depend on a rounding.
|
|
3463
3491
|
*/
|
|
3464
|
-
function buildBone(spec: RigBone, soFar:
|
|
3465
|
-
const bone:
|
|
3492
|
+
function buildBone(spec: RigBone, soFar: ModelBone[], ctx: BoneContext): ModelBone {
|
|
3493
|
+
const bone: ModelBone = { name: spec.name };
|
|
3466
3494
|
if (spec.parent !== undefined) bone.parent = spec.parent;
|
|
3467
3495
|
if (spec.length !== undefined) bone.length = f32(spec.length);
|
|
3468
3496
|
|
|
@@ -3493,10 +3521,13 @@ function buildBone(spec: RigBone, soFar: SpineBone[], ctx: BoneContext): SpineBo
|
|
|
3493
3521
|
if (spec.scaleY !== undefined) bone.scaleY = f32(spec.scaleY);
|
|
3494
3522
|
if (spec.shearX !== undefined) bone.shearX = f32(spec.shearX);
|
|
3495
3523
|
if (spec.shearY !== undefined) bone.shearY = f32(spec.shearY);
|
|
3496
|
-
if (spec.inherit !== undefined) bone.
|
|
3497
|
-
if (spec.skin !== undefined) bone.
|
|
3498
|
-
if (spec.color !== undefined
|
|
3499
|
-
|
|
3524
|
+
if (spec.inherit !== undefined) bone.inheritMode = spec.inherit;
|
|
3525
|
+
if (spec.skin !== undefined) bone.skinRequired = spec.skin;
|
|
3526
|
+
if (spec.color !== undefined || spec.icon !== undefined) {
|
|
3527
|
+
bone.editor = {};
|
|
3528
|
+
if (spec.color !== undefined) bone.editor.color = spec.color;
|
|
3529
|
+
if (spec.icon !== undefined) bone.editor.icon = spec.icon;
|
|
3530
|
+
}
|
|
3500
3531
|
return bone;
|
|
3501
3532
|
}
|
|
3502
3533
|
|
|
@@ -3621,9 +3652,9 @@ function rotationOf(spec: RigBone, ctx: BoneContext): number | null {
|
|
|
3621
3652
|
* with a name was in that set, so for those types the position is the same
|
|
3622
3653
|
* choice rather than a second measurement.
|
|
3623
3654
|
*/
|
|
3624
|
-
function withStatedName(att:
|
|
3655
|
+
function withStatedName(att: SkinTableEntry, stated: RigAttachment): SkinTableEntry {
|
|
3625
3656
|
const name = (stated as { name?: string }).name;
|
|
3626
|
-
return name === undefined ? att :
|
|
3657
|
+
return name === undefined ? att : { name, ...att };
|
|
3627
3658
|
}
|
|
3628
3659
|
|
|
3629
3660
|
/**
|
|
@@ -3734,7 +3765,7 @@ function refuseDefaultSkinContests(skinNames: readonly string[], skinParts: Map<
|
|
|
3734
3765
|
|
|
3735
3766
|
interface AttachmentContext {
|
|
3736
3767
|
images: CompiledImage[];
|
|
3737
|
-
bones:
|
|
3768
|
+
bones: ModelBone[];
|
|
3738
3769
|
transforms: Map<string, BoneTransform>;
|
|
3739
3770
|
meshBones: Set<string>;
|
|
3740
3771
|
meshes: CompileResult['meshes'];
|
|
@@ -3761,6 +3792,12 @@ interface AttachmentContext {
|
|
|
3761
3792
|
depths: Map<string, number[]>;
|
|
3762
3793
|
/** Every slot the rig declares — a clipping attachment's `end` resolves here. */
|
|
3763
3794
|
slotNames: Set<string>;
|
|
3795
|
+
/**
|
|
3796
|
+
* The `--atlas-in` pack's regions by trimmed name, first match — the map
|
|
3797
|
+
* `resolveFromAtlas` reads — or `null` on the loose route, where the regions
|
|
3798
|
+
* are `images`. A region record's rectangle is looked up here (`regionAtlasRect`).
|
|
3799
|
+
*/
|
|
3800
|
+
atlasRegions: ReadonlyMap<string, { region: AtlasRegion }> | null;
|
|
3764
3801
|
/**
|
|
3765
3802
|
* Linked meshes whose `source` is still unresolved — `resolveLinkedMeshes`
|
|
3766
3803
|
* empties it once every skin has been built.
|
|
@@ -3850,7 +3887,7 @@ function buildRigAttachment(
|
|
|
3850
3887
|
placeholder: string,
|
|
3851
3888
|
where: string,
|
|
3852
3889
|
ctx: AttachmentContext,
|
|
3853
|
-
):
|
|
3890
|
+
): SkinTableEntry {
|
|
3854
3891
|
const stated = (att as { type?: unknown }).type;
|
|
3855
3892
|
if (stated !== undefined && typeof stated !== 'string') {
|
|
3856
3893
|
throw new CompileError(
|
|
@@ -3919,7 +3956,8 @@ function deferredAttachmentRefusal(type: string, where: string, how: string): st
|
|
|
3919
3956
|
}
|
|
3920
3957
|
|
|
3921
3958
|
/**
|
|
3922
|
-
*
|
|
3959
|
+
* The polygon a bounding box, a clipping attachment or a path carries, as the
|
|
3960
|
+
* model's `ModelVertices`.
|
|
3923
3961
|
*
|
|
3924
3962
|
* 🚨 `vertexCount` is required, and everything else here is a cross-check of it.
|
|
3925
3963
|
* The parser reads `map.vertexCount << 1` and hands that to `readVertices` as the
|
|
@@ -3929,12 +3967,12 @@ function deferredAttachmentRefusal(type: string, where: string, how: string): st
|
|
|
3929
3967
|
* whatever that garbage produced. It loads. It draws nothing (a bounding box
|
|
3930
3968
|
* never did) and clips nothing, or clips the wrong shape, in complete silence.
|
|
3931
3969
|
*
|
|
3932
|
-
* The two encodings are the mesh's — `
|
|
3970
|
+
* The two encodings are the mesh's — `bindNamedWeights` is the same function —
|
|
3933
3971
|
* because they are the same field with the same trap: `readVertices` decides
|
|
3934
3972
|
* weighted vs unweighted by a length comparison alone, and a coincidental match
|
|
3935
|
-
* reads weight data as coordinates.
|
|
3973
|
+
* reads weight data as coordinates. The model says which one it is outright.
|
|
3936
3974
|
*/
|
|
3937
|
-
function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: AttachmentContext):
|
|
3975
|
+
function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: AttachmentContext): ModelVertices {
|
|
3938
3976
|
const count = att.vertexCount;
|
|
3939
3977
|
if (typeof count !== 'number' || !Number.isInteger(count) || count < 3) {
|
|
3940
3978
|
throw new CompileError(
|
|
@@ -3954,7 +3992,7 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
|
|
|
3954
3992
|
if (att.weights.length !== count) {
|
|
3955
3993
|
throw new CompileError(`${where}: weights cover ${att.weights.length} vertices but vertexCount is ${count}`);
|
|
3956
3994
|
}
|
|
3957
|
-
return
|
|
3995
|
+
return { weighted: true, bindings: bindNamedWeights(att.weights, where, ctx) };
|
|
3958
3996
|
}
|
|
3959
3997
|
const raw = att.vertices;
|
|
3960
3998
|
if (!raw || raw.length === 0) {
|
|
@@ -3973,48 +4011,89 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
|
|
|
3973
4011
|
}
|
|
3974
4012
|
// A raw run still has to decode to exactly `vertexCount` vertices, or the
|
|
3975
4013
|
// count and the polygon disagree and the parser believes the count.
|
|
3976
|
-
|
|
3977
|
-
for (let i = 0; i < raw.length; decoded++) {
|
|
3978
|
-
const bones = raw[i++];
|
|
3979
|
-
if (!Number.isInteger(bones) || bones < 1) {
|
|
3980
|
-
throw new CompileError(`${where}: the raw weighted run has a bone count of ${String(bones)} at index ${i - 1}`);
|
|
3981
|
-
}
|
|
3982
|
-
i += bones * 4;
|
|
3983
|
-
if (i > raw.length) {
|
|
3984
|
-
throw new CompileError(
|
|
3985
|
-
`${where}: the raw weighted run is truncated — vertex ${decoded} claims ${bones} bone(s) and the array ends first`,
|
|
3986
|
-
);
|
|
3987
|
-
}
|
|
3988
|
-
}
|
|
3989
|
-
if (decoded !== count) {
|
|
3990
|
-
throw new CompileError(`${where}: the raw weighted run decodes to ${decoded} vertices but vertexCount is ${count}`);
|
|
3991
|
-
}
|
|
3992
|
-
// Register the bones it binds so the mesh-bone reports stay complete.
|
|
3993
|
-
for (let i = 0; i < raw.length; ) {
|
|
3994
|
-
const bones = raw[i++];
|
|
3995
|
-
for (let k = 0; k < bones; k++, i += 4) {
|
|
3996
|
-
const bone = ctx.bones[raw[i]];
|
|
3997
|
-
if (bone) ctx.meshBones.add(bone.name);
|
|
3998
|
-
}
|
|
3999
|
-
}
|
|
4014
|
+
checkRawRunShape(raw, count, `vertexCount is ${count}`, where);
|
|
4000
4015
|
}
|
|
4001
4016
|
for (const n of raw) {
|
|
4002
4017
|
if (!Number.isFinite(n)) throw new CompileError(`${where}: the vertex array holds a non-finite value ${String(n)}`);
|
|
4003
4018
|
}
|
|
4004
|
-
return raw.map(f32);
|
|
4019
|
+
if (unweighted) return { weighted: false, xy: raw.map(f32) };
|
|
4020
|
+
const bindings = bindRawRun(raw.map(f32), ctx.bones, where);
|
|
4021
|
+
// Register the bones it binds so the mesh-bone reports stay complete.
|
|
4022
|
+
for (const vertex of bindings) for (const binding of vertex) ctx.meshBones.add(binding.bone);
|
|
4023
|
+
return { weighted: true, bindings };
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
/**
|
|
4027
|
+
* Refuse a raw weighted run whose shape is not `vertices` vertices of
|
|
4028
|
+
* `boneCount, (boneIndex, x, y, weight) × boneCount` each. `countSays` is the
|
|
4029
|
+
* clause naming where the expected count came from.
|
|
4030
|
+
*/
|
|
4031
|
+
function checkRawRunShape(raw: readonly number[], vertices: number, countSays: string, where: string): void {
|
|
4032
|
+
let decoded = 0;
|
|
4033
|
+
for (let i = 0; i < raw.length; decoded++) {
|
|
4034
|
+
const bones = raw[i++];
|
|
4035
|
+
if (!Number.isInteger(bones) || bones < 1) {
|
|
4036
|
+
throw new CompileError(`${where}: the raw weighted run has a bone count of ${String(bones)} at index ${i - 1}`);
|
|
4037
|
+
}
|
|
4038
|
+
i += bones * 4;
|
|
4039
|
+
if (i > raw.length) {
|
|
4040
|
+
throw new CompileError(
|
|
4041
|
+
`${where}: the raw weighted run is truncated — vertex ${decoded} claims ${bones} bone(s) and the array ends first`,
|
|
4042
|
+
);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
if (decoded !== vertices) {
|
|
4046
|
+
throw new CompileError(`${where}: the raw weighted run decodes to ${decoded} vertices but ${countSays}`);
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
/**
|
|
4051
|
+
* Decode an authored raw run (`"boneIndexing": "raw"`) to bindings by name, ONCE,
|
|
4052
|
+
* at intake, against the model's bone order — the order the emitter writes the
|
|
4053
|
+
* indexes back in, so the emitted run is the run the spec stated.
|
|
4054
|
+
*
|
|
4055
|
+
* The shape is already checked (`checkRawRunShape`). An index that names no bone
|
|
4056
|
+
* — past the end of the list, negative, fractional — is refused here with the
|
|
4057
|
+
* sentence the path-length measurement used to raise for it; before issue #917
|
|
4058
|
+
* every other reader of the run skipped it at compile time and left it to the
|
|
4059
|
+
* gate (`A20` on a mesh, `A33` on the other three).
|
|
4060
|
+
*/
|
|
4061
|
+
function bindRawRun(run: readonly number[], bones: readonly ModelBone[], where: string): ModelBinding[][] {
|
|
4062
|
+
const out: ModelBinding[][] = [];
|
|
4063
|
+
for (let i = 0; i < run.length; ) {
|
|
4064
|
+
const count = run[i++];
|
|
4065
|
+
const vertex: ModelBinding[] = [];
|
|
4066
|
+
for (let k = 0; k < count; k++, i += 4) {
|
|
4067
|
+
const bone = bones[run[i]];
|
|
4068
|
+
if (bone === undefined) {
|
|
4069
|
+
throw new CompileError(`${where}: vertex ${out.length} binds bone index ${run[i]}, which is not in the bone list`);
|
|
4070
|
+
}
|
|
4071
|
+
vertex.push({ bone: bone.name, x: run[i + 1], y: run[i + 2], weight: run[i + 3] });
|
|
4072
|
+
}
|
|
4073
|
+
out.push(vertex);
|
|
4074
|
+
}
|
|
4075
|
+
return out;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
/** Every number of a weighted binding list on the float32 grid — what `.map(f32)` did to the run. */
|
|
4079
|
+
function bindingsOnF32(vertices: Extract<ModelVertices, { weighted: true }>): Extract<ModelVertices, { weighted: true }> {
|
|
4080
|
+
return {
|
|
4081
|
+
weighted: true,
|
|
4082
|
+
bindings: vertices.bindings.map((vertex) => vertex.map((b) => ({ bone: b.bone, x: f32(b.x), y: f32(b.y), weight: f32(b.weight) }))),
|
|
4083
|
+
};
|
|
4005
4084
|
}
|
|
4006
4085
|
|
|
4007
4086
|
function buildRigBoundingBox(
|
|
4008
4087
|
att: RigBoundingBoxAttachment,
|
|
4009
4088
|
where: string,
|
|
4010
4089
|
ctx: AttachmentContext,
|
|
4011
|
-
):
|
|
4012
|
-
const out:
|
|
4013
|
-
|
|
4090
|
+
): ModelBoundingBoxAttachment {
|
|
4091
|
+
const out: ModelBoundingBoxAttachment = {
|
|
4092
|
+
kind: 'boundingbox',
|
|
4014
4093
|
vertexCount: att.vertexCount,
|
|
4015
4094
|
vertices: buildVertexGeometry(att, where, ctx),
|
|
4016
4095
|
};
|
|
4017
|
-
if (att.color !== undefined) out.
|
|
4096
|
+
if (att.color !== undefined) out.editorColor = att.color;
|
|
4018
4097
|
return out;
|
|
4019
4098
|
}
|
|
4020
4099
|
|
|
@@ -4022,7 +4101,7 @@ function buildRigClipping(
|
|
|
4022
4101
|
att: RigClippingAttachment,
|
|
4023
4102
|
where: string,
|
|
4024
4103
|
ctx: AttachmentContext,
|
|
4025
|
-
):
|
|
4104
|
+
): ModelClippingAttachment {
|
|
4026
4105
|
if (att.end !== undefined) {
|
|
4027
4106
|
// `skeletonData.findSlot` returns null on a miss and the parser assigns that
|
|
4028
4107
|
// null (`:626-627`), so a typo does not fail — the clip simply never ends and
|
|
@@ -4034,57 +4113,50 @@ function buildRigClipping(
|
|
|
4034
4113
|
);
|
|
4035
4114
|
}
|
|
4036
4115
|
}
|
|
4037
|
-
// Field order is the
|
|
4038
|
-
//
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
const out: SpineClippingAttachment = {
|
|
4042
|
-
type: 'clipping',
|
|
4116
|
+
// Field order is the emitter's (`emitClipping`); a present key here is one the
|
|
4117
|
+
// spec declared.
|
|
4118
|
+
const out: ModelClippingAttachment = {
|
|
4119
|
+
kind: 'clipping',
|
|
4043
4120
|
...(att.end !== undefined ? { end: att.end } : {}),
|
|
4044
4121
|
...(att.convex !== undefined ? { convex: att.convex } : {}),
|
|
4045
4122
|
...(att.inverse !== undefined ? { inverse: att.inverse } : {}),
|
|
4046
4123
|
vertexCount: att.vertexCount,
|
|
4047
4124
|
vertices: buildVertexGeometry(att, where, ctx),
|
|
4048
4125
|
};
|
|
4049
|
-
if (att.color !== undefined) out.
|
|
4126
|
+
if (att.color !== undefined) out.editorColor = att.color;
|
|
4050
4127
|
return out;
|
|
4051
4128
|
}
|
|
4052
4129
|
|
|
4053
4130
|
/**
|
|
4054
|
-
* Setup-pose world position of every vertex of
|
|
4131
|
+
* Setup-pose world position of every vertex of a model vertex attachment.
|
|
4055
4132
|
*
|
|
4056
4133
|
* The two encodings again, and the same split `readVertices` makes: an unweighted
|
|
4057
|
-
*
|
|
4058
|
-
*
|
|
4059
|
-
*
|
|
4060
|
-
*
|
|
4061
|
-
*
|
|
4134
|
+
* attachment is one `x, y` in the SLOT BONE's space; a weighted one binds each
|
|
4135
|
+
* vertex to its bones by name, and the vertex is the weighted sum of each
|
|
4136
|
+
* influence's bind point taken to world through its own bone. This is
|
|
4137
|
+
* `VertexAttachment.computeWorldVertices` at setup, restated in the compiler
|
|
4138
|
+
* because the compiler must not link the runtime.
|
|
4062
4139
|
*/
|
|
4063
4140
|
function setupWorldVertices(
|
|
4064
|
-
vertices:
|
|
4065
|
-
vertexCount: number,
|
|
4141
|
+
vertices: ModelVertices,
|
|
4066
4142
|
anchor: BoneTransform,
|
|
4067
|
-
bones: SpineBone[],
|
|
4068
4143
|
transforms: Map<string, BoneTransform>,
|
|
4069
4144
|
where: string,
|
|
4070
4145
|
): Array<[number, number]> {
|
|
4071
4146
|
const out: Array<[number, number]> = [];
|
|
4072
|
-
if (vertices.
|
|
4073
|
-
for (let i = 0; i < vertices.length; i += 2) out.push(toWorld(anchor, vertices[i], vertices[i + 1]));
|
|
4147
|
+
if (!vertices.weighted) {
|
|
4148
|
+
for (let i = 0; i < vertices.xy.length; i += 2) out.push(toWorld(anchor, vertices.xy[i], vertices.xy[i + 1]));
|
|
4074
4149
|
return out;
|
|
4075
4150
|
}
|
|
4076
|
-
for (
|
|
4077
|
-
const count = vertices[i++];
|
|
4151
|
+
for (const vertex of vertices.bindings) {
|
|
4078
4152
|
let x = 0;
|
|
4079
4153
|
let y = 0;
|
|
4080
|
-
for (
|
|
4081
|
-
const
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
x += wx * weight;
|
|
4087
|
-
y += wy * weight;
|
|
4154
|
+
for (const binding of vertex) {
|
|
4155
|
+
const m = transforms.get(binding.bone);
|
|
4156
|
+
if (!m) throw new CompileError(`${where}: vertex ${out.length} binds bone "${binding.bone}", which has no setup transform`);
|
|
4157
|
+
const [wx, wy] = toWorld(m, binding.x, binding.y);
|
|
4158
|
+
x += wx * binding.weight;
|
|
4159
|
+
y += wy * binding.weight;
|
|
4088
4160
|
}
|
|
4089
4161
|
out.push([x, y]);
|
|
4090
4162
|
}
|
|
@@ -4113,8 +4185,11 @@ function setupWorldVertices(
|
|
|
4113
4185
|
* prefix, so the entries the runtime reads are the same numbers either way; the
|
|
4114
4186
|
* trailing one is read by nothing, and is written so a rebuild is the file the
|
|
4115
4187
|
* editor writes rather than one entry short of it.
|
|
4188
|
+
*
|
|
4189
|
+
* Exported, with `pathCurveLengths`, for the selftest alone: `MV07` measures a
|
|
4190
|
+
* decode of the emitted run through the two and compares.
|
|
4116
4191
|
*/
|
|
4117
|
-
function pathChain(points: Array<[number, number]>, closed: boolean): Array<[number, number]> {
|
|
4192
|
+
export function pathChain(points: Array<[number, number]>, closed: boolean): Array<[number, number]> {
|
|
4118
4193
|
if (!closed) return points.slice(1, points.length - 1);
|
|
4119
4194
|
return [...points.slice(1), points[0], points[1]];
|
|
4120
4195
|
}
|
|
@@ -4181,7 +4256,7 @@ function pathChain(points: Array<[number, number]>, closed: boolean): Array<[num
|
|
|
4181
4256
|
* running total carried across curves rather than restarted. Each of those is a
|
|
4182
4257
|
* place where a more accurate line would emit a different file.
|
|
4183
4258
|
*/
|
|
4184
|
-
function pathCurveLengths(chain: Array<[number, number]>): number[] {
|
|
4259
|
+
export function pathCurveLengths(chain: Array<[number, number]>): number[] {
|
|
4185
4260
|
const out: number[] = [];
|
|
4186
4261
|
let total = 0;
|
|
4187
4262
|
for (let c = 0; c + 3 < chain.length; c += 3) {
|
|
@@ -4235,7 +4310,7 @@ function pathCurveLengths(chain: Array<[number, number]>): number[] {
|
|
|
4235
4310
|
* 3. **An omitted one is measured**, `vertexCount / 3` entries over the closed
|
|
4236
4311
|
* chain (`pathChain`), on the unconstrained setup pose.
|
|
4237
4312
|
*/
|
|
4238
|
-
function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentContext):
|
|
4313
|
+
function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentContext): ModelPathAttachment {
|
|
4239
4314
|
const vertices = buildVertexGeometry(att, where, ctx);
|
|
4240
4315
|
const count = att.vertexCount;
|
|
4241
4316
|
const closed = att.closed === true;
|
|
@@ -4260,7 +4335,7 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
|
|
|
4260
4335
|
} else {
|
|
4261
4336
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
4262
4337
|
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
4263
|
-
const points = setupWorldVertices(vertices,
|
|
4338
|
+
const points = setupWorldVertices(vertices, anchor, ctx.transforms, where);
|
|
4264
4339
|
lengths = pathCurveLengths(pathChain(points, true));
|
|
4265
4340
|
// The total the runtime reads is the last CURVE's entry, `lengths[vertexCount
|
|
4266
4341
|
// / 3 - (closed ? 1 : 2)]` (`PathConstraint.js:204-206`) — on an open path
|
|
@@ -4273,17 +4348,17 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
|
|
|
4273
4348
|
);
|
|
4274
4349
|
}
|
|
4275
4350
|
}
|
|
4276
|
-
//
|
|
4277
|
-
//
|
|
4278
|
-
//
|
|
4351
|
+
// Each optional key is present exactly when the spec declared it — the rule
|
|
4352
|
+
// the whole rig spec follows, so Spine's own defaults stand for the rest. The
|
|
4353
|
+
// field order (the parser's reading order, `:606-623`) is `emitPath`'s.
|
|
4279
4354
|
return {
|
|
4280
|
-
|
|
4355
|
+
kind: 'path',
|
|
4281
4356
|
...(att.closed !== undefined ? { closed: att.closed } : {}),
|
|
4282
4357
|
...(att.constantSpeed !== undefined ? { constantSpeed: att.constantSpeed } : {}),
|
|
4283
4358
|
vertexCount: count,
|
|
4284
4359
|
vertices,
|
|
4285
4360
|
lengths: lengths.map(f32),
|
|
4286
|
-
...(att.color !== undefined ? {
|
|
4361
|
+
...(att.color !== undefined ? { editorColor: att.color } : {}),
|
|
4287
4362
|
};
|
|
4288
4363
|
}
|
|
4289
4364
|
|
|
@@ -4412,8 +4487,9 @@ function attachmentPath(att: { name?: string; path?: string; image?: string }, p
|
|
|
4412
4487
|
}
|
|
4413
4488
|
|
|
4414
4489
|
/**
|
|
4415
|
-
* A mesh attachment's `path` and `color
|
|
4416
|
-
*
|
|
4490
|
+
* A mesh attachment's `path` and `color` — the two values every mesh builder
|
|
4491
|
+
* spreads into its record, and which `emitMesh` (`src/emit_spine.ts`) writes
|
|
4492
|
+
* right after `type` (issue #791; the position moved to the emitter with #917).
|
|
4417
4493
|
*
|
|
4418
4494
|
* 🔬 The editor writes a mesh `type, path, color, uvs, …`: right after `type`,
|
|
4419
4495
|
* `path` before `color` — measured on a production set (#791), not on
|
|
@@ -4422,16 +4498,17 @@ function attachmentPath(att: { name?: string; path?: string; image?: string }, p
|
|
|
4422
4498
|
* `path` before `color` is the order the two measured positions leave, not a
|
|
4423
4499
|
* third measurement. The twelve exports carry neither key on a mesh, which is
|
|
4424
4500
|
* why `EDITOR_KEY_ORDER`'s `mesh attachment` row does not list them: an unlisted
|
|
4425
|
-
* key keeps its constructor's index, so the position is stated
|
|
4426
|
-
* row stays what the
|
|
4501
|
+
* key keeps its constructor's index, so the position is stated in the one
|
|
4502
|
+
* constructor that writes the bytes, `emitMesh`, and the row stays what the
|
|
4503
|
+
* public exports derive.
|
|
4427
4504
|
*
|
|
4428
4505
|
* ⚠️ Not a linked mesh's, and not a region's. Neither was in the measured set —
|
|
4429
4506
|
* a region's `path` stays where `buildRigRegion` puts it, a linked mesh's
|
|
4430
4507
|
* keys where `buildRigLinkedMesh` does — because the analogy to a mesh is
|
|
4431
4508
|
* obvious and it is not a measurement.
|
|
4432
4509
|
*/
|
|
4433
|
-
function meshTextureKeys(att: { name?: string; path?: string; image?: string; color?: string }, placeholder: string): Pick<
|
|
4434
|
-
const out: Pick<
|
|
4510
|
+
function meshTextureKeys(att: { name?: string; path?: string; image?: string; color?: string }, placeholder: string): Pick<ModelMeshAttachment, 'path' | 'color'> {
|
|
4511
|
+
const out: Pick<ModelMeshAttachment, 'path' | 'color'> = {};
|
|
4435
4512
|
const path = attachmentPath(att, placeholder);
|
|
4436
4513
|
if (path !== undefined) out.path = path;
|
|
4437
4514
|
if (att.color !== undefined) out.color = att.color;
|
|
@@ -4455,9 +4532,9 @@ function sequenceFrameRegion(stem: string, seq: RigSequence, i: number): string
|
|
|
4455
4532
|
return `${stem}${'0'.repeat(Math.max(0, (seq.digits ?? 0) - frame.length))}${frame}`;
|
|
4456
4533
|
}
|
|
4457
4534
|
|
|
4458
|
-
/** The
|
|
4459
|
-
function
|
|
4460
|
-
const out:
|
|
4535
|
+
/** The model's `sequence` record: the four fields exactly as the spec stated them. */
|
|
4536
|
+
function buildSequence(seq: RigSequence): ModelSequence {
|
|
4537
|
+
const out: ModelSequence = { count: seq.count };
|
|
4461
4538
|
if (seq.start !== undefined) out.start = seq.start;
|
|
4462
4539
|
if (seq.digits !== undefined) out.digits = seq.digits;
|
|
4463
4540
|
if (seq.setup !== undefined) out.setup = seq.setup;
|
|
@@ -4545,10 +4622,11 @@ function buildRigRegion(
|
|
|
4545
4622
|
placeholder: string,
|
|
4546
4623
|
where: string,
|
|
4547
4624
|
ctx: AttachmentContext,
|
|
4548
|
-
):
|
|
4625
|
+
): ModelRegionAttachment {
|
|
4549
4626
|
if (att.sequence !== undefined) {
|
|
4550
|
-
const
|
|
4551
|
-
const
|
|
4627
|
+
const frames = sequenceFrames(att, placeholder, where, ctx);
|
|
4628
|
+
const size = sequenceFrameSize(frames, att, where);
|
|
4629
|
+
const out: ModelRegionAttachment = { kind: 'region', width: f32(size.width!), height: f32(size.height!) };
|
|
4552
4630
|
if (att.path !== undefined) out.path = att.path;
|
|
4553
4631
|
if (att.x !== undefined) out.x = f32(att.x);
|
|
4554
4632
|
if (att.y !== undefined) out.y = f32(att.y);
|
|
@@ -4556,7 +4634,9 @@ function buildRigRegion(
|
|
|
4556
4634
|
if (att.scaleX !== undefined) out.scaleX = f32(att.scaleX);
|
|
4557
4635
|
if (att.scaleY !== undefined) out.scaleY = f32(att.scaleY);
|
|
4558
4636
|
if (att.color !== undefined) out.color = att.color;
|
|
4559
|
-
|
|
4637
|
+
// Each frame draws through its own rectangle (`Sequence.apply`), in frame order.
|
|
4638
|
+
const sequence: ModelRegionSequence = { ...buildSequence(att.sequence), atlas: frames.map(imageAtlasRect) };
|
|
4639
|
+
out.sequence = sequence;
|
|
4560
4640
|
return out;
|
|
4561
4641
|
}
|
|
4562
4642
|
const img = att.image === undefined ? null : atlasedImage(att.image, where, ctx);
|
|
@@ -4598,7 +4678,7 @@ function buildRigRegion(
|
|
|
4598
4678
|
`${where}: a region needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
4599
4679
|
);
|
|
4600
4680
|
}
|
|
4601
|
-
const out:
|
|
4681
|
+
const out: ModelRegionAttachment = { kind: 'region', width: f32(width), height: f32(height) };
|
|
4602
4682
|
const path = attachmentPath(att, placeholder);
|
|
4603
4683
|
if (path !== undefined) out.path = path;
|
|
4604
4684
|
if (att.x !== undefined) out.x = f32(att.x);
|
|
@@ -4607,41 +4687,89 @@ function buildRigRegion(
|
|
|
4607
4687
|
if (att.scaleX !== undefined) out.scaleX = f32(att.scaleX);
|
|
4608
4688
|
if (att.scaleY !== undefined) out.scaleY = f32(att.scaleY);
|
|
4609
4689
|
if (att.color !== undefined) out.color = att.color;
|
|
4690
|
+
// The region the runtime asks the atlas for: `path`, else the name
|
|
4691
|
+
// (`getValue(map, "path", name)`), which is the stated `name`, else the placeholder.
|
|
4692
|
+
out.atlas = regionAtlasRect(path ?? att.name ?? placeholder, ctx.images, ctx.atlasRegions);
|
|
4610
4693
|
return out;
|
|
4611
4694
|
}
|
|
4612
4695
|
|
|
4613
4696
|
/**
|
|
4614
|
-
*
|
|
4697
|
+
* The rectangle a region draws through, as the atlas states it — the six
|
|
4698
|
+
* fields `ModelAtlasRect` holds, copied, nothing derived.
|
|
4699
|
+
*/
|
|
4700
|
+
function atlasRectOf(region: AtlasRegion): ModelAtlasRect {
|
|
4701
|
+
return {
|
|
4702
|
+
width: region.width,
|
|
4703
|
+
height: region.height,
|
|
4704
|
+
offsetX: region.offsetX,
|
|
4705
|
+
offsetY: region.offsetY,
|
|
4706
|
+
originalWidth: region.originalWidth,
|
|
4707
|
+
originalHeight: region.originalHeight,
|
|
4708
|
+
};
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
/**
|
|
4712
|
+
* The rectangle an atlased image draws through: the pack's region under
|
|
4713
|
+
* `--atlas-in`, else the loose PNG as its own page, which is exactly the region
|
|
4714
|
+
* `buildAtlasText` writes for it — kept size and original size the PNG's,
|
|
4715
|
+
* offsets 0.
|
|
4716
|
+
*/
|
|
4717
|
+
function imageAtlasRect(img: CompiledImage): ModelAtlasRect {
|
|
4718
|
+
if (img.atlas !== undefined) return atlasRectOf(img.atlas);
|
|
4719
|
+
return { width: img.width, height: img.height, offsetX: 0, offsetY: 0, originalWidth: img.width, originalHeight: img.height };
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
/**
|
|
4723
|
+
* The rectangle of the region named `name` in the build's one atlas source —
|
|
4724
|
+
* the `--atlas-in` pack when there is one, else the images atlased from loose
|
|
4725
|
+
* PNGs — or `null` when that source has no region of the name (issue #935).
|
|
4615
4726
|
*
|
|
4616
|
-
*
|
|
4727
|
+
* `null` is the build saying it has no source, never a guessed trim of 0: the
|
|
4728
|
+
* atlas this build emits is that same source, so the region is missing from it
|
|
4729
|
+
* too, and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refuses the build by the name.
|
|
4730
|
+
*/
|
|
4731
|
+
function regionAtlasRect(
|
|
4732
|
+
name: string,
|
|
4733
|
+
images: readonly CompiledImage[],
|
|
4734
|
+
atlasRegions: ReadonlyMap<string, { region: AtlasRegion }> | null,
|
|
4735
|
+
): ModelAtlasRect | null {
|
|
4736
|
+
if (atlasRegions !== null) {
|
|
4737
|
+
const found = atlasRegions.get(name);
|
|
4738
|
+
return found === undefined ? null : atlasRectOf(found.region);
|
|
4739
|
+
}
|
|
4740
|
+
const img = images.find((im) => im.region === name);
|
|
4741
|
+
return img === undefined ? null : imageAtlasRect(img);
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
/**
|
|
4745
|
+
* Resolve an authored attachment's by-name weights into the model's bindings.
|
|
4746
|
+
*
|
|
4747
|
+
* 🚨 This is the whole point of the `weights` form. Spine's run is
|
|
4617
4748
|
* `boneCount, (boneIndex, bindX, bindY, weight) x n` per vertex and those
|
|
4618
4749
|
* indices are positions in the emitted bone array — a thing the rig spec never
|
|
4619
|
-
* writes.
|
|
4750
|
+
* writes. Keeping the NAME here, and letting the emitter write the index from
|
|
4751
|
+
* the model's bone order (`emitVertices`), is what makes "insert a bone" a
|
|
4620
4752
|
* renumbering rather than a rebinding: the names still point at the same bones,
|
|
4621
4753
|
* so the emitted indices move and the mesh does not.
|
|
4622
4754
|
*
|
|
4623
4755
|
* An unknown name is a `CompileError`, the same as a bone's `parent`, a slot's
|
|
4624
4756
|
* `bone` or a constraint's `target`. The alternative — the raw form — cannot
|
|
4625
|
-
* refuse
|
|
4757
|
+
* refuse a rebind, because an index has no name to be wrong.
|
|
4626
4758
|
*/
|
|
4627
|
-
function
|
|
4628
|
-
|
|
4629
|
-
weights.forEach((vertex, i) => {
|
|
4759
|
+
function bindNamedWeights(weights: RigMeshBinding[][], where: string, ctx: AttachmentContext): ModelBinding[][] {
|
|
4760
|
+
return weights.map((vertex, i) => {
|
|
4630
4761
|
if (!Array.isArray(vertex) || vertex.length === 0) {
|
|
4631
4762
|
throw new CompileError(`${where}: vertex ${i} has no bone bindings; a weighted vertex names at least one bone`);
|
|
4632
4763
|
}
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
const index = ctx.bones.findIndex((b) => b.name === binding.bone);
|
|
4636
|
-
if (index < 0) {
|
|
4764
|
+
return vertex.map((binding) => {
|
|
4765
|
+
if (!ctx.bones.some((b) => b.name === binding.bone)) {
|
|
4637
4766
|
throw new CompileError(
|
|
4638
4767
|
`${where}: vertex ${i} binds bone ${JSON.stringify(binding.bone)}, which the rig does not declare as a bone`,
|
|
4639
4768
|
);
|
|
4640
4769
|
}
|
|
4641
|
-
|
|
4642
|
-
}
|
|
4770
|
+
return { bone: binding.bone, x: f32(binding.x), y: f32(binding.y), weight: f32(binding.weight) };
|
|
4771
|
+
});
|
|
4643
4772
|
});
|
|
4644
|
-
return out;
|
|
4645
4773
|
}
|
|
4646
4774
|
|
|
4647
4775
|
/**
|
|
@@ -4786,7 +4914,7 @@ function buildRigMesh(
|
|
|
4786
4914
|
placeholder: string,
|
|
4787
4915
|
where: string,
|
|
4788
4916
|
ctx: AttachmentContext,
|
|
4789
|
-
):
|
|
4917
|
+
): ModelMeshAttachment {
|
|
4790
4918
|
const authored =
|
|
4791
4919
|
att.uvs !== undefined || att.triangles !== undefined || att.vertices !== undefined || att.weights !== undefined;
|
|
4792
4920
|
if (authored && att.generator) {
|
|
@@ -4802,7 +4930,7 @@ function buildRigMesh(
|
|
|
4802
4930
|
throw new CompileError(`${where}: an authored mesh needs uvs, triangles and vertices or weights (or a "generator")`);
|
|
4803
4931
|
}
|
|
4804
4932
|
const uvCount = att.uvs.length;
|
|
4805
|
-
let vertices:
|
|
4933
|
+
let vertices: ModelVertices;
|
|
4806
4934
|
let boundBones: string[] = [];
|
|
4807
4935
|
if (att.weights) {
|
|
4808
4936
|
if (att.boneIndexing === 'raw') {
|
|
@@ -4813,7 +4941,7 @@ function buildRigMesh(
|
|
|
4813
4941
|
`${where}: weights cover ${att.weights.length} vertices but there are ${uvCount / 2} uv pairs`,
|
|
4814
4942
|
);
|
|
4815
4943
|
}
|
|
4816
|
-
vertices =
|
|
4944
|
+
vertices = { weighted: true, bindings: bindNamedWeights(att.weights, where, ctx) };
|
|
4817
4945
|
boundBones = [...new Set(att.weights.flat().map((b) => b.bone))];
|
|
4818
4946
|
} else {
|
|
4819
4947
|
const raw = att.vertices!;
|
|
@@ -4828,17 +4956,17 @@ function buildRigMesh(
|
|
|
4828
4956
|
'or say "boneIndexing": "raw" on this attachment to keep the index form deliberately.',
|
|
4829
4957
|
);
|
|
4830
4958
|
}
|
|
4831
|
-
vertices = raw.map(f32);
|
|
4832
4959
|
if (weighted) {
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4960
|
+
// Decoded to names once, here, against the model's bone order — which is
|
|
4961
|
+
// the order the emitter writes the indexes back in, so the emitted run is
|
|
4962
|
+
// the run the spec stated. A run the model cannot hold is refused by name
|
|
4963
|
+
// rather than left to `A20` at the gate (issue #917).
|
|
4964
|
+
checkRawRunShape(raw, uvCount / 2, `there are ${uvCount / 2} uv pairs`, where);
|
|
4965
|
+
const bindings = bindRawRun(raw.map(f32), ctx.bones, where);
|
|
4966
|
+
vertices = { weighted: true, bindings };
|
|
4967
|
+
boundBones = [...new Set(bindings.flat().map((b) => b.bone))];
|
|
4968
|
+
} else {
|
|
4969
|
+
vertices = { weighted: false, xy: raw.map(f32) };
|
|
4842
4970
|
}
|
|
4843
4971
|
}
|
|
4844
4972
|
const { hull, edges } = authoredHullAndEdges(att, uvCount / 2, att.triangles, where);
|
|
@@ -4858,8 +4986,8 @@ function buildRigMesh(
|
|
|
4858
4986
|
`${where}: a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
4859
4987
|
);
|
|
4860
4988
|
}
|
|
4861
|
-
const out:
|
|
4862
|
-
|
|
4989
|
+
const out: ModelMeshAttachment = {
|
|
4990
|
+
kind: 'mesh',
|
|
4863
4991
|
...meshTextureKeys(att, placeholder),
|
|
4864
4992
|
uvs: att.uvs.map(f32),
|
|
4865
4993
|
triangles: att.triangles,
|
|
@@ -4869,7 +4997,7 @@ function buildRigMesh(
|
|
|
4869
4997
|
width: f32(width),
|
|
4870
4998
|
height: f32(height),
|
|
4871
4999
|
};
|
|
4872
|
-
if (att.sequence !== undefined) out.sequence =
|
|
5000
|
+
if (att.sequence !== undefined) out.sequence = buildSequence(att.sequence);
|
|
4873
5001
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
4874
5002
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
4875
5003
|
// read this and skip rather than measuring a ring that was never a ring.
|
|
@@ -4922,7 +5050,7 @@ function buildRigLinkedMesh(
|
|
|
4922
5050
|
placeholder: string,
|
|
4923
5051
|
where: string,
|
|
4924
5052
|
ctx: AttachmentContext,
|
|
4925
|
-
):
|
|
5053
|
+
): ModelLinkedMeshAttachment {
|
|
4926
5054
|
// The mesh keys the parser does not reach on this branch. Named one by one,
|
|
4927
5055
|
// because "remove what does not belong" is not an instruction an author can
|
|
4928
5056
|
// act on and the remedy for each of these is the same single sentence.
|
|
@@ -4974,17 +5102,23 @@ function buildRigLinkedMesh(
|
|
|
4974
5102
|
`${where}: a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
4975
5103
|
);
|
|
4976
5104
|
}
|
|
4977
|
-
|
|
5105
|
+
// The link in full — skin, slot and `timelines` as the parser resolves them.
|
|
5106
|
+
// Writing each only where it differs from the parser's own default (a
|
|
5107
|
+
// `timelines: true` or a `skin` of "default" would be a byte the editor's own
|
|
5108
|
+
// export does not carry) is the emitter's (`emitLinkedMesh`).
|
|
5109
|
+
const out: ModelLinkedMeshAttachment = {
|
|
5110
|
+
kind: 'linkedmesh',
|
|
5111
|
+
source,
|
|
5112
|
+
skin: att.skin ?? 'default',
|
|
5113
|
+
slot: att.slot ?? ctx.slotName,
|
|
5114
|
+
timelines: att.timelines !== false,
|
|
5115
|
+
width: f32(width),
|
|
5116
|
+
height: f32(height),
|
|
5117
|
+
};
|
|
4978
5118
|
const path = attachmentPath(att, placeholder);
|
|
4979
5119
|
if (path !== undefined) out.path = path;
|
|
4980
|
-
// Only where they differ from the parser's own defaults. Writing `timelines:
|
|
4981
|
-
// true`, or a `skin` of "default", would be a byte the editor's own export
|
|
4982
|
-
// does not carry.
|
|
4983
|
-
if (att.slot !== undefined && att.slot !== ctx.slotName) out.slot = att.slot;
|
|
4984
|
-
if (att.skin !== undefined && att.skin !== 'default') out.skin = att.skin;
|
|
4985
|
-
if (att.timelines === false) out.timelines = false;
|
|
4986
5120
|
if (att.color !== undefined) out.color = att.color;
|
|
4987
|
-
if (att.sequence !== undefined) out.sequence =
|
|
5121
|
+
if (att.sequence !== undefined) out.sequence = buildSequence(att.sequence);
|
|
4988
5122
|
// 🚫 NOT registered in `ctx.meshes`, and that is a decision rather than an
|
|
4989
5123
|
// omission. `meshKinds` is keyed by SLOT and the commonest linked mesh shares
|
|
4990
5124
|
// its source's slot from another skin, so an entry here would overwrite the
|
|
@@ -5011,7 +5145,7 @@ function buildRigLinkedMesh(
|
|
|
5011
5145
|
*/
|
|
5012
5146
|
function resolveLinkedMeshes(
|
|
5013
5147
|
links: readonly PendingLink[],
|
|
5014
|
-
tables:
|
|
5148
|
+
tables: ReadonlyMap<string, SkinTable>,
|
|
5015
5149
|
): void {
|
|
5016
5150
|
const skinNames = [...tables.keys()];
|
|
5017
5151
|
for (const link of links) {
|
|
@@ -5052,7 +5186,7 @@ function resolveLinkedMeshes(
|
|
|
5052
5186
|
"Left to the round trip this is the runtime's `Source mesh not found`.",
|
|
5053
5187
|
);
|
|
5054
5188
|
}
|
|
5055
|
-
const type =
|
|
5189
|
+
const type = found.kind;
|
|
5056
5190
|
if (type === 'linkedmesh') {
|
|
5057
5191
|
throw new CompileError(
|
|
5058
5192
|
`${link.where}: "source" is ${JSON.stringify(link.source)}, which is itself a linked mesh, and a chain of ` +
|
|
@@ -5087,25 +5221,24 @@ function buildGeneratedMesh(
|
|
|
5087
5221
|
placeholder: string,
|
|
5088
5222
|
where: string,
|
|
5089
5223
|
ctx: AttachmentContext,
|
|
5090
|
-
):
|
|
5224
|
+
): ModelMeshAttachment {
|
|
5091
5225
|
if (generator.kind === 'contour') return buildContourAttachment(att, generator, placeholder, where, ctx);
|
|
5092
5226
|
if (generator.kind === 'grid') return buildGridAttachment(att, generator, placeholder, where, ctx);
|
|
5093
5227
|
if (generator.kind === 'segments') return buildSegmentsAttachment(att, generator, placeholder, where, ctx);
|
|
5094
5228
|
const controls = generator.kind === 'ring' ? generator.controls : generator.chain;
|
|
5095
|
-
// Resolving the bone
|
|
5096
|
-
// refusals, because the ring's control angles need the transform and the
|
|
5097
|
-
// needs the
|
|
5229
|
+
// Resolving the bone and its setup transform is one step with two named
|
|
5230
|
+
// refusals, because the ring's control angles need the transform and the bind
|
|
5231
|
+
// needs the bone — and the two must refuse a bone the rig lacks in the same
|
|
5098
5232
|
// words whichever of them asks for it first.
|
|
5099
|
-
const resolve = (name: string):
|
|
5100
|
-
|
|
5101
|
-
if (index < 0) throw new CompileError(`${where}: mesh bone "${name}" is not in the rig's bone list`);
|
|
5233
|
+
const resolve = (name: string): BoneTransform => {
|
|
5234
|
+
if (!ctx.bones.some((b) => b.name === name)) throw new CompileError(`${where}: mesh bone "${name}" is not in the rig's bone list`);
|
|
5102
5235
|
const transform = ctx.transforms.get(name);
|
|
5103
5236
|
if (!transform) throw new CompileError(`${where}: no setup transform for mesh bone "${name}"`);
|
|
5104
|
-
return
|
|
5237
|
+
return transform;
|
|
5105
5238
|
};
|
|
5106
5239
|
const refFor = (name: string): MeshBoneRef => {
|
|
5107
|
-
const
|
|
5108
|
-
return {
|
|
5240
|
+
const transform = resolve(name);
|
|
5241
|
+
return { name, toBind: (wx, wy) => toBoneLocal(transform, wx, wy) };
|
|
5109
5242
|
};
|
|
5110
5243
|
// The generator works in part-local pixels, y down. Without a manifest there is
|
|
5111
5244
|
// no crop to flip against, so the part window is centred on its own slot bone.
|
|
@@ -5129,7 +5262,7 @@ function buildGeneratedMesh(
|
|
|
5129
5262
|
size: generator.size,
|
|
5130
5263
|
bias: generator.bias,
|
|
5131
5264
|
controlAngles: ringControlAngles(controls, generator.center, (name) => {
|
|
5132
|
-
const
|
|
5265
|
+
const transform = resolve(name);
|
|
5133
5266
|
return toPartLocal(transform.worldX, transform.worldY);
|
|
5134
5267
|
}),
|
|
5135
5268
|
});
|
|
@@ -5137,7 +5270,7 @@ function buildGeneratedMesh(
|
|
|
5137
5270
|
if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
|
|
5138
5271
|
throw err;
|
|
5139
5272
|
}
|
|
5140
|
-
const vertices =
|
|
5273
|
+
const vertices = bindWeightedVertices(
|
|
5141
5274
|
geometry,
|
|
5142
5275
|
// The world point is an INTERMEDIATE — `toBind` makes it local and the
|
|
5143
5276
|
// result is what is emitted, through `f32` below — so it carries the double.
|
|
@@ -5156,12 +5289,12 @@ function buildGeneratedMesh(
|
|
|
5156
5289
|
triangles: geometry.triangles.length / 3,
|
|
5157
5290
|
bones: [ctx.anchorBone, ...controls],
|
|
5158
5291
|
});
|
|
5159
|
-
const out:
|
|
5160
|
-
|
|
5292
|
+
const out: ModelMeshAttachment = {
|
|
5293
|
+
kind: 'mesh',
|
|
5161
5294
|
...meshTextureKeys(att, placeholder),
|
|
5162
5295
|
uvs: geometry.uvs.map(f32),
|
|
5163
5296
|
triangles: geometry.triangles,
|
|
5164
|
-
vertices: vertices
|
|
5297
|
+
vertices: bindingsOnF32(vertices),
|
|
5165
5298
|
...generatedHullAndEdges(geometry, where),
|
|
5166
5299
|
width: f32(w),
|
|
5167
5300
|
height: f32(h),
|
|
@@ -5237,7 +5370,7 @@ function sampleMeshDepth(
|
|
|
5237
5370
|
triangles: ReadonlyArray<number>,
|
|
5238
5371
|
/**
|
|
5239
5372
|
* A part-local pixel to the BIND space this mesh's vertices are emitted in —
|
|
5240
|
-
* the same composition `
|
|
5373
|
+
* the same composition `bindWeightedVertices` applies, handed in rather than
|
|
5241
5374
|
* rebuilt so the two cannot diverge.
|
|
5242
5375
|
*
|
|
5243
5376
|
* 🚨 The ceiling has to be taken in bind space and in no other. A deform
|
|
@@ -5397,13 +5530,12 @@ function sampleMeshDepth(
|
|
|
5397
5530
|
};
|
|
5398
5531
|
}
|
|
5399
5532
|
|
|
5400
|
-
/** One bone of the rig as `
|
|
5533
|
+
/** One bone of the rig as `bindWeightedVertices` wants it: by name, with its setup inverse. */
|
|
5401
5534
|
function meshBoneRef(name: string, where: string, ctx: AttachmentContext): MeshBoneRef {
|
|
5402
5535
|
const transform = ctx.transforms.get(name);
|
|
5403
5536
|
if (!transform) throw new CompileError(`${where}: bone "${name}" has no setup transform`);
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
return { index, toBind: (wx, wy) => toBoneLocal(transform, wx, wy) };
|
|
5537
|
+
if (!ctx.bones.some((b) => b.name === name)) throw new CompileError(`${where}: bone "${name}" is not in the rig's bone list`);
|
|
5538
|
+
return { name, toBind: (wx, wy) => toBoneLocal(transform, wx, wy) };
|
|
5407
5539
|
}
|
|
5408
5540
|
|
|
5409
5541
|
/**
|
|
@@ -5608,7 +5740,7 @@ function buildGridAttachment(
|
|
|
5608
5740
|
placeholder: string,
|
|
5609
5741
|
where: string,
|
|
5610
5742
|
ctx: AttachmentContext,
|
|
5611
|
-
):
|
|
5743
|
+
): ModelMeshAttachment {
|
|
5612
5744
|
if (att.image === undefined) {
|
|
5613
5745
|
throw new CompileError(
|
|
5614
5746
|
`${where}: a "grid" generator lays its lattice over the part's own window, so the attachment needs an ` +
|
|
@@ -5669,8 +5801,9 @@ function buildGridAttachment(
|
|
|
5669
5801
|
}
|
|
5670
5802
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
5671
5803
|
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
5672
|
-
|
|
5673
|
-
|
|
5804
|
+
if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
|
|
5805
|
+
throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`);
|
|
5806
|
+
}
|
|
5674
5807
|
// Sampled BEFORE the encode, because `bind` rewrites the weights the encode
|
|
5675
5808
|
// then writes out.
|
|
5676
5809
|
const depth =
|
|
@@ -5697,11 +5830,11 @@ function buildGridAttachment(
|
|
|
5697
5830
|
? undefined
|
|
5698
5831
|
: softRegionWeights(generator.soft, geometry.points, sheetGridOf(img, plate), where, ctx);
|
|
5699
5832
|
if (bound) geometry = { ...geometry, weights: bound.weights };
|
|
5700
|
-
const vertices =
|
|
5833
|
+
const vertices = bindWeightedVertices(
|
|
5701
5834
|
geometry,
|
|
5702
5835
|
(px, py) => [anchor.worldX + px * toArt - w / 2, anchor.worldY + h / 2 - py * toArt],
|
|
5703
5836
|
{
|
|
5704
|
-
anchor: {
|
|
5837
|
+
anchor: { name: ctx.anchorBone, toBind: (wx, wy) => toBoneLocal(anchor, wx, wy) },
|
|
5705
5838
|
controls: bound ? [meshBoneRef(bound.bone, where, ctx)] : [],
|
|
5706
5839
|
},
|
|
5707
5840
|
);
|
|
@@ -5721,12 +5854,12 @@ function buildGridAttachment(
|
|
|
5721
5854
|
? undefined
|
|
5722
5855
|
: { mask: bound.mask, digest: bound.digest, bone: bound.bone, carried: bound.carried, ramped: bound.ramped },
|
|
5723
5856
|
});
|
|
5724
|
-
const out:
|
|
5725
|
-
|
|
5857
|
+
const out: ModelMeshAttachment = {
|
|
5858
|
+
kind: 'mesh',
|
|
5726
5859
|
...meshTextureKeys(att, placeholder),
|
|
5727
5860
|
uvs: geometry.uvs.map(f32),
|
|
5728
5861
|
triangles: geometry.triangles,
|
|
5729
|
-
vertices: vertices
|
|
5862
|
+
vertices: bindingsOnF32(vertices),
|
|
5730
5863
|
...generatedHullAndEdges(geometry, where),
|
|
5731
5864
|
width: f32(w),
|
|
5732
5865
|
height: f32(h),
|
|
@@ -5778,7 +5911,7 @@ function buildSegmentsAttachment(
|
|
|
5778
5911
|
placeholder: string,
|
|
5779
5912
|
where: string,
|
|
5780
5913
|
ctx: AttachmentContext,
|
|
5781
|
-
):
|
|
5914
|
+
): ModelMeshAttachment {
|
|
5782
5915
|
if (att.image === undefined) {
|
|
5783
5916
|
throw new CompileError(
|
|
5784
5917
|
`${where}: a "segments" generator lays its lattice over the part's own alpha, so the attachment needs an ` +
|
|
@@ -6005,7 +6138,7 @@ function buildSegmentsAttachment(
|
|
|
6005
6138
|
weights,
|
|
6006
6139
|
hullVertices: lattice.hullVertices,
|
|
6007
6140
|
};
|
|
6008
|
-
const vertices =
|
|
6141
|
+
const vertices = bindWeightedVertices(geometry, place, {
|
|
6009
6142
|
anchor: meshBoneRef(ctx.anchorBone, where, ctx),
|
|
6010
6143
|
controls: refs,
|
|
6011
6144
|
});
|
|
@@ -6052,11 +6185,11 @@ function buildSegmentsAttachment(
|
|
|
6052
6185
|
},
|
|
6053
6186
|
});
|
|
6054
6187
|
return {
|
|
6055
|
-
|
|
6188
|
+
kind: 'mesh',
|
|
6056
6189
|
...meshTextureKeys(att, placeholder),
|
|
6057
6190
|
uvs: geometry.uvs.map(f32),
|
|
6058
6191
|
triangles: geometry.triangles,
|
|
6059
|
-
vertices: vertices
|
|
6192
|
+
vertices: bindingsOnF32(vertices),
|
|
6060
6193
|
...generatedHullAndEdges(geometry, where),
|
|
6061
6194
|
width: f32(w),
|
|
6062
6195
|
height: f32(h),
|
|
@@ -6094,7 +6227,7 @@ function buildContourAttachment(
|
|
|
6094
6227
|
placeholder: string,
|
|
6095
6228
|
where: string,
|
|
6096
6229
|
ctx: AttachmentContext,
|
|
6097
|
-
):
|
|
6230
|
+
): ModelMeshAttachment {
|
|
6098
6231
|
if (att.image === undefined) {
|
|
6099
6232
|
throw new CompileError(
|
|
6100
6233
|
`${where}: a "contour" generator traces the part's own alpha, so the attachment needs an "image" — ` +
|
|
@@ -6167,12 +6300,13 @@ function buildContourAttachment(
|
|
|
6167
6300
|
}
|
|
6168
6301
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
6169
6302
|
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6303
|
+
if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
|
|
6304
|
+
throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`);
|
|
6305
|
+
}
|
|
6306
|
+
const vertices = bindWeightedVertices(
|
|
6173
6307
|
geometry,
|
|
6174
6308
|
(px, py) => [anchor.worldX + px * toArt - w / 2, anchor.worldY + h / 2 - py * toArt],
|
|
6175
|
-
{ anchor: {
|
|
6309
|
+
{ anchor: { name: ctx.anchorBone, toBind: (wx, wy) => toBoneLocal(anchor, wx, wy) }, controls: [] },
|
|
6176
6310
|
);
|
|
6177
6311
|
ctx.meshBones.add(ctx.anchorBone);
|
|
6178
6312
|
// The vertices are on the TRACED grid — the plate's — and the depth map is
|
|
@@ -6217,12 +6351,12 @@ function buildContourAttachment(
|
|
|
6217
6351
|
// this comment used to be the rule's only statement, beside four emit sites
|
|
6218
6352
|
// that disagreed with it — and spread right after `type` by
|
|
6219
6353
|
// `meshTextureKeys` since #791.
|
|
6220
|
-
const out:
|
|
6221
|
-
|
|
6354
|
+
const out: ModelMeshAttachment = {
|
|
6355
|
+
kind: 'mesh',
|
|
6222
6356
|
...meshTextureKeys(att, placeholder),
|
|
6223
6357
|
uvs: geometry.uvs.map(f32),
|
|
6224
6358
|
triangles: geometry.triangles,
|
|
6225
|
-
vertices: vertices
|
|
6359
|
+
vertices: bindingsOnF32(vertices),
|
|
6226
6360
|
...generatedHullAndEdges(geometry, where),
|
|
6227
6361
|
width: f32(w),
|
|
6228
6362
|
height: f32(h),
|
|
@@ -6276,7 +6410,7 @@ interface ConstraintContext {
|
|
|
6276
6410
|
* type matches no case is dropped with no error and no `default:` branch, so an
|
|
6277
6411
|
* unimplemented type is refused here by name rather than emitted and lost.
|
|
6278
6412
|
*/
|
|
6279
|
-
function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext):
|
|
6413
|
+
function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): ModelConstraint {
|
|
6280
6414
|
const where = `rig constraint "${spec.name}"`;
|
|
6281
6415
|
const boneNames = ctx.boneNames;
|
|
6282
6416
|
const needBone = (name: unknown, field: string): string => {
|
|
@@ -6309,7 +6443,11 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6309
6443
|
}
|
|
6310
6444
|
return value;
|
|
6311
6445
|
};
|
|
6312
|
-
|
|
6446
|
+
// The constraint's fields under their spec names; `made` puts the kind the
|
|
6447
|
+
// branch has established and the name on them. `name, type` first and each
|
|
6448
|
+
// kind's field order are the emitter's (`emitConstraint`).
|
|
6449
|
+
const out: Record<string, unknown> = {};
|
|
6450
|
+
const made = (kind: ModelConstraintKind): ModelConstraint => ({ kind, name: spec.name, declaredIn: 'rig', ...out });
|
|
6313
6451
|
const copy = (fields: readonly string[]) => {
|
|
6314
6452
|
for (const field of fields) {
|
|
6315
6453
|
const v = spec[field];
|
|
@@ -6333,7 +6471,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6333
6471
|
// see `RIG_SCALE_Y_MODES`.
|
|
6334
6472
|
if (spec.scaleY !== undefined) out.scaleY = needEnum(spec.scaleY, 'scaleY', RIG_SCALE_Y_MODES);
|
|
6335
6473
|
copy(['mix', 'softness', 'bendPositive', 'compress', 'stretch', 'skin']);
|
|
6336
|
-
return
|
|
6474
|
+
return made('ik');
|
|
6337
6475
|
}
|
|
6338
6476
|
if (spec.type === 'transform') {
|
|
6339
6477
|
out.bones = boneList();
|
|
@@ -6372,7 +6510,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6372
6510
|
'mixShearY',
|
|
6373
6511
|
'skin',
|
|
6374
6512
|
]);
|
|
6375
|
-
return
|
|
6513
|
+
return made('transform');
|
|
6376
6514
|
}
|
|
6377
6515
|
if (spec.type === 'path') {
|
|
6378
6516
|
out.bones = boneList();
|
|
@@ -6406,7 +6544,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6406
6544
|
if (spec[field] !== undefined) out[field] = f32(needNumber(spec[field], field));
|
|
6407
6545
|
}
|
|
6408
6546
|
copy(['skin']);
|
|
6409
|
-
return
|
|
6547
|
+
return made('path');
|
|
6410
6548
|
}
|
|
6411
6549
|
if (spec.type === 'slider') {
|
|
6412
6550
|
// ⭐ The one field in a rig spec that points at the MOTION spec. It is
|
|
@@ -6798,7 +6936,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6798
6936
|
}
|
|
6799
6937
|
}
|
|
6800
6938
|
copy(['skin']);
|
|
6801
|
-
return
|
|
6939
|
+
return made('slider');
|
|
6802
6940
|
}
|
|
6803
6941
|
if (spec.type === 'physics') {
|
|
6804
6942
|
out.bone = needBone(spec.bone, 'bone');
|
|
@@ -6829,7 +6967,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6829
6967
|
'mixGlobal',
|
|
6830
6968
|
'skin',
|
|
6831
6969
|
]);
|
|
6832
|
-
return
|
|
6970
|
+
return made('physics');
|
|
6833
6971
|
}
|
|
6834
6972
|
// Every type 4.3 has is implemented, so this is now only reachable from a typo
|
|
6835
6973
|
// — and a typo is exactly what the parser drops in silence (no `default:`
|
|
@@ -6852,7 +6990,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6852
6990
|
*/
|
|
6853
6991
|
function buildRigInfo(
|
|
6854
6992
|
rig: RigSpec,
|
|
6855
|
-
bones:
|
|
6993
|
+
bones: ModelBone[],
|
|
6856
6994
|
meshes: CompileResult['meshes'],
|
|
6857
6995
|
manifest: FaceManifest | null,
|
|
6858
6996
|
images: CompiledImage[],
|
|
@@ -6965,26 +7103,23 @@ function checkAxisSelfConsistency(manifest: FaceManifest): void {
|
|
|
6965
7103
|
* without it every slot hanging off a rotated axis bone would render tilted by
|
|
6966
7104
|
* the cut's axis angle.
|
|
6967
7105
|
*
|
|
6968
|
-
* On an unrotated bone sitting at its window centre both terms are zero and the
|
|
6969
|
-
*
|
|
6970
|
-
* bytes it always did.
|
|
7106
|
+
* On an unrotated bone sitting at its window centre both terms are zero, and the
|
|
7107
|
+
* emitter leaves a zero out, which is why a formation with no axis emits the
|
|
7108
|
+
* same bytes it always did.
|
|
6971
7109
|
*/
|
|
6972
7110
|
function placeRegion(
|
|
6973
7111
|
part: FaceManifestPart,
|
|
6974
7112
|
manifest: FaceManifest,
|
|
6975
7113
|
bone: BoneTransform,
|
|
6976
7114
|
img: CompiledImage,
|
|
6977
|
-
):
|
|
7115
|
+
): ModelRegionAttachment {
|
|
6978
7116
|
const win = partWindow(part, manifest);
|
|
6979
7117
|
// width/height are NOT optional: omitting them loads as NaN with no error.
|
|
6980
7118
|
// The compiler fills them from the PNG.
|
|
6981
|
-
const att: SpineRegionAttachment = { width: img.width, height: img.height };
|
|
6982
7119
|
const [ax, ay] = toBoneLocal(bone, win.x + win.w / 2, cropToSpineY(win.y + win.h / 2, manifest.crop.h)).map(f32);
|
|
6983
|
-
if (ax !== 0) att.x = ax;
|
|
6984
|
-
if (ay !== 0) att.y = ay;
|
|
6985
7120
|
const rotation = f32(normaliseDegrees(-bone.worldRotation));
|
|
6986
|
-
|
|
6987
|
-
return
|
|
7121
|
+
// All three held, 0 included: leaving a 0 out is the emitter's (`emitRegion`).
|
|
7122
|
+
return { kind: 'region', width: img.width, height: img.height, x: ax, y: ay, rotation, atlas: imageAtlasRect(img) };
|
|
6988
7123
|
}
|
|
6989
7124
|
|
|
6990
7125
|
/**
|
|
@@ -6998,10 +7133,10 @@ function placeRegion(
|
|
|
6998
7133
|
function buildMesh(
|
|
6999
7134
|
part: FaceManifestPart,
|
|
7000
7135
|
manifest: FaceManifest,
|
|
7001
|
-
bones:
|
|
7136
|
+
bones: ModelBone[],
|
|
7002
7137
|
transforms: Map<string, BoneTransform>,
|
|
7003
7138
|
anchorName: string,
|
|
7004
|
-
): { attachment:
|
|
7139
|
+
): { attachment: ModelMeshAttachment; kind: 'ring' | 'ribbon' } {
|
|
7005
7140
|
const spec = part.mesh!;
|
|
7006
7141
|
const kind = spec.kind ?? 'ring';
|
|
7007
7142
|
const win = partWindow(part, manifest);
|
|
@@ -7009,11 +7144,10 @@ function buildMesh(
|
|
|
7009
7144
|
const controls = meshControlBones(part);
|
|
7010
7145
|
|
|
7011
7146
|
const refFor = (name: string): MeshBoneRef => {
|
|
7012
|
-
|
|
7013
|
-
if (index < 0) throw new CompileError(`internal: mesh bone "${name}" is not in the bone list`);
|
|
7147
|
+
if (!bones.some((b) => b.name === name)) throw new CompileError(`internal: mesh bone "${name}" is not in the bone list`);
|
|
7014
7148
|
const m = transforms.get(name);
|
|
7015
7149
|
if (!m) throw new CompileError(`internal: no setup transform for mesh bone "${name}"`);
|
|
7016
|
-
return {
|
|
7150
|
+
return { name, toBind: (wx, wy) => toBoneLocal(m, wx, wy) };
|
|
7017
7151
|
};
|
|
7018
7152
|
|
|
7019
7153
|
let geometry;
|
|
@@ -7053,7 +7187,7 @@ function buildMesh(
|
|
|
7053
7187
|
throw err;
|
|
7054
7188
|
}
|
|
7055
7189
|
|
|
7056
|
-
const vertices =
|
|
7190
|
+
const vertices = bindWeightedVertices(
|
|
7057
7191
|
geometry,
|
|
7058
7192
|
(px, py) => [win.x + px, cropToSpineY(win.y + py, cropH)],
|
|
7059
7193
|
{ anchor: refFor(anchorName), controls: controls.map(refFor) },
|
|
@@ -7062,10 +7196,10 @@ function buildMesh(
|
|
|
7062
7196
|
return {
|
|
7063
7197
|
kind: geometry.kind,
|
|
7064
7198
|
attachment: {
|
|
7065
|
-
|
|
7199
|
+
kind: 'mesh',
|
|
7066
7200
|
uvs: geometry.uvs.map(f32),
|
|
7067
7201
|
triangles: geometry.triangles,
|
|
7068
|
-
vertices: vertices
|
|
7202
|
+
vertices: bindingsOnF32(vertices),
|
|
7069
7203
|
...generatedHullAndEdges(geometry, `slot "${part.slot}" mesh`),
|
|
7070
7204
|
width: win.w,
|
|
7071
7205
|
height: win.h,
|
|
@@ -7129,13 +7263,13 @@ function compileValueTrack(
|
|
|
7129
7263
|
shift: number,
|
|
7130
7264
|
shapes: Record<string, ValueTrackShape>,
|
|
7131
7265
|
kind: string,
|
|
7132
|
-
):
|
|
7266
|
+
): ModelKey[] {
|
|
7133
7267
|
const shape = shapes[track.property];
|
|
7134
7268
|
if (!shape) throw new CompileError(`animation "${animName}": ${kind} "${target}" has no property "${track.property}"`);
|
|
7135
7269
|
const where = `animation "${animName}" ${kind} "${target}" ${track.property}`;
|
|
7136
7270
|
if (!track.keys.length) throw new CompileError(`${where}: no keys`);
|
|
7137
7271
|
|
|
7138
|
-
const out:
|
|
7272
|
+
const out: ModelKey[] = [];
|
|
7139
7273
|
for (let i = 0; i < track.keys.length; i++) {
|
|
7140
7274
|
const key = track.keys[i];
|
|
7141
7275
|
const next = track.keys[i + 1];
|
|
@@ -7175,7 +7309,7 @@ function compileValueTrack(
|
|
|
7175
7309
|
if (!Array.isArray(key.v) || key.v.length !== shape.fields.length) {
|
|
7176
7310
|
throw new CompileError(`${where}: key value must be an array of ${shape.fields.length} number(s)`);
|
|
7177
7311
|
}
|
|
7178
|
-
const entry:
|
|
7312
|
+
const entry: ModelKey = { time };
|
|
7179
7313
|
shape.fields.forEach((field, c) => {
|
|
7180
7314
|
const v = key.v as number[];
|
|
7181
7315
|
if (!Number.isFinite(v[c])) throw new CompileError(`${where}: non-finite value ${String(v[c])}`);
|
|
@@ -7240,7 +7374,8 @@ function compileValueTrack(
|
|
|
7240
7374
|
* than loading wrong. The author states a set of moves; the array order in
|
|
7241
7375
|
* the file is the parser's requirement and not a decision, so rigc sorts
|
|
7242
7376
|
* rather than making every caller remember. Deterministic: the key is the
|
|
7243
|
-
* emitted slot index.
|
|
7377
|
+
* emitted slot index. The model holds the moves as stated and the Spine
|
|
7378
|
+
* emitter sorts them (`emitDrawOrderKey`, issue #921).
|
|
7244
7379
|
* 2. **One slot per key.** Two entries for the same slot means two writes at
|
|
7245
7380
|
* one cursor position; the second silently wins and the first slot's place
|
|
7246
7381
|
* is left to the unchanged-fill.
|
|
@@ -7260,13 +7395,13 @@ function compileDrawOrder(
|
|
|
7260
7395
|
keys: MotionDrawOrderKey[],
|
|
7261
7396
|
animName: string,
|
|
7262
7397
|
duration: number,
|
|
7263
|
-
slots:
|
|
7264
|
-
):
|
|
7398
|
+
slots: readonly ModelSlot[],
|
|
7399
|
+
): ModelKey[] {
|
|
7265
7400
|
const where = `animation "${animName}" drawOrder`;
|
|
7266
7401
|
if (!keys.length) throw new CompileError(`${where}: no keys`);
|
|
7267
7402
|
const indexOf = new Map(slots.map((s, i) => [s.name, i]));
|
|
7268
7403
|
|
|
7269
|
-
const out:
|
|
7404
|
+
const out: ModelKey[] = [];
|
|
7270
7405
|
for (let i = 0; i < keys.length; i++) {
|
|
7271
7406
|
const key = keys[i];
|
|
7272
7407
|
const time = keyTime(key.t);
|
|
@@ -7282,7 +7417,7 @@ function compileDrawOrder(
|
|
|
7282
7417
|
continue;
|
|
7283
7418
|
}
|
|
7284
7419
|
const seen = new Set<string>();
|
|
7285
|
-
const entries: Array<{ slot: string; offset: number
|
|
7420
|
+
const entries: Array<{ slot: string; offset: number }> = [];
|
|
7286
7421
|
for (const off of key.offsets) {
|
|
7287
7422
|
const index = indexOf.get(off.slot);
|
|
7288
7423
|
if (index === undefined) {
|
|
@@ -7302,10 +7437,11 @@ function compileDrawOrder(
|
|
|
7302
7437
|
);
|
|
7303
7438
|
}
|
|
7304
7439
|
seen.add(off.slot);
|
|
7305
|
-
entries.push({ slot: off.slot, offset: off.offset
|
|
7440
|
+
entries.push({ slot: off.slot, offset: off.offset });
|
|
7306
7441
|
}
|
|
7307
|
-
|
|
7308
|
-
|
|
7442
|
+
// In the order the spec states the moves: the setup-order sort the parser
|
|
7443
|
+
// needs is the Spine emitter's (`emitDrawOrderKey`, issue #921).
|
|
7444
|
+
out.push({ time, offsets: entries });
|
|
7309
7445
|
}
|
|
7310
7446
|
return out;
|
|
7311
7447
|
}
|
|
@@ -7340,11 +7476,11 @@ function compileEvents(
|
|
|
7340
7476
|
animName: string,
|
|
7341
7477
|
duration: number,
|
|
7342
7478
|
events: Record<string, RigEvent>,
|
|
7343
|
-
):
|
|
7479
|
+
): ModelKey[] {
|
|
7344
7480
|
const where = `animation "${animName}" events`;
|
|
7345
7481
|
if (!keys.length) throw new CompileError(`${where}: no keys`);
|
|
7346
7482
|
|
|
7347
|
-
const out:
|
|
7483
|
+
const out: ModelKey[] = [];
|
|
7348
7484
|
for (let i = 0; i < keys.length; i++) {
|
|
7349
7485
|
const key = keys[i];
|
|
7350
7486
|
if (typeof key.name !== 'string' || key.name.length === 0) {
|
|
@@ -7367,7 +7503,7 @@ function compileEvents(
|
|
|
7367
7503
|
}
|
|
7368
7504
|
checkKeyTime(where, time, key.t, duration);
|
|
7369
7505
|
|
|
7370
|
-
const entry:
|
|
7506
|
+
const entry: ModelKey = { time, name: key.name };
|
|
7371
7507
|
if (key.int !== undefined) {
|
|
7372
7508
|
if (!Number.isInteger(key.int)) {
|
|
7373
7509
|
throw new CompileError(`${where} at t=${key.t}: int ${String(key.int)} is not an integer`);
|
|
@@ -7454,9 +7590,9 @@ function compileConstraintTrack(
|
|
|
7454
7590
|
motion: MotionSpec,
|
|
7455
7591
|
animName: string,
|
|
7456
7592
|
duration: number,
|
|
7457
|
-
/**
|
|
7593
|
+
/** The ik constraint's three booleans in effect, by field. Empty for `transform`. */
|
|
7458
7594
|
rigFlags: Record<string, boolean>,
|
|
7459
|
-
):
|
|
7595
|
+
): ModelKey[] {
|
|
7460
7596
|
const shape = CONSTRAINT_TIMELINES[group];
|
|
7461
7597
|
const article = group === 'ik' ? 'an' : 'a';
|
|
7462
7598
|
const where = `animation "${animName}" ${group} constraint "${track.constraint}"`;
|
|
@@ -7505,7 +7641,7 @@ function compileConstraintTrack(
|
|
|
7505
7641
|
return inherited === undefined ? channel.dflt : (inherited as number);
|
|
7506
7642
|
};
|
|
7507
7643
|
|
|
7508
|
-
const out:
|
|
7644
|
+
const out: ModelKey[] = [];
|
|
7509
7645
|
for (let i = 0; i < keys.length; i++) {
|
|
7510
7646
|
const key = keys[i];
|
|
7511
7647
|
const next = keys[i + 1];
|
|
@@ -7515,7 +7651,7 @@ function compileConstraintTrack(
|
|
|
7515
7651
|
}
|
|
7516
7652
|
checkKeyTime(where, time, key.t, duration);
|
|
7517
7653
|
|
|
7518
|
-
const entry:
|
|
7654
|
+
const entry: ModelKey = { time };
|
|
7519
7655
|
for (const channel of shape.channels) {
|
|
7520
7656
|
const v = read(key, channel.field);
|
|
7521
7657
|
if (v === undefined) continue;
|
|
@@ -7535,10 +7671,10 @@ function compileConstraintTrack(
|
|
|
7535
7671
|
for (const flag of shape.flags) {
|
|
7536
7672
|
const v = read(key, flag.field);
|
|
7537
7673
|
if (v === undefined) {
|
|
7538
|
-
// The
|
|
7539
|
-
//
|
|
7540
|
-
//
|
|
7541
|
-
//
|
|
7674
|
+
// The constraint's flag in effect, held on this key because the parser
|
|
7675
|
+
// reads the flag per key and does not inherit it. The model key holds
|
|
7676
|
+
// every flag in effect; the Spine emitter writes one only where it
|
|
7677
|
+
// differs from the per-key default (`emitAnimations`, issue #921).
|
|
7542
7678
|
const carried = rigFlags[flag.field];
|
|
7543
7679
|
if (carried !== undefined) entry[flag.field] = carried;
|
|
7544
7680
|
continue;
|
|
@@ -7600,7 +7736,7 @@ function defaultOf(shape: ConstraintTimelineShape, field: string): number | bool
|
|
|
7600
7736
|
* Same shape, two meanings, and picking the wrong one writes a run that silently
|
|
7601
7737
|
* lands on the wrong vertices.
|
|
7602
7738
|
*/
|
|
7603
|
-
interface DeformGeometry {
|
|
7739
|
+
export interface DeformGeometry {
|
|
7604
7740
|
weighted: boolean;
|
|
7605
7741
|
/** How long the array the key edits is. */
|
|
7606
7742
|
deformLength: number;
|
|
@@ -7647,7 +7783,7 @@ interface DeformGeometry {
|
|
|
7647
7783
|
/**
|
|
7648
7784
|
* Per-vertex `z`, when this attachment's generator named a depth map.
|
|
7649
7785
|
*
|
|
7650
|
-
* Not measured from the
|
|
7786
|
+
* Not measured from the attachment like everything else here — there
|
|
7651
7787
|
* is nowhere in the format to measure it FROM, which is the whole reason the
|
|
7652
7788
|
* sampler keeps it beside the skeleton instead of in it. Filled in by the
|
|
7653
7789
|
* caller, which is where the skin, slot and attachment names live.
|
|
@@ -7656,25 +7792,35 @@ interface DeformGeometry {
|
|
|
7656
7792
|
}
|
|
7657
7793
|
|
|
7658
7794
|
/**
|
|
7659
|
-
* Measure one
|
|
7795
|
+
* Measure one attachment's deform array, off its model record (issue #917): the
|
|
7796
|
+
* bindings name their bones, so each influence's setup transform is looked up by
|
|
7797
|
+
* name and no index into any bone array is read.
|
|
7660
7798
|
*
|
|
7661
7799
|
* A region attachment is refused rather than measured: it has no `vertices` at
|
|
7662
7800
|
* all, so `attachment.vertices.length` throws inside the parser — one of the very
|
|
7663
7801
|
* few places this format fails loudly, and it fails in the consumer's process.
|
|
7802
|
+
* A linked mesh is refused the same way, by its type.
|
|
7803
|
+
*
|
|
7804
|
+
* Exported for the selftest alone: `MV06` holds it to the decoder of the emitted
|
|
7805
|
+
* run it replaced, to the bit.
|
|
7664
7806
|
*/
|
|
7665
|
-
function deformGeometryOf(
|
|
7666
|
-
att:
|
|
7807
|
+
export function deformGeometryOf(
|
|
7808
|
+
att: SkinTableEntry,
|
|
7667
7809
|
where: string,
|
|
7668
|
-
/**
|
|
7669
|
-
rigBones: SpineBone[],
|
|
7670
|
-
/** Their setup world transforms, by name. */
|
|
7810
|
+
/** Every bone's setup world transform, by name. */
|
|
7671
7811
|
transforms: Map<string, BoneTransform>,
|
|
7672
7812
|
): DeformGeometry {
|
|
7673
|
-
const type =
|
|
7813
|
+
const type = att.kind;
|
|
7674
7814
|
let worldVerticesLength: number;
|
|
7675
|
-
if (
|
|
7676
|
-
|
|
7677
|
-
|
|
7815
|
+
if (!isModelVertexAttachment(att)) {
|
|
7816
|
+
throw new CompileError(
|
|
7817
|
+
`${where}: a deform timeline keys the vertices of an attachment, and this one is a "${type}" — ` +
|
|
7818
|
+
'it has no vertex array to deform. Deformable types: mesh, boundingbox, clipping, path.',
|
|
7819
|
+
);
|
|
7820
|
+
}
|
|
7821
|
+
if (att.kind === 'mesh') {
|
|
7822
|
+
worldVerticesLength = att.uvs.length;
|
|
7823
|
+
} else {
|
|
7678
7824
|
// ⭐ A `path` reaches this line as of issue #696, and it is the SAME line the
|
|
7679
7825
|
// other two vertex-and-no-triangles types take: the array the parser sizes
|
|
7680
7826
|
// is `vertexCount * 2`, the two encodings below are the mesh's own, and a
|
|
@@ -7703,16 +7849,11 @@ function deformGeometryOf(
|
|
|
7703
7849
|
// file twice. `docs/AUTHORING.md` §4.11 states the `constantSpeed: false`
|
|
7704
7850
|
// reading instead, which is the honest home for it: a fact about the format
|
|
7705
7851
|
// the author is choosing, not a fault rigc can measure.
|
|
7706
|
-
worldVerticesLength =
|
|
7707
|
-
} else {
|
|
7708
|
-
throw new CompileError(
|
|
7709
|
-
`${where}: a deform timeline keys the vertices of an attachment, and this one is a "${type}" — ` +
|
|
7710
|
-
'it has no vertex array to deform. Deformable types: mesh, boundingbox, clipping, path.',
|
|
7711
|
-
);
|
|
7852
|
+
worldVerticesLength = att.vertexCount * 2;
|
|
7712
7853
|
}
|
|
7713
|
-
const vertices =
|
|
7714
|
-
const weighted = vertices.
|
|
7715
|
-
if (!weighted) {
|
|
7854
|
+
const vertices = att.vertices;
|
|
7855
|
+
const weighted = vertices.weighted;
|
|
7856
|
+
if (!vertices.weighted) {
|
|
7716
7857
|
return {
|
|
7717
7858
|
weighted,
|
|
7718
7859
|
deformLength: worldVerticesLength,
|
|
@@ -7720,41 +7861,37 @@ function deformGeometryOf(
|
|
|
7720
7861
|
boneCounts: null,
|
|
7721
7862
|
// An unweighted attachment IS its own space: the array is one `x, y` per
|
|
7722
7863
|
// vertex in the slot bone's space, which is the space the offsets are in.
|
|
7723
|
-
setup: vertices.slice(),
|
|
7864
|
+
setup: vertices.xy.slice(),
|
|
7724
7865
|
influenceBones: null,
|
|
7725
7866
|
setupWhy: null,
|
|
7726
|
-
// Filled in by the caller; `deformGeometryOf` reads the
|
|
7727
|
-
//
|
|
7867
|
+
// Filled in by the caller; `deformGeometryOf` reads the attachment's
|
|
7868
|
+
// record, and the depth is deliberately not in it.
|
|
7728
7869
|
depth: null,
|
|
7729
7870
|
};
|
|
7730
7871
|
}
|
|
7731
|
-
// Walk the
|
|
7732
|
-
//
|
|
7733
|
-
//
|
|
7872
|
+
// Walk the bindings for the per-vertex influence counts. Their shape is
|
|
7873
|
+
// already assured by the attachment builders and by A33/A04; this only counts,
|
|
7874
|
+
// and a vertex with no binding stops rather than producing a plausible number
|
|
7875
|
+
// — at the index its count would hold in the emitted run, as it was worded
|
|
7876
|
+
// when this walked the run.
|
|
7734
7877
|
const boneCounts: number[] = [];
|
|
7735
7878
|
const bindSpace: number[] = [];
|
|
7736
|
-
const bones = new Set<
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
throw new CompileError(`${where}: the attachment's weighted vertex run has a bone count of ${String(n)} at index ${i - 1}`);
|
|
7879
|
+
const bones = new Set<string>();
|
|
7880
|
+
for (let v = 0, at = 0; v < vertices.bindings.length; v++) {
|
|
7881
|
+
const vertex = vertices.bindings[v];
|
|
7882
|
+
const n = vertex.length;
|
|
7883
|
+
if (n < 1) {
|
|
7884
|
+
throw new CompileError(`${where}: the attachment's weighted vertex run has a bone count of ${String(n)} at index ${at}`);
|
|
7743
7885
|
}
|
|
7744
7886
|
if (n === 1) {
|
|
7745
|
-
bones.add(
|
|
7746
|
-
bindSpace.push(
|
|
7747
|
-
}
|
|
7748
|
-
const from = i;
|
|
7749
|
-
i += n * 4;
|
|
7750
|
-
if (i > vertices.length) {
|
|
7751
|
-
throw new CompileError(`${where}: the attachment's weighted vertex run is truncated at vertex ${boneCounts.length}`);
|
|
7887
|
+
bones.add(vertex[0].bone);
|
|
7888
|
+
bindSpace.push(vertex[0].x, vertex[0].y);
|
|
7752
7889
|
}
|
|
7753
|
-
for (let k = from; k < i; k++) influenceRun.push(vertices[k]);
|
|
7754
7890
|
boneCounts.push(n);
|
|
7891
|
+
at += 1 + n * 4;
|
|
7755
7892
|
}
|
|
7756
7893
|
// ⚠️ The influence count is the SUM of the per-vertex counts, not a division
|
|
7757
|
-
// of the JSON array's length. The emitted array is `boneCount` followed by
|
|
7894
|
+
// of the emitted JSON array's length. The emitted array is `boneCount` followed by
|
|
7758
7895
|
// `boneIndex, x, y, weight` per influence — five numbers for a single-bone
|
|
7759
7896
|
// vertex, not three — so `vertices.length / 3` overstated the deform array by
|
|
7760
7897
|
// two thirds on a one-bone-per-vertex mesh (`gallery/flex`'s 77-vertex leaf
|
|
@@ -7792,17 +7929,15 @@ function deformGeometryOf(
|
|
|
7792
7929
|
const setupWorld: number[] = [];
|
|
7793
7930
|
const influenceBones: BoneTransform[] = [];
|
|
7794
7931
|
let openWeights: string | null = null;
|
|
7795
|
-
for (let v = 0
|
|
7932
|
+
for (let v = 0; v < boneCounts.length; v++) {
|
|
7796
7933
|
let wx = 0;
|
|
7797
7934
|
let wy = 0;
|
|
7798
7935
|
let sum = 0;
|
|
7799
|
-
for (
|
|
7800
|
-
const
|
|
7801
|
-
|
|
7802
|
-
const
|
|
7803
|
-
|
|
7804
|
-
const weight = influenceRun[4 * k + 3];
|
|
7805
|
-
const [x, y] = toWorld(m, influenceRun[4 * k + 1], influenceRun[4 * k + 2]);
|
|
7936
|
+
for (const binding of vertices.bindings[v]) {
|
|
7937
|
+
const m = transforms.get(binding.bone);
|
|
7938
|
+
if (!m) throw new CompileError(`${where}: vertex ${v} binds bone "${binding.bone}", which has no setup transform`);
|
|
7939
|
+
const weight = binding.weight;
|
|
7940
|
+
const [x, y] = toWorld(m, binding.x, binding.y);
|
|
7806
7941
|
wx += x * weight;
|
|
7807
7942
|
wy += y * weight;
|
|
7808
7943
|
sum += weight;
|
|
@@ -7897,11 +8032,11 @@ function deformGeometryOf(
|
|
|
7897
8032
|
function compileSequenceTrack(
|
|
7898
8033
|
track: MotionSequenceTrack,
|
|
7899
8034
|
duration: number,
|
|
7900
|
-
attachment:
|
|
8035
|
+
attachment: SkinTableEntry,
|
|
7901
8036
|
where: string,
|
|
7902
|
-
):
|
|
7903
|
-
const
|
|
7904
|
-
|
|
8037
|
+
): ModelKey[] {
|
|
8038
|
+
const series =
|
|
8039
|
+
attachment.kind === 'region' || attachment.kind === 'mesh' || attachment.kind === 'linkedmesh' ? attachment.sequence : undefined;
|
|
7905
8040
|
if (series === undefined) {
|
|
7906
8041
|
throw new CompileError(
|
|
7907
8042
|
`${where}: attachment "${track.attachment}" carries no "sequence" block, so there is no series to step. The ` +
|
|
@@ -7910,8 +8045,8 @@ function compileSequenceTrack(
|
|
|
7910
8045
|
'attachment a "sequence" in the rig spec, or remove the track.',
|
|
7911
8046
|
);
|
|
7912
8047
|
}
|
|
7913
|
-
if (
|
|
7914
|
-
const source =
|
|
8048
|
+
if (attachment.kind === 'linkedmesh' && attachment.timelines) {
|
|
8049
|
+
const source = attachment.source;
|
|
7915
8050
|
throw new CompileError(
|
|
7916
8051
|
`${where}: attachment "${track.attachment}" is a linked mesh that plays its source's timelines ("timelines" ` +
|
|
7917
8052
|
`is not false), so its \`timelineAttachment\` is "${source}" (\`SkeletonJson.js:437-448\`) and ` +
|
|
@@ -7921,7 +8056,7 @@ function compileSequenceTrack(
|
|
|
7921
8056
|
);
|
|
7922
8057
|
}
|
|
7923
8058
|
if (track.keys.length === 0) throw new CompileError(`${where}: no keys`);
|
|
7924
|
-
const out:
|
|
8059
|
+
const out: ModelKey[] = [];
|
|
7925
8060
|
for (let i = 0; i < track.keys.length; i++) {
|
|
7926
8061
|
const key = track.keys[i];
|
|
7927
8062
|
const time = keyTime(key.t);
|
|
@@ -7936,7 +8071,7 @@ function compileSequenceTrack(
|
|
|
7936
8071
|
'frame it does not name. `index` is 0-based.',
|
|
7937
8072
|
);
|
|
7938
8073
|
}
|
|
7939
|
-
const entry:
|
|
8074
|
+
const entry: ModelKey = { time };
|
|
7940
8075
|
if (key.mode !== undefined) entry.mode = key.mode;
|
|
7941
8076
|
if (key.index !== undefined) entry.index = key.index;
|
|
7942
8077
|
if (key.delay !== undefined) entry.delay = f32(key.delay);
|
|
@@ -7952,7 +8087,7 @@ function compileDeformTrack(
|
|
|
7952
8087
|
duration: number,
|
|
7953
8088
|
geometry: DeformGeometry,
|
|
7954
8089
|
generated: CompileResult['deformTransforms'],
|
|
7955
|
-
):
|
|
8090
|
+
): ModelKey[] {
|
|
7956
8091
|
const skin = track.skin ?? 'default';
|
|
7957
8092
|
const where = `animation "${animName}" deform ${skin}/${track.slot}/${track.attachment}`;
|
|
7958
8093
|
const keys = track.keys;
|
|
@@ -7961,7 +8096,7 @@ function compileDeformTrack(
|
|
|
7961
8096
|
// skips the timeline, which is assertion A34's silent case.
|
|
7962
8097
|
if (keys.length === 0) throw new CompileError(`${where}: no keys`);
|
|
7963
8098
|
|
|
7964
|
-
const out:
|
|
8099
|
+
const out: ModelKey[] = [];
|
|
7965
8100
|
for (let i = 0; i < keys.length; i++) {
|
|
7966
8101
|
const key = keys[i];
|
|
7967
8102
|
const time = keyTime(key.t);
|
|
@@ -8120,7 +8255,7 @@ function compileDeformTrack(
|
|
|
8120
8255
|
'The parser copies into a Float32Array, so everything past the end is dropped without a word.',
|
|
8121
8256
|
);
|
|
8122
8257
|
}
|
|
8123
|
-
const entry:
|
|
8258
|
+
const entry: ModelKey = { time };
|
|
8124
8259
|
// `offset` defaults to 0 in the parser and the editor omits it there, so an
|
|
8125
8260
|
// authored 0, an authored `fromVertex: 0` and an absent start all emit the
|
|
8126
8261
|
// same bytes — one meaning, one file.
|
|
@@ -8248,15 +8383,15 @@ function deformStart(
|
|
|
8248
8383
|
* than to a coordinate, and a raw curve is four numbers whose value axis is 0..1.
|
|
8249
8384
|
*/
|
|
8250
8385
|
function deformKeyCurve(
|
|
8251
|
-
entry:
|
|
8386
|
+
entry: ModelKey,
|
|
8252
8387
|
key: MotionDeformTrack['keys'][number],
|
|
8253
8388
|
keys: MotionDeformTrack['keys'],
|
|
8254
8389
|
index: number,
|
|
8255
8390
|
motion: MotionSpec,
|
|
8256
8391
|
where: string,
|
|
8257
8392
|
/** The next key AS EMITTED, or undefined on the last key. */
|
|
8258
|
-
next:
|
|
8259
|
-
):
|
|
8393
|
+
next: ModelKey | undefined,
|
|
8394
|
+
): ModelKey {
|
|
8260
8395
|
if (key.ease !== undefined && key.curve !== undefined) {
|
|
8261
8396
|
throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
|
|
8262
8397
|
}
|
|
@@ -8288,8 +8423,8 @@ function deformKeyCurve(
|
|
|
8288
8423
|
* are three spellings of one geometry, and the comparison expands both keys onto
|
|
8289
8424
|
* the same index range before reading them. The runs are already `f32`'d.
|
|
8290
8425
|
*/
|
|
8291
|
-
function sameDeform(a:
|
|
8292
|
-
const runOf = (k:
|
|
8426
|
+
function sameDeform(a: ModelKey, b: ModelKey): boolean {
|
|
8427
|
+
const runOf = (k: ModelKey): { start: number; run: number[] } => ({
|
|
8293
8428
|
start: typeof k.offset === 'number' ? k.offset : 0,
|
|
8294
8429
|
run: Array.isArray(k.vertices) ? (k.vertices as number[]) : [],
|
|
8295
8430
|
});
|
|
@@ -8468,7 +8603,7 @@ function resolveMemberTrack(
|
|
|
8468
8603
|
track: MotionTrack,
|
|
8469
8604
|
animName: string,
|
|
8470
8605
|
targets: readonly string[],
|
|
8471
|
-
bones: readonly
|
|
8606
|
+
bones: readonly ModelBone[],
|
|
8472
8607
|
derivations: CompileResult['trackDerivations'],
|
|
8473
8608
|
): Map<string, MotionValueTrack> {
|
|
8474
8609
|
const carriesMap = track.keys.some((key) => isMemberValues(key.v));
|
|
@@ -8628,7 +8763,7 @@ function compileTrack(
|
|
|
8628
8763
|
shift: number,
|
|
8629
8764
|
attachments: SlotAttachmentIndex,
|
|
8630
8765
|
darkSlots: ReadonlySet<string>,
|
|
8631
|
-
):
|
|
8766
|
+
): ModelKey[] {
|
|
8632
8767
|
const where = `animation "${animName}" slot "${target}" ${track.property}`;
|
|
8633
8768
|
// Before any key is shaped, and before the empty-track refusal: a property
|
|
8634
8769
|
// the emitter has no branch for is the fault, and a track that names one has
|
|
@@ -8667,7 +8802,7 @@ function compileTrack(
|
|
|
8667
8802
|
}
|
|
8668
8803
|
if (!track.keys.length) throw new CompileError(`${where}: no keys`);
|
|
8669
8804
|
|
|
8670
|
-
const out:
|
|
8805
|
+
const out: ModelKey[] = [];
|
|
8671
8806
|
for (let i = 0; i < track.keys.length; i++) {
|
|
8672
8807
|
const key = track.keys[i];
|
|
8673
8808
|
const next = track.keys[i + 1];
|
|
@@ -8730,7 +8865,7 @@ function compileTrack(
|
|
|
8730
8865
|
'the posed alpha to that range after interpolating, so a value outside it is one no pose ever holds',
|
|
8731
8866
|
);
|
|
8732
8867
|
}
|
|
8733
|
-
const entry:
|
|
8868
|
+
const entry: ModelKey = { time, ...colourOf(key.v) };
|
|
8734
8869
|
if (key.ease !== undefined && key.curve !== undefined) {
|
|
8735
8870
|
throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
|
|
8736
8871
|
}
|