spine-rigc 1.5.2 → 1.6.1
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 +72 -23
- package/docs/INGEST.md +1 -0
- package/docs/MOTION.md +26 -17
- 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 +650 -509
- 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/keys.ts +76 -2
- package/src/mesh.ts +24 -15
- package/src/model.ts +958 -0
- package/src/motion.ts +25 -5
- package/src/render.ts +601 -140
- package/src/render_core.ts +582 -0
- package/src/rig.ts +137 -41
- package/src/transform.ts +26 -4
- package/src/types.ts +50 -4
- package/src/validate.ts +142 -58
package/src/compile.ts
CHANGED
|
@@ -38,10 +38,9 @@ import { parseJsonWithPosition } from './json-position.ts';
|
|
|
38
38
|
// The "did you mean" list on a missing atlas region, from the one implementation
|
|
39
39
|
// of it — the same search serves `refuseUnknownKeys`, and a second copy here with
|
|
40
40
|
// a threshold edited is how such a pair drifts apart.
|
|
41
|
-
import { dottedPath, nearMisses, refuseNumbersTheFileCannotCarry, refuseValuesOfTheWrongType } from './keys.ts';
|
|
41
|
+
import { dottedPath, nearMisses, refuseNumbersTheFileCannotCarry, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
42
42
|
import type { ShapeVisit } from './keys.ts';
|
|
43
|
-
import { MANIFEST_TYPES } from './types.ts';
|
|
44
|
-
import { inEditorKeyOrder, withoutParserDefaults } from './keyorder.ts';
|
|
43
|
+
import { MANIFEST_ENUMS, MANIFEST_TYPES } from './types.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
|
|
@@ -2062,7 +2080,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2062
2080
|
// same rule over the other file whose numbers reach the skeleton.
|
|
2063
2081
|
// Its values of the wrong type first (issue #890): `crop.h: "806"` built
|
|
2064
2082
|
// green, the string coerced by whatever arithmetic read it.
|
|
2065
|
-
if (manifest !== null && manifestPath !== null)
|
|
2083
|
+
if (manifest !== null && manifestPath !== null) {
|
|
2084
|
+
const visits = manifestShapeVisits(manifest);
|
|
2085
|
+
refuseValuesOfTheWrongType(visits, MANIFEST_TYPES, manifestPath);
|
|
2086
|
+
// Then a name outside a stated set (issue #900): `mesh.kind: "foo"` was
|
|
2087
|
+
// read as a ribbon by one reader and a ring by two others.
|
|
2088
|
+
refuseValuesOutsideTheirSet(visits, MANIFEST_TYPES, MANIFEST_ENUMS, manifestPath);
|
|
2089
|
+
}
|
|
2066
2090
|
if (manifest !== null && manifestPath !== null) refuseNumbersTheFileCannotCarry(manifest, manifestPath, dottedPath);
|
|
2067
2091
|
|
|
2068
2092
|
// The rig spec names its own path in every message `parseRigSpec` throws (its
|
|
@@ -2531,7 +2555,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2531
2555
|
const axisSpineDeg = manifest?.axis ? screenToSpineDegrees(manifest.axis.deg) : null;
|
|
2532
2556
|
const partBySlot = new Map(parts.map((part) => [part.slot, part]));
|
|
2533
2557
|
|
|
2534
|
-
const bones:
|
|
2558
|
+
const bones: ModelBone[] = [];
|
|
2535
2559
|
for (const spec of rig.bones) {
|
|
2536
2560
|
bones.push(buildBone(spec, bones, { rig, manifest, cropH, axisSpineDeg, partBySlot }));
|
|
2537
2561
|
}
|
|
@@ -2557,9 +2581,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2557
2581
|
// -- 4. slots + skins ------------------------------------------------------
|
|
2558
2582
|
// Draw order IS the slots array order. No separate field,
|
|
2559
2583
|
// and the rig's array is that order.
|
|
2560
|
-
const slots:
|
|
2561
|
-
|
|
2562
|
-
|
|
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 => {
|
|
2563
2589
|
let table = skinTables.get(skinName);
|
|
2564
2590
|
if (!table) {
|
|
2565
2591
|
table = {};
|
|
@@ -2697,11 +2723,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2697
2723
|
if (setup?.color && rigSlot.color !== undefined) {
|
|
2698
2724
|
throw new CompileError(`slot "${rigSlot.name}" has a setup colour in the rig spec AND in the motion spec`);
|
|
2699
2725
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
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 };
|
|
2702
2729
|
if (setup?.color) slot.color = rgbaHex(setup.color);
|
|
2703
2730
|
else if (rigSlot.color !== undefined) slot.color = rigSlot.color;
|
|
2704
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).
|
|
2705
2733
|
if (rigSlot.blend !== undefined) slot.blend = rigSlot.blend;
|
|
2706
2734
|
slots.push(slot);
|
|
2707
2735
|
// ...and nothing below this line has anything to build. An empty entry in a
|
|
@@ -2711,7 +2739,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2711
2739
|
if (empty) continue;
|
|
2712
2740
|
|
|
2713
2741
|
if (part) {
|
|
2714
|
-
const perSlot: Record<string,
|
|
2742
|
+
const perSlot: Record<string, SkinTableEntry> = {};
|
|
2715
2743
|
const mesh = part.mesh ? buildMesh(part, manifest!, bones, transforms, rigSlot.bone) : null;
|
|
2716
2744
|
for (const name of names) {
|
|
2717
2745
|
const img = images.find((im) => im.region === name);
|
|
@@ -2745,7 +2773,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2745
2773
|
for (const skinName of skinNames) {
|
|
2746
2774
|
const placeholders = skinParts.get(skinName)!.attachments[rigSlot.name];
|
|
2747
2775
|
if (!placeholders) continue;
|
|
2748
|
-
const perSlot: Record<string,
|
|
2776
|
+
const perSlot: Record<string, SkinTableEntry> = {};
|
|
2749
2777
|
for (const [placeholder, att] of Object.entries(placeholders)) {
|
|
2750
2778
|
const where = `skin "${skinName}" slot "${rigSlot.name}" attachment "${placeholder}"`;
|
|
2751
2779
|
const built = buildRigAttachment(att, placeholder, where, {
|
|
@@ -2760,6 +2788,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2760
2788
|
imagesDir,
|
|
2761
2789
|
depths: attachmentDepths,
|
|
2762
2790
|
slotNames: new Set(rig.slots.map((s) => s.name)),
|
|
2791
|
+
atlasRegions: atlasIn === null ? null : atlasIn.byName,
|
|
2763
2792
|
links: pendingLinks,
|
|
2764
2793
|
});
|
|
2765
2794
|
// The name is put on AFTER the builder rather than inside it: six
|
|
@@ -2809,7 +2838,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2809
2838
|
// (structure), then the motion spec's physics table (tuning). A name in both is
|
|
2810
2839
|
// refused: `mix` timelines resolve by name, and two constraints answering to
|
|
2811
2840
|
// one name is a timeline driving something nobody chose.
|
|
2812
|
-
const constraints:
|
|
2841
|
+
const constraints: ModelConstraint[] = [];
|
|
2813
2842
|
const physicsReport: CompileResult['physics'] = [];
|
|
2814
2843
|
// Deform keys that stated a model instead of a run (issue #294). Declared here
|
|
2815
2844
|
// rather than inside the animation loop because `explain` reports it across
|
|
@@ -2845,8 +2874,8 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2845
2874
|
constraintNamesOfKind.set(type, [...(constraintNamesOfKind.get(type) ?? []), name]);
|
|
2846
2875
|
};
|
|
2847
2876
|
/**
|
|
2848
|
-
* ik constraint ->
|
|
2849
|
-
*
|
|
2877
|
+
* ik constraint -> its three booleans IN EFFECT: the value it declares, or
|
|
2878
|
+
* the parser's default where it declares none. Issue #273.
|
|
2850
2879
|
*
|
|
2851
2880
|
* 🚨 `SkeletonJson` reads `bendPositive`, `compress` and `stretch` in TWO
|
|
2852
2881
|
* places with the same defaults: once on the constraint (`:155`) and once on
|
|
@@ -2855,19 +2884,23 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2855
2884
|
* `bendPositive: false` and an `ik` timeline that keys only `mix` produce a
|
|
2856
2885
|
* constraint that bends the other way for the whole animation, with the field
|
|
2857
2886
|
* still in the file and inert: four builds differing only in these flags posed
|
|
2858
|
-
* one pose.
|
|
2859
|
-
*
|
|
2860
|
-
*
|
|
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.
|
|
2861
2893
|
*/
|
|
2862
|
-
const
|
|
2894
|
+
const ikFlagsInEffect = new Map<string, Record<string, boolean>>();
|
|
2863
2895
|
// Which slots can actually show a path, for the path constraint's own check.
|
|
2864
|
-
// Read off the
|
|
2865
|
-
//
|
|
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.)
|
|
2866
2899
|
const pathSlots = new Map<string, string[]>();
|
|
2867
2900
|
for (const [skinName, table] of skinTables) {
|
|
2868
2901
|
for (const [slotName, perSlot] of Object.entries(table)) {
|
|
2869
2902
|
for (const att of Object.values(perSlot)) {
|
|
2870
|
-
if (
|
|
2903
|
+
if (att.kind !== 'path') continue;
|
|
2871
2904
|
pathSlots.set(slotName, [...(pathSlots.get(slotName) ?? []), skinName]);
|
|
2872
2905
|
break;
|
|
2873
2906
|
}
|
|
@@ -2887,12 +2920,15 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2887
2920
|
constraints.push(buildRigConstraint(spec, constraintCtx));
|
|
2888
2921
|
declareConstraint(spec.name, spec.type);
|
|
2889
2922
|
if (spec.type === 'ik') {
|
|
2890
|
-
|
|
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> = {};
|
|
2891
2927
|
for (const flag of CONSTRAINT_TIMELINES.ik.flags) {
|
|
2892
2928
|
const declared = spec[flag.field];
|
|
2893
|
-
|
|
2929
|
+
inEffect[flag.field] = typeof declared === 'boolean' ? declared : flag.dflt;
|
|
2894
2930
|
}
|
|
2895
|
-
|
|
2931
|
+
ikFlagsInEffect.set(spec.name, inEffect);
|
|
2896
2932
|
}
|
|
2897
2933
|
}
|
|
2898
2934
|
withMotionSource(() => {
|
|
@@ -2910,17 +2946,17 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2910
2946
|
if (!boneNames.has(spec.bone)) {
|
|
2911
2947
|
throw new CompileError(`physics constraint "${name}" targets unknown bone "${spec.bone}"`);
|
|
2912
2948
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
};
|
|
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 };
|
|
2918
2954
|
const components: string[] = [];
|
|
2919
2955
|
for (const comp of PHYSICS_COMPONENTS) {
|
|
2920
2956
|
const v = spec[comp];
|
|
2921
|
-
if (v === undefined
|
|
2957
|
+
if (v === undefined) continue;
|
|
2922
2958
|
entry[comp] = f32(v);
|
|
2923
|
-
components.push(comp);
|
|
2959
|
+
if (v !== 0) components.push(comp);
|
|
2924
2960
|
}
|
|
2925
2961
|
if (!components.length) {
|
|
2926
2962
|
// The parser is happy with this and the constraint does nothing at all.
|
|
@@ -2929,9 +2965,9 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2929
2965
|
`physics constraint "${name}" drives no component — set at least one of ${PHYSICS_COMPONENTS.join('/')}`,
|
|
2930
2966
|
);
|
|
2931
2967
|
}
|
|
2932
|
-
for (const [param
|
|
2968
|
+
for (const [param] of PHYSICS_PARAMS) {
|
|
2933
2969
|
const v = spec[param as keyof typeof spec] as number | undefined;
|
|
2934
|
-
if (v === undefined
|
|
2970
|
+
if (v === undefined) continue;
|
|
2935
2971
|
entry[param] = f32(v);
|
|
2936
2972
|
}
|
|
2937
2973
|
constraints.push(entry);
|
|
@@ -2977,12 +3013,15 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2977
3013
|
// the refusal below reports as the skins it looked in, so it is read from the
|
|
2978
3014
|
// map rather than restated.
|
|
2979
3015
|
const attachmentIndex: SlotAttachmentIndex = { bySlot: attachmentsBySlot, searched: [...skinTables.keys()] };
|
|
2980
|
-
|
|
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>();
|
|
2981
3019
|
const declaredDurations: Record<string, number> = {};
|
|
2982
3020
|
const slotNames = new Set(slots.map((s) => s.name));
|
|
2983
|
-
// Which slots an `rgba2` timeline may be keyed on: the
|
|
2984
|
-
//
|
|
2985
|
-
//
|
|
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,
|
|
2986
3025
|
// not on the spec that produced it. A slot whose `dark` never reached the
|
|
2987
3026
|
// artifact is a slot the runtime allocates no dark colour for (issue #690).
|
|
2988
3027
|
const darkSlots = new Set(slots.filter((s) => s.dark !== undefined).map((s) => s.name));
|
|
@@ -2991,13 +3030,24 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2991
3030
|
checkMotionGroups(motion);
|
|
2992
3031
|
for (const [animName, anim] of Object.entries(motion.animations)) {
|
|
2993
3032
|
declaredDurations[animName] = anim.duration;
|
|
2994
|
-
const slotTimelines
|
|
2995
|
-
const boneTimelines
|
|
2996
|
-
/**
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
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;
|
|
3001
3051
|
};
|
|
3002
3052
|
const claimed = new Set<string>();
|
|
3003
3053
|
let compiledDuration = 0;
|
|
@@ -3025,7 +3075,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3025
3075
|
);
|
|
3026
3076
|
}
|
|
3027
3077
|
if (everyGlobal) {
|
|
3028
|
-
const physics = constraints.filter((one) => one.
|
|
3078
|
+
const physics = constraints.filter((one) => one.kind === 'physics');
|
|
3029
3079
|
const flag = `${track.property}Global`;
|
|
3030
3080
|
const reached =
|
|
3031
3081
|
track.property === 'reset'
|
|
@@ -3097,7 +3147,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3097
3147
|
// the fault is the pair, not a key.
|
|
3098
3148
|
if (family === null && !isBoneTrack) {
|
|
3099
3149
|
const mine = SLOT_COLOR_CHANNELS[track.property] ?? [];
|
|
3100
|
-
for (const other of
|
|
3150
|
+
for (const other of slotTimelines.get(target)?.keys() ?? []) {
|
|
3101
3151
|
const shared = (SLOT_COLOR_CHANNELS[other] ?? []).filter((channel) => mine.includes(channel));
|
|
3102
3152
|
if (shared.length === 0) continue;
|
|
3103
3153
|
throw new CompileError(
|
|
@@ -3127,11 +3177,12 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3127
3177
|
: isBoneTrack
|
|
3128
3178
|
? compileValueTrack(resolved, motion, animName, anim.duration, target, shift, BONE_TRACKS, 'bone')
|
|
3129
3179
|
: compileTrack(resolved, motion, animName, anim.duration, target, shift, attachmentIndex, darkSlots);
|
|
3130
|
-
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time
|
|
3131
|
-
//
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
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);
|
|
3135
3186
|
});
|
|
3136
3187
|
}
|
|
3137
3188
|
|
|
@@ -3142,9 +3193,9 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3142
3193
|
// The target is resolved by name AND by type: `findConstraint(name,
|
|
3143
3194
|
// IkConstraintData)` misses a transform constraint of the same name and the
|
|
3144
3195
|
// parser throws in the consumer's process, so the mismatch is named here.
|
|
3145
|
-
const constraintTimelines: Record<'ik' | 'transform',
|
|
3146
|
-
ik:
|
|
3147
|
-
transform:
|
|
3196
|
+
const constraintTimelines: Record<'ik' | 'transform', Map<string, ModelKey[]>> = {
|
|
3197
|
+
ik: new Map(),
|
|
3198
|
+
transform: new Map(),
|
|
3148
3199
|
};
|
|
3149
3200
|
for (const group of ['ik', 'transform'] as const) {
|
|
3150
3201
|
// The array, the entries and their `constraint` names are shapes, so
|
|
@@ -3169,7 +3220,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3169
3220
|
: `the rig declares no ${group} constraint at all`),
|
|
3170
3221
|
);
|
|
3171
3222
|
}
|
|
3172
|
-
if (constraintTimelines[group]
|
|
3223
|
+
if (constraintTimelines[group].has(name)) {
|
|
3173
3224
|
throw new CompileError(
|
|
3174
3225
|
`animation "${animName}" has two ${group} timelines on constraint "${name}"; ` +
|
|
3175
3226
|
'the group holds one timeline per constraint, so merge them into one',
|
|
@@ -3184,18 +3235,25 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3184
3235
|
// The ik table, asked only by the ik group: `leg` may also be a
|
|
3185
3236
|
// transform constraint, and a transform key set carries no flag for
|
|
3186
3237
|
// the rig's booleans to be stamped onto (issue #692).
|
|
3187
|
-
group === 'ik' ? (
|
|
3238
|
+
group === 'ik' ? (ikFlagsInEffect.get(name) ?? {}) : {},
|
|
3188
3239
|
);
|
|
3189
|
-
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time
|
|
3190
|
-
constraintTimelines[group]
|
|
3240
|
+
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3241
|
+
constraintTimelines[group].set(name, keys);
|
|
3191
3242
|
}
|
|
3192
3243
|
}
|
|
3193
3244
|
|
|
3194
3245
|
// -- deform timelines: keyed on a skin/slot/attachment triple ----------
|
|
3195
|
-
//
|
|
3196
|
-
//
|
|
3197
|
-
|
|
3198
|
-
|
|
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
|
+
};
|
|
3199
3257
|
const deformTracks: MotionDeformTrack[] = anim.deform ?? [];
|
|
3200
3258
|
for (const track of deformTracks) {
|
|
3201
3259
|
const skinName = track.skin ?? 'default';
|
|
@@ -3220,7 +3278,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3220
3278
|
`(it has: ${Object.keys(perSlot).join(', ')})`,
|
|
3221
3279
|
);
|
|
3222
3280
|
}
|
|
3223
|
-
if (deformTimelines
|
|
3281
|
+
if (deformTimelines.get(skinName)?.get(track.slot)?.has(track.attachment)) {
|
|
3224
3282
|
throw new CompileError(`${at}: two deform timelines on one attachment; merge them into one`);
|
|
3225
3283
|
}
|
|
3226
3284
|
const keys = compileDeformTrack(
|
|
@@ -3229,13 +3287,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3229
3287
|
animName,
|
|
3230
3288
|
anim.duration,
|
|
3231
3289
|
{
|
|
3232
|
-
...deformGeometryOf(attachment, at,
|
|
3290
|
+
...deformGeometryOf(attachment, at, transforms),
|
|
3233
3291
|
depth: attachmentDepths.get(`${skinName}/${track.slot}/${track.attachment}`) ?? null,
|
|
3234
3292
|
},
|
|
3235
3293
|
deformTransforms,
|
|
3236
3294
|
);
|
|
3237
|
-
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time
|
|
3238
|
-
(
|
|
3295
|
+
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3296
|
+
attachmentTimelinesOf(skinName, track.slot).set(track.attachment, { deform: keys });
|
|
3239
3297
|
}
|
|
3240
3298
|
|
|
3241
3299
|
// -- sequence timelines: the other attachment timeline (issue #729) -----
|
|
@@ -3267,22 +3325,22 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3267
3325
|
);
|
|
3268
3326
|
}
|
|
3269
3327
|
const keys = compileSequenceTrack(track, anim.duration, attachment, at);
|
|
3270
|
-
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time
|
|
3271
|
-
const slot = (
|
|
3272
|
-
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) {
|
|
3273
3331
|
throw new CompileError(`${at}: two sequence timelines on one attachment; merge them into one`);
|
|
3274
3332
|
}
|
|
3275
|
-
slot
|
|
3333
|
+
slot.set(track.attachment, { ...(slot.get(track.attachment) ?? {}), sequence: keys });
|
|
3276
3334
|
}
|
|
3277
3335
|
|
|
3278
|
-
const drawOrder = anim.drawOrder ? compileDrawOrder(anim.drawOrder, animName, anim.duration, slots) :
|
|
3279
|
-
|
|
3336
|
+
const drawOrder = anim.drawOrder ? compileDrawOrder(anim.drawOrder, animName, anim.duration, slots) : [];
|
|
3337
|
+
for (const key of drawOrder) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3280
3338
|
|
|
3281
|
-
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 ?? {}) : [];
|
|
3282
3340
|
// An event timeline counts towards the animation's length the same as any
|
|
3283
3341
|
// other: `readAnimation` takes the duration from the longest timeline it
|
|
3284
3342
|
// built, and `EventTimeline.getDuration()` is its last frame like the rest.
|
|
3285
|
-
|
|
3343
|
+
for (const key of eventKeys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3286
3344
|
|
|
3287
3345
|
// Rule 4: the declared duration is verified, because skeleton JSON does not
|
|
3288
3346
|
// carry one — the loader takes the max key time.
|
|
@@ -3298,133 +3356,101 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3298
3356
|
`animation "${animName}" declares duration ${anim.duration}s but its last key is at ${compiledDuration}s`,
|
|
3299
3357
|
);
|
|
3300
3358
|
}
|
|
3301
|
-
//
|
|
3302
|
-
//
|
|
3303
|
-
//
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
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
|
+
});
|
|
3317
3378
|
}
|
|
3318
3379
|
});
|
|
3319
3380
|
|
|
3320
3381
|
// -- 6. assemble -----------------------------------------------------------
|
|
3321
3382
|
//
|
|
3322
|
-
//
|
|
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
|
|
3323
3385
|
// `width`/`height` give an extent to, so a header carrying an origin for a box
|
|
3324
|
-
// it does not declare would be a shape no export has
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
// Event definitions. Emitted in the order the rig spec declares them — object
|
|
3341
|
-
// key order is the spec's, not a set's, so A18 stays a contract.
|
|
3342
|
-
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>();
|
|
3343
3401
|
for (const [name, def] of Object.entries(rig.events ?? {})) {
|
|
3344
|
-
const entry:
|
|
3402
|
+
const entry: ModelEvent = {};
|
|
3345
3403
|
if (def.int !== undefined) entry.int = def.int;
|
|
3346
3404
|
if (def.float !== undefined) entry.float = f32(def.float);
|
|
3347
3405
|
if (def.string !== undefined) entry.string = def.string;
|
|
3348
3406
|
if (def.audio !== undefined) entry.audio = def.audio;
|
|
3349
3407
|
if (def.volume !== undefined) entry.volume = f32(def.volume);
|
|
3350
3408
|
if (def.balance !== undefined) entry.balance = f32(def.balance);
|
|
3351
|
-
events
|
|
3409
|
+
events.set(name, entry);
|
|
3352
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
|
+
});
|
|
3353
3423
|
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
skins
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
name,
|
|
3373
|
-
...(parts?.bones.length ? { bones: parts.bones } : {}),
|
|
3374
|
-
...Object.fromEntries(
|
|
3375
|
-
RIG_SKIN_CONSTRAINT_KEYS.filter((key) => parts?.constraints[key].length).map((key) => [
|
|
3376
|
-
key,
|
|
3377
|
-
parts!.constraints[key],
|
|
3378
|
-
]),
|
|
3379
|
-
),
|
|
3380
|
-
attachments: editorSlotKeyOrder(attachments),
|
|
3381
|
-
};
|
|
3382
|
-
}),
|
|
3383
|
-
),
|
|
3384
|
-
// Between `skins` and `animations`, which is where the editor writes it. A
|
|
3385
|
-
// conditional spread rather than an assignment after the literal, so the key
|
|
3386
|
-
// lands in that position instead of at the end.
|
|
3387
|
-
...(Object.keys(events).length ? { events } : {}),
|
|
3388
|
-
// Keyed in the editor's own order rather than the motion spec's, because a
|
|
3389
|
-
// slider's reference to an animation is an ordinal in the format and the
|
|
3390
|
-
// editor re-sorts this object — see `editorAnimationOrder`. The sort is
|
|
3391
|
-
// applied HERE and not to the loop above, so what the compiler reads, the
|
|
3392
|
-
// order it reports durations in, and which animation a CompileError names
|
|
3393
|
-
// first are all still the spec's own; only the emitted key order moves.
|
|
3394
|
-
animations: editorAnimationOrder(animations),
|
|
3395
|
-
};
|
|
3396
|
-
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
|
+
);
|
|
3397
3442
|
|
|
3398
3443
|
for (const slot of slots) {
|
|
3399
3444
|
if (!boneNames.has(slot.bone)) throw new CompileError(`slot "${slot.name}" has no bone`);
|
|
3400
3445
|
}
|
|
3401
3446
|
|
|
3402
|
-
//
|
|
3403
|
-
// one
|
|
3404
|
-
|
|
3405
|
-
// are free to build in whatever order reads best and none of them states the
|
|
3406
|
-
// order a second time. It moves positions and nothing else: no key is added,
|
|
3407
|
-
// dropped or re-valued, and what the gate and `A18` read is this object's text.
|
|
3408
|
-
//
|
|
3409
|
-
// Before it, and on the same finished object: every key whose value is the
|
|
3410
|
-
// one the 4.3 parser reads in its absence is left out, the way the editor
|
|
3411
|
-
// leaves it out (issue #716 tranche 3). One pass beside the other rather than
|
|
3412
|
-
// folded into it, because each has its own table and the selftest plants a
|
|
3413
|
-
// wrong row into each on its own; both walk the object through
|
|
3414
|
-
// `forEachKindedObject`, so what a kind is stays said in one place. The spec
|
|
3415
|
-
// is untouched — an author may still write `x: 0`, and it is still the record
|
|
3416
|
-
// of what was written — and what the parser loads does not move (`S103`).
|
|
3417
|
-
inEditorKeyOrder(withoutParserDefaults(skeleton));
|
|
3418
|
-
|
|
3419
|
-
return {
|
|
3420
|
-
skeleton,
|
|
3421
|
-
skeletonText: `${JSON.stringify(skeleton, null, 2)}\n`,
|
|
3422
|
-
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 = {
|
|
3423
3450
|
images,
|
|
3424
3451
|
pageGrids: atlasIn === null ? [] : [...atlasIn.grids].map(([page, grid]) => ({ page, said: grid.said })),
|
|
3425
3452
|
droppedStates,
|
|
3426
3453
|
absentParts,
|
|
3427
|
-
declaredDurations,
|
|
3428
3454
|
meshBones: [...meshBones],
|
|
3429
3455
|
meshes,
|
|
3430
3456
|
physics: physicsReport,
|
|
@@ -3432,6 +3458,14 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3432
3458
|
trackDerivations,
|
|
3433
3459
|
rig: buildRigInfo(rig, bones, meshes, manifest, images),
|
|
3434
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
|
+
};
|
|
3435
3469
|
}
|
|
3436
3470
|
|
|
3437
3471
|
// ---------------------------------------------------------------------------
|
|
@@ -3447,7 +3481,7 @@ interface BoneContext {
|
|
|
3447
3481
|
}
|
|
3448
3482
|
|
|
3449
3483
|
/**
|
|
3450
|
-
* One rig bone -> one
|
|
3484
|
+
* One rig bone -> one model bone (`ModelBone`); `emitBones` writes it as Spine's.
|
|
3451
3485
|
*
|
|
3452
3486
|
* 🔑 A field is emitted exactly when the spec declared it. That is not Spine's
|
|
3453
3487
|
* own exporter convention (it omits anything equal to the default) and the
|
|
@@ -3455,8 +3489,8 @@ interface BoneContext {
|
|
|
3455
3489
|
* deciding emission from the arithmetic rather than from the author's text makes
|
|
3456
3490
|
* the file depend on a rounding.
|
|
3457
3491
|
*/
|
|
3458
|
-
function buildBone(spec: RigBone, soFar:
|
|
3459
|
-
const bone:
|
|
3492
|
+
function buildBone(spec: RigBone, soFar: ModelBone[], ctx: BoneContext): ModelBone {
|
|
3493
|
+
const bone: ModelBone = { name: spec.name };
|
|
3460
3494
|
if (spec.parent !== undefined) bone.parent = spec.parent;
|
|
3461
3495
|
if (spec.length !== undefined) bone.length = f32(spec.length);
|
|
3462
3496
|
|
|
@@ -3487,10 +3521,13 @@ function buildBone(spec: RigBone, soFar: SpineBone[], ctx: BoneContext): SpineBo
|
|
|
3487
3521
|
if (spec.scaleY !== undefined) bone.scaleY = f32(spec.scaleY);
|
|
3488
3522
|
if (spec.shearX !== undefined) bone.shearX = f32(spec.shearX);
|
|
3489
3523
|
if (spec.shearY !== undefined) bone.shearY = f32(spec.shearY);
|
|
3490
|
-
if (spec.inherit !== undefined) bone.
|
|
3491
|
-
if (spec.skin !== undefined) bone.
|
|
3492
|
-
if (spec.color !== undefined
|
|
3493
|
-
|
|
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
|
+
}
|
|
3494
3531
|
return bone;
|
|
3495
3532
|
}
|
|
3496
3533
|
|
|
@@ -3615,9 +3652,9 @@ function rotationOf(spec: RigBone, ctx: BoneContext): number | null {
|
|
|
3615
3652
|
* with a name was in that set, so for those types the position is the same
|
|
3616
3653
|
* choice rather than a second measurement.
|
|
3617
3654
|
*/
|
|
3618
|
-
function withStatedName(att:
|
|
3655
|
+
function withStatedName(att: SkinTableEntry, stated: RigAttachment): SkinTableEntry {
|
|
3619
3656
|
const name = (stated as { name?: string }).name;
|
|
3620
|
-
return name === undefined ? att :
|
|
3657
|
+
return name === undefined ? att : { name, ...att };
|
|
3621
3658
|
}
|
|
3622
3659
|
|
|
3623
3660
|
/**
|
|
@@ -3728,7 +3765,7 @@ function refuseDefaultSkinContests(skinNames: readonly string[], skinParts: Map<
|
|
|
3728
3765
|
|
|
3729
3766
|
interface AttachmentContext {
|
|
3730
3767
|
images: CompiledImage[];
|
|
3731
|
-
bones:
|
|
3768
|
+
bones: ModelBone[];
|
|
3732
3769
|
transforms: Map<string, BoneTransform>;
|
|
3733
3770
|
meshBones: Set<string>;
|
|
3734
3771
|
meshes: CompileResult['meshes'];
|
|
@@ -3755,6 +3792,12 @@ interface AttachmentContext {
|
|
|
3755
3792
|
depths: Map<string, number[]>;
|
|
3756
3793
|
/** Every slot the rig declares — a clipping attachment's `end` resolves here. */
|
|
3757
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;
|
|
3758
3801
|
/**
|
|
3759
3802
|
* Linked meshes whose `source` is still unresolved — `resolveLinkedMeshes`
|
|
3760
3803
|
* empties it once every skin has been built.
|
|
@@ -3844,7 +3887,7 @@ function buildRigAttachment(
|
|
|
3844
3887
|
placeholder: string,
|
|
3845
3888
|
where: string,
|
|
3846
3889
|
ctx: AttachmentContext,
|
|
3847
|
-
):
|
|
3890
|
+
): SkinTableEntry {
|
|
3848
3891
|
const stated = (att as { type?: unknown }).type;
|
|
3849
3892
|
if (stated !== undefined && typeof stated !== 'string') {
|
|
3850
3893
|
throw new CompileError(
|
|
@@ -3913,7 +3956,8 @@ function deferredAttachmentRefusal(type: string, where: string, how: string): st
|
|
|
3913
3956
|
}
|
|
3914
3957
|
|
|
3915
3958
|
/**
|
|
3916
|
-
*
|
|
3959
|
+
* The polygon a bounding box, a clipping attachment or a path carries, as the
|
|
3960
|
+
* model's `ModelVertices`.
|
|
3917
3961
|
*
|
|
3918
3962
|
* 🚨 `vertexCount` is required, and everything else here is a cross-check of it.
|
|
3919
3963
|
* The parser reads `map.vertexCount << 1` and hands that to `readVertices` as the
|
|
@@ -3923,12 +3967,12 @@ function deferredAttachmentRefusal(type: string, where: string, how: string): st
|
|
|
3923
3967
|
* whatever that garbage produced. It loads. It draws nothing (a bounding box
|
|
3924
3968
|
* never did) and clips nothing, or clips the wrong shape, in complete silence.
|
|
3925
3969
|
*
|
|
3926
|
-
* The two encodings are the mesh's — `
|
|
3970
|
+
* The two encodings are the mesh's — `bindNamedWeights` is the same function —
|
|
3927
3971
|
* because they are the same field with the same trap: `readVertices` decides
|
|
3928
3972
|
* weighted vs unweighted by a length comparison alone, and a coincidental match
|
|
3929
|
-
* reads weight data as coordinates.
|
|
3973
|
+
* reads weight data as coordinates. The model says which one it is outright.
|
|
3930
3974
|
*/
|
|
3931
|
-
function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: AttachmentContext):
|
|
3975
|
+
function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: AttachmentContext): ModelVertices {
|
|
3932
3976
|
const count = att.vertexCount;
|
|
3933
3977
|
if (typeof count !== 'number' || !Number.isInteger(count) || count < 3) {
|
|
3934
3978
|
throw new CompileError(
|
|
@@ -3948,7 +3992,7 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
|
|
|
3948
3992
|
if (att.weights.length !== count) {
|
|
3949
3993
|
throw new CompileError(`${where}: weights cover ${att.weights.length} vertices but vertexCount is ${count}`);
|
|
3950
3994
|
}
|
|
3951
|
-
return
|
|
3995
|
+
return { weighted: true, bindings: bindNamedWeights(att.weights, where, ctx) };
|
|
3952
3996
|
}
|
|
3953
3997
|
const raw = att.vertices;
|
|
3954
3998
|
if (!raw || raw.length === 0) {
|
|
@@ -3967,48 +4011,89 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
|
|
|
3967
4011
|
}
|
|
3968
4012
|
// A raw run still has to decode to exactly `vertexCount` vertices, or the
|
|
3969
4013
|
// count and the polygon disagree and the parser believes the count.
|
|
3970
|
-
|
|
3971
|
-
for (let i = 0; i < raw.length; decoded++) {
|
|
3972
|
-
const bones = raw[i++];
|
|
3973
|
-
if (!Number.isInteger(bones) || bones < 1) {
|
|
3974
|
-
throw new CompileError(`${where}: the raw weighted run has a bone count of ${String(bones)} at index ${i - 1}`);
|
|
3975
|
-
}
|
|
3976
|
-
i += bones * 4;
|
|
3977
|
-
if (i > raw.length) {
|
|
3978
|
-
throw new CompileError(
|
|
3979
|
-
`${where}: the raw weighted run is truncated — vertex ${decoded} claims ${bones} bone(s) and the array ends first`,
|
|
3980
|
-
);
|
|
3981
|
-
}
|
|
3982
|
-
}
|
|
3983
|
-
if (decoded !== count) {
|
|
3984
|
-
throw new CompileError(`${where}: the raw weighted run decodes to ${decoded} vertices but vertexCount is ${count}`);
|
|
3985
|
-
}
|
|
3986
|
-
// Register the bones it binds so the mesh-bone reports stay complete.
|
|
3987
|
-
for (let i = 0; i < raw.length; ) {
|
|
3988
|
-
const bones = raw[i++];
|
|
3989
|
-
for (let k = 0; k < bones; k++, i += 4) {
|
|
3990
|
-
const bone = ctx.bones[raw[i]];
|
|
3991
|
-
if (bone) ctx.meshBones.add(bone.name);
|
|
3992
|
-
}
|
|
3993
|
-
}
|
|
4014
|
+
checkRawRunShape(raw, count, `vertexCount is ${count}`, where);
|
|
3994
4015
|
}
|
|
3995
4016
|
for (const n of raw) {
|
|
3996
4017
|
if (!Number.isFinite(n)) throw new CompileError(`${where}: the vertex array holds a non-finite value ${String(n)}`);
|
|
3997
4018
|
}
|
|
3998
|
-
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
|
+
};
|
|
3999
4084
|
}
|
|
4000
4085
|
|
|
4001
4086
|
function buildRigBoundingBox(
|
|
4002
4087
|
att: RigBoundingBoxAttachment,
|
|
4003
4088
|
where: string,
|
|
4004
4089
|
ctx: AttachmentContext,
|
|
4005
|
-
):
|
|
4006
|
-
const out:
|
|
4007
|
-
|
|
4090
|
+
): ModelBoundingBoxAttachment {
|
|
4091
|
+
const out: ModelBoundingBoxAttachment = {
|
|
4092
|
+
kind: 'boundingbox',
|
|
4008
4093
|
vertexCount: att.vertexCount,
|
|
4009
4094
|
vertices: buildVertexGeometry(att, where, ctx),
|
|
4010
4095
|
};
|
|
4011
|
-
if (att.color !== undefined) out.
|
|
4096
|
+
if (att.color !== undefined) out.editorColor = att.color;
|
|
4012
4097
|
return out;
|
|
4013
4098
|
}
|
|
4014
4099
|
|
|
@@ -4016,7 +4101,7 @@ function buildRigClipping(
|
|
|
4016
4101
|
att: RigClippingAttachment,
|
|
4017
4102
|
where: string,
|
|
4018
4103
|
ctx: AttachmentContext,
|
|
4019
|
-
):
|
|
4104
|
+
): ModelClippingAttachment {
|
|
4020
4105
|
if (att.end !== undefined) {
|
|
4021
4106
|
// `skeletonData.findSlot` returns null on a miss and the parser assigns that
|
|
4022
4107
|
// null (`:626-627`), so a typo does not fail — the clip simply never ends and
|
|
@@ -4028,57 +4113,50 @@ function buildRigClipping(
|
|
|
4028
4113
|
);
|
|
4029
4114
|
}
|
|
4030
4115
|
}
|
|
4031
|
-
// Field order is the
|
|
4032
|
-
//
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
const out: SpineClippingAttachment = {
|
|
4036
|
-
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',
|
|
4037
4120
|
...(att.end !== undefined ? { end: att.end } : {}),
|
|
4038
4121
|
...(att.convex !== undefined ? { convex: att.convex } : {}),
|
|
4039
4122
|
...(att.inverse !== undefined ? { inverse: att.inverse } : {}),
|
|
4040
4123
|
vertexCount: att.vertexCount,
|
|
4041
4124
|
vertices: buildVertexGeometry(att, where, ctx),
|
|
4042
4125
|
};
|
|
4043
|
-
if (att.color !== undefined) out.
|
|
4126
|
+
if (att.color !== undefined) out.editorColor = att.color;
|
|
4044
4127
|
return out;
|
|
4045
4128
|
}
|
|
4046
4129
|
|
|
4047
4130
|
/**
|
|
4048
|
-
* Setup-pose world position of every vertex of
|
|
4131
|
+
* Setup-pose world position of every vertex of a model vertex attachment.
|
|
4049
4132
|
*
|
|
4050
4133
|
* The two encodings again, and the same split `readVertices` makes: an unweighted
|
|
4051
|
-
*
|
|
4052
|
-
*
|
|
4053
|
-
*
|
|
4054
|
-
*
|
|
4055
|
-
*
|
|
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.
|
|
4056
4139
|
*/
|
|
4057
4140
|
function setupWorldVertices(
|
|
4058
|
-
vertices:
|
|
4059
|
-
vertexCount: number,
|
|
4141
|
+
vertices: ModelVertices,
|
|
4060
4142
|
anchor: BoneTransform,
|
|
4061
|
-
bones: SpineBone[],
|
|
4062
4143
|
transforms: Map<string, BoneTransform>,
|
|
4063
4144
|
where: string,
|
|
4064
4145
|
): Array<[number, number]> {
|
|
4065
4146
|
const out: Array<[number, number]> = [];
|
|
4066
|
-
if (vertices.
|
|
4067
|
-
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]));
|
|
4068
4149
|
return out;
|
|
4069
4150
|
}
|
|
4070
|
-
for (
|
|
4071
|
-
const count = vertices[i++];
|
|
4151
|
+
for (const vertex of vertices.bindings) {
|
|
4072
4152
|
let x = 0;
|
|
4073
4153
|
let y = 0;
|
|
4074
|
-
for (
|
|
4075
|
-
const
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
x += wx * weight;
|
|
4081
|
-
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;
|
|
4082
4160
|
}
|
|
4083
4161
|
out.push([x, y]);
|
|
4084
4162
|
}
|
|
@@ -4107,8 +4185,11 @@ function setupWorldVertices(
|
|
|
4107
4185
|
* prefix, so the entries the runtime reads are the same numbers either way; the
|
|
4108
4186
|
* trailing one is read by nothing, and is written so a rebuild is the file the
|
|
4109
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.
|
|
4110
4191
|
*/
|
|
4111
|
-
function pathChain(points: Array<[number, number]>, closed: boolean): Array<[number, number]> {
|
|
4192
|
+
export function pathChain(points: Array<[number, number]>, closed: boolean): Array<[number, number]> {
|
|
4112
4193
|
if (!closed) return points.slice(1, points.length - 1);
|
|
4113
4194
|
return [...points.slice(1), points[0], points[1]];
|
|
4114
4195
|
}
|
|
@@ -4175,7 +4256,7 @@ function pathChain(points: Array<[number, number]>, closed: boolean): Array<[num
|
|
|
4175
4256
|
* running total carried across curves rather than restarted. Each of those is a
|
|
4176
4257
|
* place where a more accurate line would emit a different file.
|
|
4177
4258
|
*/
|
|
4178
|
-
function pathCurveLengths(chain: Array<[number, number]>): number[] {
|
|
4259
|
+
export function pathCurveLengths(chain: Array<[number, number]>): number[] {
|
|
4179
4260
|
const out: number[] = [];
|
|
4180
4261
|
let total = 0;
|
|
4181
4262
|
for (let c = 0; c + 3 < chain.length; c += 3) {
|
|
@@ -4229,7 +4310,7 @@ function pathCurveLengths(chain: Array<[number, number]>): number[] {
|
|
|
4229
4310
|
* 3. **An omitted one is measured**, `vertexCount / 3` entries over the closed
|
|
4230
4311
|
* chain (`pathChain`), on the unconstrained setup pose.
|
|
4231
4312
|
*/
|
|
4232
|
-
function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentContext):
|
|
4313
|
+
function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentContext): ModelPathAttachment {
|
|
4233
4314
|
const vertices = buildVertexGeometry(att, where, ctx);
|
|
4234
4315
|
const count = att.vertexCount;
|
|
4235
4316
|
const closed = att.closed === true;
|
|
@@ -4254,7 +4335,7 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
|
|
|
4254
4335
|
} else {
|
|
4255
4336
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
4256
4337
|
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
4257
|
-
const points = setupWorldVertices(vertices,
|
|
4338
|
+
const points = setupWorldVertices(vertices, anchor, ctx.transforms, where);
|
|
4258
4339
|
lengths = pathCurveLengths(pathChain(points, true));
|
|
4259
4340
|
// The total the runtime reads is the last CURVE's entry, `lengths[vertexCount
|
|
4260
4341
|
// / 3 - (closed ? 1 : 2)]` (`PathConstraint.js:204-206`) — on an open path
|
|
@@ -4267,17 +4348,17 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
|
|
|
4267
4348
|
);
|
|
4268
4349
|
}
|
|
4269
4350
|
}
|
|
4270
|
-
//
|
|
4271
|
-
//
|
|
4272
|
-
//
|
|
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.
|
|
4273
4354
|
return {
|
|
4274
|
-
|
|
4355
|
+
kind: 'path',
|
|
4275
4356
|
...(att.closed !== undefined ? { closed: att.closed } : {}),
|
|
4276
4357
|
...(att.constantSpeed !== undefined ? { constantSpeed: att.constantSpeed } : {}),
|
|
4277
4358
|
vertexCount: count,
|
|
4278
4359
|
vertices,
|
|
4279
4360
|
lengths: lengths.map(f32),
|
|
4280
|
-
...(att.color !== undefined ? {
|
|
4361
|
+
...(att.color !== undefined ? { editorColor: att.color } : {}),
|
|
4281
4362
|
};
|
|
4282
4363
|
}
|
|
4283
4364
|
|
|
@@ -4406,8 +4487,9 @@ function attachmentPath(att: { name?: string; path?: string; image?: string }, p
|
|
|
4406
4487
|
}
|
|
4407
4488
|
|
|
4408
4489
|
/**
|
|
4409
|
-
* A mesh attachment's `path` and `color
|
|
4410
|
-
*
|
|
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).
|
|
4411
4493
|
*
|
|
4412
4494
|
* 🔬 The editor writes a mesh `type, path, color, uvs, …`: right after `type`,
|
|
4413
4495
|
* `path` before `color` — measured on a production set (#791), not on
|
|
@@ -4416,16 +4498,17 @@ function attachmentPath(att: { name?: string; path?: string; image?: string }, p
|
|
|
4416
4498
|
* `path` before `color` is the order the two measured positions leave, not a
|
|
4417
4499
|
* third measurement. The twelve exports carry neither key on a mesh, which is
|
|
4418
4500
|
* why `EDITOR_KEY_ORDER`'s `mesh attachment` row does not list them: an unlisted
|
|
4419
|
-
* key keeps its constructor's index, so the position is stated
|
|
4420
|
-
* 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.
|
|
4421
4504
|
*
|
|
4422
4505
|
* ⚠️ Not a linked mesh's, and not a region's. Neither was in the measured set —
|
|
4423
4506
|
* a region's `path` stays where `buildRigRegion` puts it, a linked mesh's
|
|
4424
4507
|
* keys where `buildRigLinkedMesh` does — because the analogy to a mesh is
|
|
4425
4508
|
* obvious and it is not a measurement.
|
|
4426
4509
|
*/
|
|
4427
|
-
function meshTextureKeys(att: { name?: string; path?: string; image?: string; color?: string }, placeholder: string): Pick<
|
|
4428
|
-
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'> = {};
|
|
4429
4512
|
const path = attachmentPath(att, placeholder);
|
|
4430
4513
|
if (path !== undefined) out.path = path;
|
|
4431
4514
|
if (att.color !== undefined) out.color = att.color;
|
|
@@ -4449,9 +4532,9 @@ function sequenceFrameRegion(stem: string, seq: RigSequence, i: number): string
|
|
|
4449
4532
|
return `${stem}${'0'.repeat(Math.max(0, (seq.digits ?? 0) - frame.length))}${frame}`;
|
|
4450
4533
|
}
|
|
4451
4534
|
|
|
4452
|
-
/** The
|
|
4453
|
-
function
|
|
4454
|
-
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 };
|
|
4455
4538
|
if (seq.start !== undefined) out.start = seq.start;
|
|
4456
4539
|
if (seq.digits !== undefined) out.digits = seq.digits;
|
|
4457
4540
|
if (seq.setup !== undefined) out.setup = seq.setup;
|
|
@@ -4539,10 +4622,11 @@ function buildRigRegion(
|
|
|
4539
4622
|
placeholder: string,
|
|
4540
4623
|
where: string,
|
|
4541
4624
|
ctx: AttachmentContext,
|
|
4542
|
-
):
|
|
4625
|
+
): ModelRegionAttachment {
|
|
4543
4626
|
if (att.sequence !== undefined) {
|
|
4544
|
-
const
|
|
4545
|
-
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!) };
|
|
4546
4630
|
if (att.path !== undefined) out.path = att.path;
|
|
4547
4631
|
if (att.x !== undefined) out.x = f32(att.x);
|
|
4548
4632
|
if (att.y !== undefined) out.y = f32(att.y);
|
|
@@ -4550,7 +4634,9 @@ function buildRigRegion(
|
|
|
4550
4634
|
if (att.scaleX !== undefined) out.scaleX = f32(att.scaleX);
|
|
4551
4635
|
if (att.scaleY !== undefined) out.scaleY = f32(att.scaleY);
|
|
4552
4636
|
if (att.color !== undefined) out.color = att.color;
|
|
4553
|
-
|
|
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;
|
|
4554
4640
|
return out;
|
|
4555
4641
|
}
|
|
4556
4642
|
const img = att.image === undefined ? null : atlasedImage(att.image, where, ctx);
|
|
@@ -4592,7 +4678,7 @@ function buildRigRegion(
|
|
|
4592
4678
|
`${where}: a region needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
4593
4679
|
);
|
|
4594
4680
|
}
|
|
4595
|
-
const out:
|
|
4681
|
+
const out: ModelRegionAttachment = { kind: 'region', width: f32(width), height: f32(height) };
|
|
4596
4682
|
const path = attachmentPath(att, placeholder);
|
|
4597
4683
|
if (path !== undefined) out.path = path;
|
|
4598
4684
|
if (att.x !== undefined) out.x = f32(att.x);
|
|
@@ -4601,41 +4687,89 @@ function buildRigRegion(
|
|
|
4601
4687
|
if (att.scaleX !== undefined) out.scaleX = f32(att.scaleX);
|
|
4602
4688
|
if (att.scaleY !== undefined) out.scaleY = f32(att.scaleY);
|
|
4603
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);
|
|
4604
4693
|
return out;
|
|
4605
4694
|
}
|
|
4606
4695
|
|
|
4607
4696
|
/**
|
|
4608
|
-
*
|
|
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).
|
|
4609
4726
|
*
|
|
4610
|
-
*
|
|
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
|
|
4611
4748
|
* `boneCount, (boneIndex, bindX, bindY, weight) x n` per vertex and those
|
|
4612
4749
|
* indices are positions in the emitted bone array — a thing the rig spec never
|
|
4613
|
-
* 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
|
|
4614
4752
|
* renumbering rather than a rebinding: the names still point at the same bones,
|
|
4615
4753
|
* so the emitted indices move and the mesh does not.
|
|
4616
4754
|
*
|
|
4617
4755
|
* An unknown name is a `CompileError`, the same as a bone's `parent`, a slot's
|
|
4618
4756
|
* `bone` or a constraint's `target`. The alternative — the raw form — cannot
|
|
4619
|
-
* refuse
|
|
4757
|
+
* refuse a rebind, because an index has no name to be wrong.
|
|
4620
4758
|
*/
|
|
4621
|
-
function
|
|
4622
|
-
|
|
4623
|
-
weights.forEach((vertex, i) => {
|
|
4759
|
+
function bindNamedWeights(weights: RigMeshBinding[][], where: string, ctx: AttachmentContext): ModelBinding[][] {
|
|
4760
|
+
return weights.map((vertex, i) => {
|
|
4624
4761
|
if (!Array.isArray(vertex) || vertex.length === 0) {
|
|
4625
4762
|
throw new CompileError(`${where}: vertex ${i} has no bone bindings; a weighted vertex names at least one bone`);
|
|
4626
4763
|
}
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
const index = ctx.bones.findIndex((b) => b.name === binding.bone);
|
|
4630
|
-
if (index < 0) {
|
|
4764
|
+
return vertex.map((binding) => {
|
|
4765
|
+
if (!ctx.bones.some((b) => b.name === binding.bone)) {
|
|
4631
4766
|
throw new CompileError(
|
|
4632
4767
|
`${where}: vertex ${i} binds bone ${JSON.stringify(binding.bone)}, which the rig does not declare as a bone`,
|
|
4633
4768
|
);
|
|
4634
4769
|
}
|
|
4635
|
-
|
|
4636
|
-
}
|
|
4770
|
+
return { bone: binding.bone, x: f32(binding.x), y: f32(binding.y), weight: f32(binding.weight) };
|
|
4771
|
+
});
|
|
4637
4772
|
});
|
|
4638
|
-
return out;
|
|
4639
4773
|
}
|
|
4640
4774
|
|
|
4641
4775
|
/**
|
|
@@ -4780,7 +4914,7 @@ function buildRigMesh(
|
|
|
4780
4914
|
placeholder: string,
|
|
4781
4915
|
where: string,
|
|
4782
4916
|
ctx: AttachmentContext,
|
|
4783
|
-
):
|
|
4917
|
+
): ModelMeshAttachment {
|
|
4784
4918
|
const authored =
|
|
4785
4919
|
att.uvs !== undefined || att.triangles !== undefined || att.vertices !== undefined || att.weights !== undefined;
|
|
4786
4920
|
if (authored && att.generator) {
|
|
@@ -4796,7 +4930,7 @@ function buildRigMesh(
|
|
|
4796
4930
|
throw new CompileError(`${where}: an authored mesh needs uvs, triangles and vertices or weights (or a "generator")`);
|
|
4797
4931
|
}
|
|
4798
4932
|
const uvCount = att.uvs.length;
|
|
4799
|
-
let vertices:
|
|
4933
|
+
let vertices: ModelVertices;
|
|
4800
4934
|
let boundBones: string[] = [];
|
|
4801
4935
|
if (att.weights) {
|
|
4802
4936
|
if (att.boneIndexing === 'raw') {
|
|
@@ -4807,7 +4941,7 @@ function buildRigMesh(
|
|
|
4807
4941
|
`${where}: weights cover ${att.weights.length} vertices but there are ${uvCount / 2} uv pairs`,
|
|
4808
4942
|
);
|
|
4809
4943
|
}
|
|
4810
|
-
vertices =
|
|
4944
|
+
vertices = { weighted: true, bindings: bindNamedWeights(att.weights, where, ctx) };
|
|
4811
4945
|
boundBones = [...new Set(att.weights.flat().map((b) => b.bone))];
|
|
4812
4946
|
} else {
|
|
4813
4947
|
const raw = att.vertices!;
|
|
@@ -4822,17 +4956,17 @@ function buildRigMesh(
|
|
|
4822
4956
|
'or say "boneIndexing": "raw" on this attachment to keep the index form deliberately.',
|
|
4823
4957
|
);
|
|
4824
4958
|
}
|
|
4825
|
-
vertices = raw.map(f32);
|
|
4826
4959
|
if (weighted) {
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
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) };
|
|
4836
4970
|
}
|
|
4837
4971
|
}
|
|
4838
4972
|
const { hull, edges } = authoredHullAndEdges(att, uvCount / 2, att.triangles, where);
|
|
@@ -4852,8 +4986,8 @@ function buildRigMesh(
|
|
|
4852
4986
|
`${where}: a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
4853
4987
|
);
|
|
4854
4988
|
}
|
|
4855
|
-
const out:
|
|
4856
|
-
|
|
4989
|
+
const out: ModelMeshAttachment = {
|
|
4990
|
+
kind: 'mesh',
|
|
4857
4991
|
...meshTextureKeys(att, placeholder),
|
|
4858
4992
|
uvs: att.uvs.map(f32),
|
|
4859
4993
|
triangles: att.triangles,
|
|
@@ -4863,7 +4997,7 @@ function buildRigMesh(
|
|
|
4863
4997
|
width: f32(width),
|
|
4864
4998
|
height: f32(height),
|
|
4865
4999
|
};
|
|
4866
|
-
if (att.sequence !== undefined) out.sequence =
|
|
5000
|
+
if (att.sequence !== undefined) out.sequence = buildSequence(att.sequence);
|
|
4867
5001
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
4868
5002
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
4869
5003
|
// read this and skip rather than measuring a ring that was never a ring.
|
|
@@ -4916,7 +5050,7 @@ function buildRigLinkedMesh(
|
|
|
4916
5050
|
placeholder: string,
|
|
4917
5051
|
where: string,
|
|
4918
5052
|
ctx: AttachmentContext,
|
|
4919
|
-
):
|
|
5053
|
+
): ModelLinkedMeshAttachment {
|
|
4920
5054
|
// The mesh keys the parser does not reach on this branch. Named one by one,
|
|
4921
5055
|
// because "remove what does not belong" is not an instruction an author can
|
|
4922
5056
|
// act on and the remedy for each of these is the same single sentence.
|
|
@@ -4968,17 +5102,23 @@ function buildRigLinkedMesh(
|
|
|
4968
5102
|
`${where}: a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
4969
5103
|
);
|
|
4970
5104
|
}
|
|
4971
|
-
|
|
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
|
+
};
|
|
4972
5118
|
const path = attachmentPath(att, placeholder);
|
|
4973
5119
|
if (path !== undefined) out.path = path;
|
|
4974
|
-
// Only where they differ from the parser's own defaults. Writing `timelines:
|
|
4975
|
-
// true`, or a `skin` of "default", would be a byte the editor's own export
|
|
4976
|
-
// does not carry.
|
|
4977
|
-
if (att.slot !== undefined && att.slot !== ctx.slotName) out.slot = att.slot;
|
|
4978
|
-
if (att.skin !== undefined && att.skin !== 'default') out.skin = att.skin;
|
|
4979
|
-
if (att.timelines === false) out.timelines = false;
|
|
4980
5120
|
if (att.color !== undefined) out.color = att.color;
|
|
4981
|
-
if (att.sequence !== undefined) out.sequence =
|
|
5121
|
+
if (att.sequence !== undefined) out.sequence = buildSequence(att.sequence);
|
|
4982
5122
|
// 🚫 NOT registered in `ctx.meshes`, and that is a decision rather than an
|
|
4983
5123
|
// omission. `meshKinds` is keyed by SLOT and the commonest linked mesh shares
|
|
4984
5124
|
// its source's slot from another skin, so an entry here would overwrite the
|
|
@@ -5005,7 +5145,7 @@ function buildRigLinkedMesh(
|
|
|
5005
5145
|
*/
|
|
5006
5146
|
function resolveLinkedMeshes(
|
|
5007
5147
|
links: readonly PendingLink[],
|
|
5008
|
-
tables:
|
|
5148
|
+
tables: ReadonlyMap<string, SkinTable>,
|
|
5009
5149
|
): void {
|
|
5010
5150
|
const skinNames = [...tables.keys()];
|
|
5011
5151
|
for (const link of links) {
|
|
@@ -5046,7 +5186,7 @@ function resolveLinkedMeshes(
|
|
|
5046
5186
|
"Left to the round trip this is the runtime's `Source mesh not found`.",
|
|
5047
5187
|
);
|
|
5048
5188
|
}
|
|
5049
|
-
const type =
|
|
5189
|
+
const type = found.kind;
|
|
5050
5190
|
if (type === 'linkedmesh') {
|
|
5051
5191
|
throw new CompileError(
|
|
5052
5192
|
`${link.where}: "source" is ${JSON.stringify(link.source)}, which is itself a linked mesh, and a chain of ` +
|
|
@@ -5081,25 +5221,24 @@ function buildGeneratedMesh(
|
|
|
5081
5221
|
placeholder: string,
|
|
5082
5222
|
where: string,
|
|
5083
5223
|
ctx: AttachmentContext,
|
|
5084
|
-
):
|
|
5224
|
+
): ModelMeshAttachment {
|
|
5085
5225
|
if (generator.kind === 'contour') return buildContourAttachment(att, generator, placeholder, where, ctx);
|
|
5086
5226
|
if (generator.kind === 'grid') return buildGridAttachment(att, generator, placeholder, where, ctx);
|
|
5087
5227
|
if (generator.kind === 'segments') return buildSegmentsAttachment(att, generator, placeholder, where, ctx);
|
|
5088
5228
|
const controls = generator.kind === 'ring' ? generator.controls : generator.chain;
|
|
5089
|
-
// Resolving the bone
|
|
5090
|
-
// refusals, because the ring's control angles need the transform and the
|
|
5091
|
-
// 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
|
|
5092
5232
|
// words whichever of them asks for it first.
|
|
5093
|
-
const resolve = (name: string):
|
|
5094
|
-
|
|
5095
|
-
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`);
|
|
5096
5235
|
const transform = ctx.transforms.get(name);
|
|
5097
5236
|
if (!transform) throw new CompileError(`${where}: no setup transform for mesh bone "${name}"`);
|
|
5098
|
-
return
|
|
5237
|
+
return transform;
|
|
5099
5238
|
};
|
|
5100
5239
|
const refFor = (name: string): MeshBoneRef => {
|
|
5101
|
-
const
|
|
5102
|
-
return {
|
|
5240
|
+
const transform = resolve(name);
|
|
5241
|
+
return { name, toBind: (wx, wy) => toBoneLocal(transform, wx, wy) };
|
|
5103
5242
|
};
|
|
5104
5243
|
// The generator works in part-local pixels, y down. Without a manifest there is
|
|
5105
5244
|
// no crop to flip against, so the part window is centred on its own slot bone.
|
|
@@ -5123,7 +5262,7 @@ function buildGeneratedMesh(
|
|
|
5123
5262
|
size: generator.size,
|
|
5124
5263
|
bias: generator.bias,
|
|
5125
5264
|
controlAngles: ringControlAngles(controls, generator.center, (name) => {
|
|
5126
|
-
const
|
|
5265
|
+
const transform = resolve(name);
|
|
5127
5266
|
return toPartLocal(transform.worldX, transform.worldY);
|
|
5128
5267
|
}),
|
|
5129
5268
|
});
|
|
@@ -5131,7 +5270,7 @@ function buildGeneratedMesh(
|
|
|
5131
5270
|
if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
|
|
5132
5271
|
throw err;
|
|
5133
5272
|
}
|
|
5134
|
-
const vertices =
|
|
5273
|
+
const vertices = bindWeightedVertices(
|
|
5135
5274
|
geometry,
|
|
5136
5275
|
// The world point is an INTERMEDIATE — `toBind` makes it local and the
|
|
5137
5276
|
// result is what is emitted, through `f32` below — so it carries the double.
|
|
@@ -5150,12 +5289,12 @@ function buildGeneratedMesh(
|
|
|
5150
5289
|
triangles: geometry.triangles.length / 3,
|
|
5151
5290
|
bones: [ctx.anchorBone, ...controls],
|
|
5152
5291
|
});
|
|
5153
|
-
const out:
|
|
5154
|
-
|
|
5292
|
+
const out: ModelMeshAttachment = {
|
|
5293
|
+
kind: 'mesh',
|
|
5155
5294
|
...meshTextureKeys(att, placeholder),
|
|
5156
5295
|
uvs: geometry.uvs.map(f32),
|
|
5157
5296
|
triangles: geometry.triangles,
|
|
5158
|
-
vertices: vertices
|
|
5297
|
+
vertices: bindingsOnF32(vertices),
|
|
5159
5298
|
...generatedHullAndEdges(geometry, where),
|
|
5160
5299
|
width: f32(w),
|
|
5161
5300
|
height: f32(h),
|
|
@@ -5231,7 +5370,7 @@ function sampleMeshDepth(
|
|
|
5231
5370
|
triangles: ReadonlyArray<number>,
|
|
5232
5371
|
/**
|
|
5233
5372
|
* A part-local pixel to the BIND space this mesh's vertices are emitted in —
|
|
5234
|
-
* the same composition `
|
|
5373
|
+
* the same composition `bindWeightedVertices` applies, handed in rather than
|
|
5235
5374
|
* rebuilt so the two cannot diverge.
|
|
5236
5375
|
*
|
|
5237
5376
|
* 🚨 The ceiling has to be taken in bind space and in no other. A deform
|
|
@@ -5391,13 +5530,12 @@ function sampleMeshDepth(
|
|
|
5391
5530
|
};
|
|
5392
5531
|
}
|
|
5393
5532
|
|
|
5394
|
-
/** One bone of the rig as `
|
|
5533
|
+
/** One bone of the rig as `bindWeightedVertices` wants it: by name, with its setup inverse. */
|
|
5395
5534
|
function meshBoneRef(name: string, where: string, ctx: AttachmentContext): MeshBoneRef {
|
|
5396
5535
|
const transform = ctx.transforms.get(name);
|
|
5397
5536
|
if (!transform) throw new CompileError(`${where}: bone "${name}" has no setup transform`);
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
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) };
|
|
5401
5539
|
}
|
|
5402
5540
|
|
|
5403
5541
|
/**
|
|
@@ -5602,7 +5740,7 @@ function buildGridAttachment(
|
|
|
5602
5740
|
placeholder: string,
|
|
5603
5741
|
where: string,
|
|
5604
5742
|
ctx: AttachmentContext,
|
|
5605
|
-
):
|
|
5743
|
+
): ModelMeshAttachment {
|
|
5606
5744
|
if (att.image === undefined) {
|
|
5607
5745
|
throw new CompileError(
|
|
5608
5746
|
`${where}: a "grid" generator lays its lattice over the part's own window, so the attachment needs an ` +
|
|
@@ -5663,8 +5801,9 @@ function buildGridAttachment(
|
|
|
5663
5801
|
}
|
|
5664
5802
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
5665
5803
|
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
5666
|
-
|
|
5667
|
-
|
|
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
|
+
}
|
|
5668
5807
|
// Sampled BEFORE the encode, because `bind` rewrites the weights the encode
|
|
5669
5808
|
// then writes out.
|
|
5670
5809
|
const depth =
|
|
@@ -5691,11 +5830,11 @@ function buildGridAttachment(
|
|
|
5691
5830
|
? undefined
|
|
5692
5831
|
: softRegionWeights(generator.soft, geometry.points, sheetGridOf(img, plate), where, ctx);
|
|
5693
5832
|
if (bound) geometry = { ...geometry, weights: bound.weights };
|
|
5694
|
-
const vertices =
|
|
5833
|
+
const vertices = bindWeightedVertices(
|
|
5695
5834
|
geometry,
|
|
5696
5835
|
(px, py) => [anchor.worldX + px * toArt - w / 2, anchor.worldY + h / 2 - py * toArt],
|
|
5697
5836
|
{
|
|
5698
|
-
anchor: {
|
|
5837
|
+
anchor: { name: ctx.anchorBone, toBind: (wx, wy) => toBoneLocal(anchor, wx, wy) },
|
|
5699
5838
|
controls: bound ? [meshBoneRef(bound.bone, where, ctx)] : [],
|
|
5700
5839
|
},
|
|
5701
5840
|
);
|
|
@@ -5715,12 +5854,12 @@ function buildGridAttachment(
|
|
|
5715
5854
|
? undefined
|
|
5716
5855
|
: { mask: bound.mask, digest: bound.digest, bone: bound.bone, carried: bound.carried, ramped: bound.ramped },
|
|
5717
5856
|
});
|
|
5718
|
-
const out:
|
|
5719
|
-
|
|
5857
|
+
const out: ModelMeshAttachment = {
|
|
5858
|
+
kind: 'mesh',
|
|
5720
5859
|
...meshTextureKeys(att, placeholder),
|
|
5721
5860
|
uvs: geometry.uvs.map(f32),
|
|
5722
5861
|
triangles: geometry.triangles,
|
|
5723
|
-
vertices: vertices
|
|
5862
|
+
vertices: bindingsOnF32(vertices),
|
|
5724
5863
|
...generatedHullAndEdges(geometry, where),
|
|
5725
5864
|
width: f32(w),
|
|
5726
5865
|
height: f32(h),
|
|
@@ -5772,7 +5911,7 @@ function buildSegmentsAttachment(
|
|
|
5772
5911
|
placeholder: string,
|
|
5773
5912
|
where: string,
|
|
5774
5913
|
ctx: AttachmentContext,
|
|
5775
|
-
):
|
|
5914
|
+
): ModelMeshAttachment {
|
|
5776
5915
|
if (att.image === undefined) {
|
|
5777
5916
|
throw new CompileError(
|
|
5778
5917
|
`${where}: a "segments" generator lays its lattice over the part's own alpha, so the attachment needs an ` +
|
|
@@ -5999,7 +6138,7 @@ function buildSegmentsAttachment(
|
|
|
5999
6138
|
weights,
|
|
6000
6139
|
hullVertices: lattice.hullVertices,
|
|
6001
6140
|
};
|
|
6002
|
-
const vertices =
|
|
6141
|
+
const vertices = bindWeightedVertices(geometry, place, {
|
|
6003
6142
|
anchor: meshBoneRef(ctx.anchorBone, where, ctx),
|
|
6004
6143
|
controls: refs,
|
|
6005
6144
|
});
|
|
@@ -6046,11 +6185,11 @@ function buildSegmentsAttachment(
|
|
|
6046
6185
|
},
|
|
6047
6186
|
});
|
|
6048
6187
|
return {
|
|
6049
|
-
|
|
6188
|
+
kind: 'mesh',
|
|
6050
6189
|
...meshTextureKeys(att, placeholder),
|
|
6051
6190
|
uvs: geometry.uvs.map(f32),
|
|
6052
6191
|
triangles: geometry.triangles,
|
|
6053
|
-
vertices: vertices
|
|
6192
|
+
vertices: bindingsOnF32(vertices),
|
|
6054
6193
|
...generatedHullAndEdges(geometry, where),
|
|
6055
6194
|
width: f32(w),
|
|
6056
6195
|
height: f32(h),
|
|
@@ -6088,7 +6227,7 @@ function buildContourAttachment(
|
|
|
6088
6227
|
placeholder: string,
|
|
6089
6228
|
where: string,
|
|
6090
6229
|
ctx: AttachmentContext,
|
|
6091
|
-
):
|
|
6230
|
+
): ModelMeshAttachment {
|
|
6092
6231
|
if (att.image === undefined) {
|
|
6093
6232
|
throw new CompileError(
|
|
6094
6233
|
`${where}: a "contour" generator traces the part's own alpha, so the attachment needs an "image" — ` +
|
|
@@ -6161,12 +6300,13 @@ function buildContourAttachment(
|
|
|
6161
6300
|
}
|
|
6162
6301
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
6163
6302
|
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
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(
|
|
6167
6307
|
geometry,
|
|
6168
6308
|
(px, py) => [anchor.worldX + px * toArt - w / 2, anchor.worldY + h / 2 - py * toArt],
|
|
6169
|
-
{ anchor: {
|
|
6309
|
+
{ anchor: { name: ctx.anchorBone, toBind: (wx, wy) => toBoneLocal(anchor, wx, wy) }, controls: [] },
|
|
6170
6310
|
);
|
|
6171
6311
|
ctx.meshBones.add(ctx.anchorBone);
|
|
6172
6312
|
// The vertices are on the TRACED grid — the plate's — and the depth map is
|
|
@@ -6211,12 +6351,12 @@ function buildContourAttachment(
|
|
|
6211
6351
|
// this comment used to be the rule's only statement, beside four emit sites
|
|
6212
6352
|
// that disagreed with it — and spread right after `type` by
|
|
6213
6353
|
// `meshTextureKeys` since #791.
|
|
6214
|
-
const out:
|
|
6215
|
-
|
|
6354
|
+
const out: ModelMeshAttachment = {
|
|
6355
|
+
kind: 'mesh',
|
|
6216
6356
|
...meshTextureKeys(att, placeholder),
|
|
6217
6357
|
uvs: geometry.uvs.map(f32),
|
|
6218
6358
|
triangles: geometry.triangles,
|
|
6219
|
-
vertices: vertices
|
|
6359
|
+
vertices: bindingsOnF32(vertices),
|
|
6220
6360
|
...generatedHullAndEdges(geometry, where),
|
|
6221
6361
|
width: f32(w),
|
|
6222
6362
|
height: f32(h),
|
|
@@ -6270,7 +6410,7 @@ interface ConstraintContext {
|
|
|
6270
6410
|
* type matches no case is dropped with no error and no `default:` branch, so an
|
|
6271
6411
|
* unimplemented type is refused here by name rather than emitted and lost.
|
|
6272
6412
|
*/
|
|
6273
|
-
function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext):
|
|
6413
|
+
function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): ModelConstraint {
|
|
6274
6414
|
const where = `rig constraint "${spec.name}"`;
|
|
6275
6415
|
const boneNames = ctx.boneNames;
|
|
6276
6416
|
const needBone = (name: unknown, field: string): string => {
|
|
@@ -6303,7 +6443,11 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6303
6443
|
}
|
|
6304
6444
|
return value;
|
|
6305
6445
|
};
|
|
6306
|
-
|
|
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 });
|
|
6307
6451
|
const copy = (fields: readonly string[]) => {
|
|
6308
6452
|
for (const field of fields) {
|
|
6309
6453
|
const v = spec[field];
|
|
@@ -6327,7 +6471,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6327
6471
|
// see `RIG_SCALE_Y_MODES`.
|
|
6328
6472
|
if (spec.scaleY !== undefined) out.scaleY = needEnum(spec.scaleY, 'scaleY', RIG_SCALE_Y_MODES);
|
|
6329
6473
|
copy(['mix', 'softness', 'bendPositive', 'compress', 'stretch', 'skin']);
|
|
6330
|
-
return
|
|
6474
|
+
return made('ik');
|
|
6331
6475
|
}
|
|
6332
6476
|
if (spec.type === 'transform') {
|
|
6333
6477
|
out.bones = boneList();
|
|
@@ -6366,7 +6510,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6366
6510
|
'mixShearY',
|
|
6367
6511
|
'skin',
|
|
6368
6512
|
]);
|
|
6369
|
-
return
|
|
6513
|
+
return made('transform');
|
|
6370
6514
|
}
|
|
6371
6515
|
if (spec.type === 'path') {
|
|
6372
6516
|
out.bones = boneList();
|
|
@@ -6400,7 +6544,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6400
6544
|
if (spec[field] !== undefined) out[field] = f32(needNumber(spec[field], field));
|
|
6401
6545
|
}
|
|
6402
6546
|
copy(['skin']);
|
|
6403
|
-
return
|
|
6547
|
+
return made('path');
|
|
6404
6548
|
}
|
|
6405
6549
|
if (spec.type === 'slider') {
|
|
6406
6550
|
// ⭐ The one field in a rig spec that points at the MOTION spec. It is
|
|
@@ -6792,7 +6936,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6792
6936
|
}
|
|
6793
6937
|
}
|
|
6794
6938
|
copy(['skin']);
|
|
6795
|
-
return
|
|
6939
|
+
return made('slider');
|
|
6796
6940
|
}
|
|
6797
6941
|
if (spec.type === 'physics') {
|
|
6798
6942
|
out.bone = needBone(spec.bone, 'bone');
|
|
@@ -6823,7 +6967,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6823
6967
|
'mixGlobal',
|
|
6824
6968
|
'skin',
|
|
6825
6969
|
]);
|
|
6826
|
-
return
|
|
6970
|
+
return made('physics');
|
|
6827
6971
|
}
|
|
6828
6972
|
// Every type 4.3 has is implemented, so this is now only reachable from a typo
|
|
6829
6973
|
// — and a typo is exactly what the parser drops in silence (no `default:`
|
|
@@ -6846,7 +6990,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
6846
6990
|
*/
|
|
6847
6991
|
function buildRigInfo(
|
|
6848
6992
|
rig: RigSpec,
|
|
6849
|
-
bones:
|
|
6993
|
+
bones: ModelBone[],
|
|
6850
6994
|
meshes: CompileResult['meshes'],
|
|
6851
6995
|
manifest: FaceManifest | null,
|
|
6852
6996
|
images: CompiledImage[],
|
|
@@ -6959,26 +7103,23 @@ function checkAxisSelfConsistency(manifest: FaceManifest): void {
|
|
|
6959
7103
|
* without it every slot hanging off a rotated axis bone would render tilted by
|
|
6960
7104
|
* the cut's axis angle.
|
|
6961
7105
|
*
|
|
6962
|
-
* On an unrotated bone sitting at its window centre both terms are zero and the
|
|
6963
|
-
*
|
|
6964
|
-
* 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.
|
|
6965
7109
|
*/
|
|
6966
7110
|
function placeRegion(
|
|
6967
7111
|
part: FaceManifestPart,
|
|
6968
7112
|
manifest: FaceManifest,
|
|
6969
7113
|
bone: BoneTransform,
|
|
6970
7114
|
img: CompiledImage,
|
|
6971
|
-
):
|
|
7115
|
+
): ModelRegionAttachment {
|
|
6972
7116
|
const win = partWindow(part, manifest);
|
|
6973
7117
|
// width/height are NOT optional: omitting them loads as NaN with no error.
|
|
6974
7118
|
// The compiler fills them from the PNG.
|
|
6975
|
-
const att: SpineRegionAttachment = { width: img.width, height: img.height };
|
|
6976
7119
|
const [ax, ay] = toBoneLocal(bone, win.x + win.w / 2, cropToSpineY(win.y + win.h / 2, manifest.crop.h)).map(f32);
|
|
6977
|
-
if (ax !== 0) att.x = ax;
|
|
6978
|
-
if (ay !== 0) att.y = ay;
|
|
6979
7120
|
const rotation = f32(normaliseDegrees(-bone.worldRotation));
|
|
6980
|
-
|
|
6981
|
-
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) };
|
|
6982
7123
|
}
|
|
6983
7124
|
|
|
6984
7125
|
/**
|
|
@@ -6992,10 +7133,10 @@ function placeRegion(
|
|
|
6992
7133
|
function buildMesh(
|
|
6993
7134
|
part: FaceManifestPart,
|
|
6994
7135
|
manifest: FaceManifest,
|
|
6995
|
-
bones:
|
|
7136
|
+
bones: ModelBone[],
|
|
6996
7137
|
transforms: Map<string, BoneTransform>,
|
|
6997
7138
|
anchorName: string,
|
|
6998
|
-
): { attachment:
|
|
7139
|
+
): { attachment: ModelMeshAttachment; kind: 'ring' | 'ribbon' } {
|
|
6999
7140
|
const spec = part.mesh!;
|
|
7000
7141
|
const kind = spec.kind ?? 'ring';
|
|
7001
7142
|
const win = partWindow(part, manifest);
|
|
@@ -7003,11 +7144,10 @@ function buildMesh(
|
|
|
7003
7144
|
const controls = meshControlBones(part);
|
|
7004
7145
|
|
|
7005
7146
|
const refFor = (name: string): MeshBoneRef => {
|
|
7006
|
-
|
|
7007
|
-
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`);
|
|
7008
7148
|
const m = transforms.get(name);
|
|
7009
7149
|
if (!m) throw new CompileError(`internal: no setup transform for mesh bone "${name}"`);
|
|
7010
|
-
return {
|
|
7150
|
+
return { name, toBind: (wx, wy) => toBoneLocal(m, wx, wy) };
|
|
7011
7151
|
};
|
|
7012
7152
|
|
|
7013
7153
|
let geometry;
|
|
@@ -7047,7 +7187,7 @@ function buildMesh(
|
|
|
7047
7187
|
throw err;
|
|
7048
7188
|
}
|
|
7049
7189
|
|
|
7050
|
-
const vertices =
|
|
7190
|
+
const vertices = bindWeightedVertices(
|
|
7051
7191
|
geometry,
|
|
7052
7192
|
(px, py) => [win.x + px, cropToSpineY(win.y + py, cropH)],
|
|
7053
7193
|
{ anchor: refFor(anchorName), controls: controls.map(refFor) },
|
|
@@ -7056,10 +7196,10 @@ function buildMesh(
|
|
|
7056
7196
|
return {
|
|
7057
7197
|
kind: geometry.kind,
|
|
7058
7198
|
attachment: {
|
|
7059
|
-
|
|
7199
|
+
kind: 'mesh',
|
|
7060
7200
|
uvs: geometry.uvs.map(f32),
|
|
7061
7201
|
triangles: geometry.triangles,
|
|
7062
|
-
vertices: vertices
|
|
7202
|
+
vertices: bindingsOnF32(vertices),
|
|
7063
7203
|
...generatedHullAndEdges(geometry, `slot "${part.slot}" mesh`),
|
|
7064
7204
|
width: win.w,
|
|
7065
7205
|
height: win.h,
|
|
@@ -7123,13 +7263,13 @@ function compileValueTrack(
|
|
|
7123
7263
|
shift: number,
|
|
7124
7264
|
shapes: Record<string, ValueTrackShape>,
|
|
7125
7265
|
kind: string,
|
|
7126
|
-
):
|
|
7266
|
+
): ModelKey[] {
|
|
7127
7267
|
const shape = shapes[track.property];
|
|
7128
7268
|
if (!shape) throw new CompileError(`animation "${animName}": ${kind} "${target}" has no property "${track.property}"`);
|
|
7129
7269
|
const where = `animation "${animName}" ${kind} "${target}" ${track.property}`;
|
|
7130
7270
|
if (!track.keys.length) throw new CompileError(`${where}: no keys`);
|
|
7131
7271
|
|
|
7132
|
-
const out:
|
|
7272
|
+
const out: ModelKey[] = [];
|
|
7133
7273
|
for (let i = 0; i < track.keys.length; i++) {
|
|
7134
7274
|
const key = track.keys[i];
|
|
7135
7275
|
const next = track.keys[i + 1];
|
|
@@ -7169,7 +7309,7 @@ function compileValueTrack(
|
|
|
7169
7309
|
if (!Array.isArray(key.v) || key.v.length !== shape.fields.length) {
|
|
7170
7310
|
throw new CompileError(`${where}: key value must be an array of ${shape.fields.length} number(s)`);
|
|
7171
7311
|
}
|
|
7172
|
-
const entry:
|
|
7312
|
+
const entry: ModelKey = { time };
|
|
7173
7313
|
shape.fields.forEach((field, c) => {
|
|
7174
7314
|
const v = key.v as number[];
|
|
7175
7315
|
if (!Number.isFinite(v[c])) throw new CompileError(`${where}: non-finite value ${String(v[c])}`);
|
|
@@ -7234,7 +7374,8 @@ function compileValueTrack(
|
|
|
7234
7374
|
* than loading wrong. The author states a set of moves; the array order in
|
|
7235
7375
|
* the file is the parser's requirement and not a decision, so rigc sorts
|
|
7236
7376
|
* rather than making every caller remember. Deterministic: the key is the
|
|
7237
|
-
* emitted slot index.
|
|
7377
|
+
* emitted slot index. The model holds the moves as stated and the Spine
|
|
7378
|
+
* emitter sorts them (`emitDrawOrderKey`, issue #921).
|
|
7238
7379
|
* 2. **One slot per key.** Two entries for the same slot means two writes at
|
|
7239
7380
|
* one cursor position; the second silently wins and the first slot's place
|
|
7240
7381
|
* is left to the unchanged-fill.
|
|
@@ -7254,13 +7395,13 @@ function compileDrawOrder(
|
|
|
7254
7395
|
keys: MotionDrawOrderKey[],
|
|
7255
7396
|
animName: string,
|
|
7256
7397
|
duration: number,
|
|
7257
|
-
slots:
|
|
7258
|
-
):
|
|
7398
|
+
slots: readonly ModelSlot[],
|
|
7399
|
+
): ModelKey[] {
|
|
7259
7400
|
const where = `animation "${animName}" drawOrder`;
|
|
7260
7401
|
if (!keys.length) throw new CompileError(`${where}: no keys`);
|
|
7261
7402
|
const indexOf = new Map(slots.map((s, i) => [s.name, i]));
|
|
7262
7403
|
|
|
7263
|
-
const out:
|
|
7404
|
+
const out: ModelKey[] = [];
|
|
7264
7405
|
for (let i = 0; i < keys.length; i++) {
|
|
7265
7406
|
const key = keys[i];
|
|
7266
7407
|
const time = keyTime(key.t);
|
|
@@ -7276,7 +7417,7 @@ function compileDrawOrder(
|
|
|
7276
7417
|
continue;
|
|
7277
7418
|
}
|
|
7278
7419
|
const seen = new Set<string>();
|
|
7279
|
-
const entries: Array<{ slot: string; offset: number
|
|
7420
|
+
const entries: Array<{ slot: string; offset: number }> = [];
|
|
7280
7421
|
for (const off of key.offsets) {
|
|
7281
7422
|
const index = indexOf.get(off.slot);
|
|
7282
7423
|
if (index === undefined) {
|
|
@@ -7296,10 +7437,11 @@ function compileDrawOrder(
|
|
|
7296
7437
|
);
|
|
7297
7438
|
}
|
|
7298
7439
|
seen.add(off.slot);
|
|
7299
|
-
entries.push({ slot: off.slot, offset: off.offset
|
|
7440
|
+
entries.push({ slot: off.slot, offset: off.offset });
|
|
7300
7441
|
}
|
|
7301
|
-
|
|
7302
|
-
|
|
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 });
|
|
7303
7445
|
}
|
|
7304
7446
|
return out;
|
|
7305
7447
|
}
|
|
@@ -7334,11 +7476,11 @@ function compileEvents(
|
|
|
7334
7476
|
animName: string,
|
|
7335
7477
|
duration: number,
|
|
7336
7478
|
events: Record<string, RigEvent>,
|
|
7337
|
-
):
|
|
7479
|
+
): ModelKey[] {
|
|
7338
7480
|
const where = `animation "${animName}" events`;
|
|
7339
7481
|
if (!keys.length) throw new CompileError(`${where}: no keys`);
|
|
7340
7482
|
|
|
7341
|
-
const out:
|
|
7483
|
+
const out: ModelKey[] = [];
|
|
7342
7484
|
for (let i = 0; i < keys.length; i++) {
|
|
7343
7485
|
const key = keys[i];
|
|
7344
7486
|
if (typeof key.name !== 'string' || key.name.length === 0) {
|
|
@@ -7361,7 +7503,7 @@ function compileEvents(
|
|
|
7361
7503
|
}
|
|
7362
7504
|
checkKeyTime(where, time, key.t, duration);
|
|
7363
7505
|
|
|
7364
|
-
const entry:
|
|
7506
|
+
const entry: ModelKey = { time, name: key.name };
|
|
7365
7507
|
if (key.int !== undefined) {
|
|
7366
7508
|
if (!Number.isInteger(key.int)) {
|
|
7367
7509
|
throw new CompileError(`${where} at t=${key.t}: int ${String(key.int)} is not an integer`);
|
|
@@ -7448,9 +7590,9 @@ function compileConstraintTrack(
|
|
|
7448
7590
|
motion: MotionSpec,
|
|
7449
7591
|
animName: string,
|
|
7450
7592
|
duration: number,
|
|
7451
|
-
/**
|
|
7593
|
+
/** The ik constraint's three booleans in effect, by field. Empty for `transform`. */
|
|
7452
7594
|
rigFlags: Record<string, boolean>,
|
|
7453
|
-
):
|
|
7595
|
+
): ModelKey[] {
|
|
7454
7596
|
const shape = CONSTRAINT_TIMELINES[group];
|
|
7455
7597
|
const article = group === 'ik' ? 'an' : 'a';
|
|
7456
7598
|
const where = `animation "${animName}" ${group} constraint "${track.constraint}"`;
|
|
@@ -7499,7 +7641,7 @@ function compileConstraintTrack(
|
|
|
7499
7641
|
return inherited === undefined ? channel.dflt : (inherited as number);
|
|
7500
7642
|
};
|
|
7501
7643
|
|
|
7502
|
-
const out:
|
|
7644
|
+
const out: ModelKey[] = [];
|
|
7503
7645
|
for (let i = 0; i < keys.length; i++) {
|
|
7504
7646
|
const key = keys[i];
|
|
7505
7647
|
const next = keys[i + 1];
|
|
@@ -7509,7 +7651,7 @@ function compileConstraintTrack(
|
|
|
7509
7651
|
}
|
|
7510
7652
|
checkKeyTime(where, time, key.t, duration);
|
|
7511
7653
|
|
|
7512
|
-
const entry:
|
|
7654
|
+
const entry: ModelKey = { time };
|
|
7513
7655
|
for (const channel of shape.channels) {
|
|
7514
7656
|
const v = read(key, channel.field);
|
|
7515
7657
|
if (v === undefined) continue;
|
|
@@ -7529,10 +7671,10 @@ function compileConstraintTrack(
|
|
|
7529
7671
|
for (const flag of shape.flags) {
|
|
7530
7672
|
const v = read(key, flag.field);
|
|
7531
7673
|
if (v === undefined) {
|
|
7532
|
-
// The
|
|
7533
|
-
//
|
|
7534
|
-
//
|
|
7535
|
-
//
|
|
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).
|
|
7536
7678
|
const carried = rigFlags[flag.field];
|
|
7537
7679
|
if (carried !== undefined) entry[flag.field] = carried;
|
|
7538
7680
|
continue;
|
|
@@ -7594,7 +7736,7 @@ function defaultOf(shape: ConstraintTimelineShape, field: string): number | bool
|
|
|
7594
7736
|
* Same shape, two meanings, and picking the wrong one writes a run that silently
|
|
7595
7737
|
* lands on the wrong vertices.
|
|
7596
7738
|
*/
|
|
7597
|
-
interface DeformGeometry {
|
|
7739
|
+
export interface DeformGeometry {
|
|
7598
7740
|
weighted: boolean;
|
|
7599
7741
|
/** How long the array the key edits is. */
|
|
7600
7742
|
deformLength: number;
|
|
@@ -7641,7 +7783,7 @@ interface DeformGeometry {
|
|
|
7641
7783
|
/**
|
|
7642
7784
|
* Per-vertex `z`, when this attachment's generator named a depth map.
|
|
7643
7785
|
*
|
|
7644
|
-
* Not measured from the
|
|
7786
|
+
* Not measured from the attachment like everything else here — there
|
|
7645
7787
|
* is nowhere in the format to measure it FROM, which is the whole reason the
|
|
7646
7788
|
* sampler keeps it beside the skeleton instead of in it. Filled in by the
|
|
7647
7789
|
* caller, which is where the skin, slot and attachment names live.
|
|
@@ -7650,25 +7792,35 @@ interface DeformGeometry {
|
|
|
7650
7792
|
}
|
|
7651
7793
|
|
|
7652
7794
|
/**
|
|
7653
|
-
* 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.
|
|
7654
7798
|
*
|
|
7655
7799
|
* A region attachment is refused rather than measured: it has no `vertices` at
|
|
7656
7800
|
* all, so `attachment.vertices.length` throws inside the parser — one of the very
|
|
7657
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.
|
|
7658
7806
|
*/
|
|
7659
|
-
function deformGeometryOf(
|
|
7660
|
-
att:
|
|
7807
|
+
export function deformGeometryOf(
|
|
7808
|
+
att: SkinTableEntry,
|
|
7661
7809
|
where: string,
|
|
7662
|
-
/**
|
|
7663
|
-
rigBones: SpineBone[],
|
|
7664
|
-
/** Their setup world transforms, by name. */
|
|
7810
|
+
/** Every bone's setup world transform, by name. */
|
|
7665
7811
|
transforms: Map<string, BoneTransform>,
|
|
7666
7812
|
): DeformGeometry {
|
|
7667
|
-
const type =
|
|
7813
|
+
const type = att.kind;
|
|
7668
7814
|
let worldVerticesLength: number;
|
|
7669
|
-
if (
|
|
7670
|
-
|
|
7671
|
-
|
|
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 {
|
|
7672
7824
|
// ⭐ A `path` reaches this line as of issue #696, and it is the SAME line the
|
|
7673
7825
|
// other two vertex-and-no-triangles types take: the array the parser sizes
|
|
7674
7826
|
// is `vertexCount * 2`, the two encodings below are the mesh's own, and a
|
|
@@ -7697,16 +7849,11 @@ function deformGeometryOf(
|
|
|
7697
7849
|
// file twice. `docs/AUTHORING.md` §4.11 states the `constantSpeed: false`
|
|
7698
7850
|
// reading instead, which is the honest home for it: a fact about the format
|
|
7699
7851
|
// the author is choosing, not a fault rigc can measure.
|
|
7700
|
-
worldVerticesLength =
|
|
7701
|
-
} else {
|
|
7702
|
-
throw new CompileError(
|
|
7703
|
-
`${where}: a deform timeline keys the vertices of an attachment, and this one is a "${type}" — ` +
|
|
7704
|
-
'it has no vertex array to deform. Deformable types: mesh, boundingbox, clipping, path.',
|
|
7705
|
-
);
|
|
7852
|
+
worldVerticesLength = att.vertexCount * 2;
|
|
7706
7853
|
}
|
|
7707
|
-
const vertices =
|
|
7708
|
-
const weighted = vertices.
|
|
7709
|
-
if (!weighted) {
|
|
7854
|
+
const vertices = att.vertices;
|
|
7855
|
+
const weighted = vertices.weighted;
|
|
7856
|
+
if (!vertices.weighted) {
|
|
7710
7857
|
return {
|
|
7711
7858
|
weighted,
|
|
7712
7859
|
deformLength: worldVerticesLength,
|
|
@@ -7714,41 +7861,37 @@ function deformGeometryOf(
|
|
|
7714
7861
|
boneCounts: null,
|
|
7715
7862
|
// An unweighted attachment IS its own space: the array is one `x, y` per
|
|
7716
7863
|
// vertex in the slot bone's space, which is the space the offsets are in.
|
|
7717
|
-
setup: vertices.slice(),
|
|
7864
|
+
setup: vertices.xy.slice(),
|
|
7718
7865
|
influenceBones: null,
|
|
7719
7866
|
setupWhy: null,
|
|
7720
|
-
// Filled in by the caller; `deformGeometryOf` reads the
|
|
7721
|
-
//
|
|
7867
|
+
// Filled in by the caller; `deformGeometryOf` reads the attachment's
|
|
7868
|
+
// record, and the depth is deliberately not in it.
|
|
7722
7869
|
depth: null,
|
|
7723
7870
|
};
|
|
7724
7871
|
}
|
|
7725
|
-
// Walk the
|
|
7726
|
-
//
|
|
7727
|
-
//
|
|
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.
|
|
7728
7877
|
const boneCounts: number[] = [];
|
|
7729
7878
|
const bindSpace: number[] = [];
|
|
7730
|
-
const bones = new Set<
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
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}`);
|
|
7737
7885
|
}
|
|
7738
7886
|
if (n === 1) {
|
|
7739
|
-
bones.add(
|
|
7740
|
-
bindSpace.push(
|
|
7741
|
-
}
|
|
7742
|
-
const from = i;
|
|
7743
|
-
i += n * 4;
|
|
7744
|
-
if (i > vertices.length) {
|
|
7745
|
-
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);
|
|
7746
7889
|
}
|
|
7747
|
-
for (let k = from; k < i; k++) influenceRun.push(vertices[k]);
|
|
7748
7890
|
boneCounts.push(n);
|
|
7891
|
+
at += 1 + n * 4;
|
|
7749
7892
|
}
|
|
7750
7893
|
// ⚠️ The influence count is the SUM of the per-vertex counts, not a division
|
|
7751
|
-
// 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
|
|
7752
7895
|
// `boneIndex, x, y, weight` per influence — five numbers for a single-bone
|
|
7753
7896
|
// vertex, not three — so `vertices.length / 3` overstated the deform array by
|
|
7754
7897
|
// two thirds on a one-bone-per-vertex mesh (`gallery/flex`'s 77-vertex leaf
|
|
@@ -7786,17 +7929,15 @@ function deformGeometryOf(
|
|
|
7786
7929
|
const setupWorld: number[] = [];
|
|
7787
7930
|
const influenceBones: BoneTransform[] = [];
|
|
7788
7931
|
let openWeights: string | null = null;
|
|
7789
|
-
for (let v = 0
|
|
7932
|
+
for (let v = 0; v < boneCounts.length; v++) {
|
|
7790
7933
|
let wx = 0;
|
|
7791
7934
|
let wy = 0;
|
|
7792
7935
|
let sum = 0;
|
|
7793
|
-
for (
|
|
7794
|
-
const
|
|
7795
|
-
|
|
7796
|
-
const
|
|
7797
|
-
|
|
7798
|
-
const weight = influenceRun[4 * k + 3];
|
|
7799
|
-
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);
|
|
7800
7941
|
wx += x * weight;
|
|
7801
7942
|
wy += y * weight;
|
|
7802
7943
|
sum += weight;
|
|
@@ -7891,11 +8032,11 @@ function deformGeometryOf(
|
|
|
7891
8032
|
function compileSequenceTrack(
|
|
7892
8033
|
track: MotionSequenceTrack,
|
|
7893
8034
|
duration: number,
|
|
7894
|
-
attachment:
|
|
8035
|
+
attachment: SkinTableEntry,
|
|
7895
8036
|
where: string,
|
|
7896
|
-
):
|
|
7897
|
-
const
|
|
7898
|
-
|
|
8037
|
+
): ModelKey[] {
|
|
8038
|
+
const series =
|
|
8039
|
+
attachment.kind === 'region' || attachment.kind === 'mesh' || attachment.kind === 'linkedmesh' ? attachment.sequence : undefined;
|
|
7899
8040
|
if (series === undefined) {
|
|
7900
8041
|
throw new CompileError(
|
|
7901
8042
|
`${where}: attachment "${track.attachment}" carries no "sequence" block, so there is no series to step. The ` +
|
|
@@ -7904,8 +8045,8 @@ function compileSequenceTrack(
|
|
|
7904
8045
|
'attachment a "sequence" in the rig spec, or remove the track.',
|
|
7905
8046
|
);
|
|
7906
8047
|
}
|
|
7907
|
-
if (
|
|
7908
|
-
const source =
|
|
8048
|
+
if (attachment.kind === 'linkedmesh' && attachment.timelines) {
|
|
8049
|
+
const source = attachment.source;
|
|
7909
8050
|
throw new CompileError(
|
|
7910
8051
|
`${where}: attachment "${track.attachment}" is a linked mesh that plays its source's timelines ("timelines" ` +
|
|
7911
8052
|
`is not false), so its \`timelineAttachment\` is "${source}" (\`SkeletonJson.js:437-448\`) and ` +
|
|
@@ -7915,7 +8056,7 @@ function compileSequenceTrack(
|
|
|
7915
8056
|
);
|
|
7916
8057
|
}
|
|
7917
8058
|
if (track.keys.length === 0) throw new CompileError(`${where}: no keys`);
|
|
7918
|
-
const out:
|
|
8059
|
+
const out: ModelKey[] = [];
|
|
7919
8060
|
for (let i = 0; i < track.keys.length; i++) {
|
|
7920
8061
|
const key = track.keys[i];
|
|
7921
8062
|
const time = keyTime(key.t);
|
|
@@ -7930,7 +8071,7 @@ function compileSequenceTrack(
|
|
|
7930
8071
|
'frame it does not name. `index` is 0-based.',
|
|
7931
8072
|
);
|
|
7932
8073
|
}
|
|
7933
|
-
const entry:
|
|
8074
|
+
const entry: ModelKey = { time };
|
|
7934
8075
|
if (key.mode !== undefined) entry.mode = key.mode;
|
|
7935
8076
|
if (key.index !== undefined) entry.index = key.index;
|
|
7936
8077
|
if (key.delay !== undefined) entry.delay = f32(key.delay);
|
|
@@ -7946,7 +8087,7 @@ function compileDeformTrack(
|
|
|
7946
8087
|
duration: number,
|
|
7947
8088
|
geometry: DeformGeometry,
|
|
7948
8089
|
generated: CompileResult['deformTransforms'],
|
|
7949
|
-
):
|
|
8090
|
+
): ModelKey[] {
|
|
7950
8091
|
const skin = track.skin ?? 'default';
|
|
7951
8092
|
const where = `animation "${animName}" deform ${skin}/${track.slot}/${track.attachment}`;
|
|
7952
8093
|
const keys = track.keys;
|
|
@@ -7955,7 +8096,7 @@ function compileDeformTrack(
|
|
|
7955
8096
|
// skips the timeline, which is assertion A34's silent case.
|
|
7956
8097
|
if (keys.length === 0) throw new CompileError(`${where}: no keys`);
|
|
7957
8098
|
|
|
7958
|
-
const out:
|
|
8099
|
+
const out: ModelKey[] = [];
|
|
7959
8100
|
for (let i = 0; i < keys.length; i++) {
|
|
7960
8101
|
const key = keys[i];
|
|
7961
8102
|
const time = keyTime(key.t);
|
|
@@ -8114,7 +8255,7 @@ function compileDeformTrack(
|
|
|
8114
8255
|
'The parser copies into a Float32Array, so everything past the end is dropped without a word.',
|
|
8115
8256
|
);
|
|
8116
8257
|
}
|
|
8117
|
-
const entry:
|
|
8258
|
+
const entry: ModelKey = { time };
|
|
8118
8259
|
// `offset` defaults to 0 in the parser and the editor omits it there, so an
|
|
8119
8260
|
// authored 0, an authored `fromVertex: 0` and an absent start all emit the
|
|
8120
8261
|
// same bytes — one meaning, one file.
|
|
@@ -8242,15 +8383,15 @@ function deformStart(
|
|
|
8242
8383
|
* than to a coordinate, and a raw curve is four numbers whose value axis is 0..1.
|
|
8243
8384
|
*/
|
|
8244
8385
|
function deformKeyCurve(
|
|
8245
|
-
entry:
|
|
8386
|
+
entry: ModelKey,
|
|
8246
8387
|
key: MotionDeformTrack['keys'][number],
|
|
8247
8388
|
keys: MotionDeformTrack['keys'],
|
|
8248
8389
|
index: number,
|
|
8249
8390
|
motion: MotionSpec,
|
|
8250
8391
|
where: string,
|
|
8251
8392
|
/** The next key AS EMITTED, or undefined on the last key. */
|
|
8252
|
-
next:
|
|
8253
|
-
):
|
|
8393
|
+
next: ModelKey | undefined,
|
|
8394
|
+
): ModelKey {
|
|
8254
8395
|
if (key.ease !== undefined && key.curve !== undefined) {
|
|
8255
8396
|
throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
|
|
8256
8397
|
}
|
|
@@ -8282,8 +8423,8 @@ function deformKeyCurve(
|
|
|
8282
8423
|
* are three spellings of one geometry, and the comparison expands both keys onto
|
|
8283
8424
|
* the same index range before reading them. The runs are already `f32`'d.
|
|
8284
8425
|
*/
|
|
8285
|
-
function sameDeform(a:
|
|
8286
|
-
const runOf = (k:
|
|
8426
|
+
function sameDeform(a: ModelKey, b: ModelKey): boolean {
|
|
8427
|
+
const runOf = (k: ModelKey): { start: number; run: number[] } => ({
|
|
8287
8428
|
start: typeof k.offset === 'number' ? k.offset : 0,
|
|
8288
8429
|
run: Array.isArray(k.vertices) ? (k.vertices as number[]) : [],
|
|
8289
8430
|
});
|
|
@@ -8462,7 +8603,7 @@ function resolveMemberTrack(
|
|
|
8462
8603
|
track: MotionTrack,
|
|
8463
8604
|
animName: string,
|
|
8464
8605
|
targets: readonly string[],
|
|
8465
|
-
bones: readonly
|
|
8606
|
+
bones: readonly ModelBone[],
|
|
8466
8607
|
derivations: CompileResult['trackDerivations'],
|
|
8467
8608
|
): Map<string, MotionValueTrack> {
|
|
8468
8609
|
const carriesMap = track.keys.some((key) => isMemberValues(key.v));
|
|
@@ -8622,7 +8763,7 @@ function compileTrack(
|
|
|
8622
8763
|
shift: number,
|
|
8623
8764
|
attachments: SlotAttachmentIndex,
|
|
8624
8765
|
darkSlots: ReadonlySet<string>,
|
|
8625
|
-
):
|
|
8766
|
+
): ModelKey[] {
|
|
8626
8767
|
const where = `animation "${animName}" slot "${target}" ${track.property}`;
|
|
8627
8768
|
// Before any key is shaped, and before the empty-track refusal: a property
|
|
8628
8769
|
// the emitter has no branch for is the fault, and a track that names one has
|
|
@@ -8661,7 +8802,7 @@ function compileTrack(
|
|
|
8661
8802
|
}
|
|
8662
8803
|
if (!track.keys.length) throw new CompileError(`${where}: no keys`);
|
|
8663
8804
|
|
|
8664
|
-
const out:
|
|
8805
|
+
const out: ModelKey[] = [];
|
|
8665
8806
|
for (let i = 0; i < track.keys.length; i++) {
|
|
8666
8807
|
const key = track.keys[i];
|
|
8667
8808
|
const next = track.keys[i + 1];
|
|
@@ -8724,7 +8865,7 @@ function compileTrack(
|
|
|
8724
8865
|
'the posed alpha to that range after interpolating, so a value outside it is one no pose ever holds',
|
|
8725
8866
|
);
|
|
8726
8867
|
}
|
|
8727
|
-
const entry:
|
|
8868
|
+
const entry: ModelKey = { time, ...colourOf(key.v) };
|
|
8728
8869
|
if (key.ease !== undefined && key.curve !== undefined) {
|
|
8729
8870
|
throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
|
|
8730
8871
|
}
|