spine-rigc 0.25.6 → 0.26.0
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 +5 -5
- package/cli.ts +25 -9
- package/docs/AUTHORING.md +163 -33
- package/docs/INGEST.md +3 -2
- package/docs/SPEC_COVERAGE.md +8 -6
- package/package.json +1 -1
- package/src/compile.ts +405 -76
- package/src/diff.ts +14 -1
- package/src/ingest.ts +126 -14
- package/src/render.ts +69 -7
- package/src/rig.ts +159 -31
- package/src/types.ts +25 -0
- package/src/validate.ts +407 -46
package/src/compile.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { parseJsonWithPosition } from './json-position.ts';
|
|
|
41
41
|
import { nearMisses } from './keys.ts';
|
|
42
42
|
import { parseMotionSpec } from './motion.ts';
|
|
43
43
|
import {
|
|
44
|
+
constraintAt,
|
|
44
45
|
declaresNoStage,
|
|
45
46
|
parseRigSpec,
|
|
46
47
|
RIG_FROM_PROPERTIES,
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
type RigBoundingBoxAttachment,
|
|
56
57
|
type RigClippingAttachment,
|
|
57
58
|
type RigEvent,
|
|
59
|
+
type RigLinkedMeshAttachment,
|
|
58
60
|
type RigMeshAttachment,
|
|
59
61
|
type RigMeshBinding,
|
|
60
62
|
type RigPathAttachment,
|
|
@@ -131,6 +133,7 @@ import type {
|
|
|
131
133
|
SpineClippingAttachment,
|
|
132
134
|
SpineConstraint,
|
|
133
135
|
SpineEvent,
|
|
136
|
+
SpineLinkedMeshAttachment,
|
|
134
137
|
SpineMeshAttachment,
|
|
135
138
|
SpinePathAttachment,
|
|
136
139
|
SpineRegionAttachment,
|
|
@@ -735,6 +738,26 @@ function rgbaHex(v: number[]): string {
|
|
|
735
738
|
return v.map(channelHex).join('');
|
|
736
739
|
}
|
|
737
740
|
|
|
741
|
+
/**
|
|
742
|
+
* A two-colour key's seven channels, as the pair of hex strings the format
|
|
743
|
+
* carries: `light` is `rrggbbaa`, `dark` is `rrggbb`.
|
|
744
|
+
*
|
|
745
|
+
* ⚠️ **Seven and not eight**, and the asymmetry is the format's rather than a
|
|
746
|
+
* simplification here. `RGBA2Timeline.setFrame(frame, time, r, g, b, a, r2, g2,
|
|
747
|
+
* b2)` stores three dark channels and no fourth, and `SkeletonJson`'s `rgba2`
|
|
748
|
+
* branch reads `Color.fromString(keyMap.dark)` into a colour whose alpha it then
|
|
749
|
+
* never passes on. `Color.setFromString` does fill one — `a = hex.length !== 8 ?
|
|
750
|
+
* 1 : …` — so a `dark` written with eight digits loads without complaint and the
|
|
751
|
+
* eighth pair is dropped one line later. Emitting six is emitting what is read.
|
|
752
|
+
*
|
|
753
|
+
* The channel ORDER is the format's too, and it is what a curve array indexes
|
|
754
|
+
* by: `readCurve(…, 0..3, light r/g/b/a)` then `readCurve(…, 4..6, dark r/g/b)`.
|
|
755
|
+
*/
|
|
756
|
+
function rgba2Hex(v: number[]): { light: string; dark: string } {
|
|
757
|
+
if (v.length !== 7) throw new CompileError(`rgba2 value needs 7 channels, got ${v.length}`);
|
|
758
|
+
return { light: v.slice(0, 4).map(channelHex).join(''), dark: v.slice(4, 7).map(channelHex).join('') };
|
|
759
|
+
}
|
|
760
|
+
|
|
738
761
|
/**
|
|
739
762
|
* One timeline a `MotionValueTrack` can name: the JSON fields a key carries, the
|
|
740
763
|
* per-key default the parser uses for each, and — where the runtime has one — the
|
|
@@ -893,12 +916,21 @@ const SLIDER_TRACKS: Record<string, ValueTrackShape> = {
|
|
|
893
916
|
* the branch below that writes its keys — so an entry added here without a
|
|
894
917
|
* branch to write it is an entry emitted in some other timeline's shape, which
|
|
895
918
|
* is the defect this table closed rather than a new affordance. The format has
|
|
896
|
-
*
|
|
897
|
-
*
|
|
919
|
+
* three more (`rgb`, `alpha`, `rgb2`) and rigc emits none of them.
|
|
920
|
+
*
|
|
921
|
+
* 🎨 `rgba2` joined in issue #690, and what it adds is the half of the two-colour
|
|
922
|
+
* tint that moves: a slot's `dark` has been an emitted setup field all along
|
|
923
|
+
* (`RigSlot.dark`, step 4 below), with no way to key it. 🚫 Both halves are still
|
|
924
|
+
* refused by `A12_NO_DARK_COLOR`, and that is not a contradiction — A12 is filed
|
|
925
|
+
* `renderer` in `ASSERTION_KIND`, so it states what ONE renderer ignores rather
|
|
926
|
+
* than what is wrong. The `spine` profile `build` runs reports it `PROF` and
|
|
927
|
+
* never applies it; `--profile spine-html` is where it fires. Emitting a
|
|
928
|
+
* construct one consumer drops is exactly what a profile is for.
|
|
898
929
|
*/
|
|
899
|
-
export const SLOT_TRACKS: Record<string, 'attachment' | 'rgba'> = {
|
|
930
|
+
export const SLOT_TRACKS: Record<string, 'attachment' | 'rgba' | 'rgba2'> = {
|
|
900
931
|
attachment: 'attachment',
|
|
901
932
|
rgba: 'rgba',
|
|
933
|
+
rgba2: 'rgba2',
|
|
902
934
|
};
|
|
903
935
|
|
|
904
936
|
/**
|
|
@@ -1988,6 +2020,8 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
1988
2020
|
}
|
|
1989
2021
|
return table;
|
|
1990
2022
|
};
|
|
2023
|
+
/** Linked meshes to resolve once every skin exists — see `resolveLinkedMeshes`. */
|
|
2024
|
+
const pendingLinks: PendingLink[] = [];
|
|
1991
2025
|
tableFor('default'); // rigc always emits a default skin, even when it is empty
|
|
1992
2026
|
// ...and every skin the rig declares, for the same reason: a skin can now carry
|
|
1993
2027
|
// `bones`/constraint lists with no attachments at all, and a skin that only
|
|
@@ -2158,6 +2192,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2158
2192
|
imagesDir,
|
|
2159
2193
|
depths: attachmentDepths,
|
|
2160
2194
|
slotNames: new Set(rig.slots.map((s) => s.name)),
|
|
2195
|
+
links: pendingLinks,
|
|
2161
2196
|
});
|
|
2162
2197
|
// The name is put on AFTER the builder rather than inside it: five
|
|
2163
2198
|
// builders write five shapes, the rule is one rule, and a rule that has
|
|
@@ -2172,6 +2207,10 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2172
2207
|
tableFor(skinName)[rigSlot.name] = perSlot;
|
|
2173
2208
|
}
|
|
2174
2209
|
}
|
|
2210
|
+
// Every skin is built, so every `source` can now be looked up — and until this
|
|
2211
|
+
// line a linked mesh is the one construct in a rig spec whose name has not been
|
|
2212
|
+
// resolved yet.
|
|
2213
|
+
resolveLinkedMeshes(pendingLinks, skinTables);
|
|
2175
2214
|
// 📐 The implicit budget of 0 is a statement about rigc's own GENERATORS:
|
|
2176
2215
|
// geometry rigc built is geometry rigc will not ship unmeasured, and
|
|
2177
2216
|
// `A13_MESH_BUDGET` has nothing to measure a generated mesh against until the
|
|
@@ -2215,12 +2254,22 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2215
2254
|
const deformTransforms: CompileResult['deformTransforms'] = [];
|
|
2216
2255
|
/** Group-track keys whose per-member values were stated or derived (issue #295). */
|
|
2217
2256
|
const trackDerivations: CompileResult['trackDerivations'] = [];
|
|
2218
|
-
const constraintNames = new Set<string>();
|
|
2219
2257
|
// `ik` and `transform` timelines resolve their target by name AND by type —
|
|
2220
2258
|
// `findConstraint(name, IkConstraintData)` returns null for a transform
|
|
2221
|
-
// constraint of the same name and the parser then throws.
|
|
2222
|
-
//
|
|
2223
|
-
|
|
2259
|
+
// constraint of the same name and the parser then throws. So the table this
|
|
2260
|
+
// compiler resolves against is keyed by BOTH (issue #692): a name alone is not
|
|
2261
|
+
// a constraint in this format, and a rig may declare `leg` once per kind.
|
|
2262
|
+
/** `<kind> constraint "<name>"` -> declared. The key is the format's namespace. */
|
|
2263
|
+
const constraintDeclared = new Set<string>();
|
|
2264
|
+
/** name -> the kinds that declare it, for the refusal that has to say which. */
|
|
2265
|
+
const constraintKinds = new Map<string, string[]>();
|
|
2266
|
+
/** kind -> the names it declares, for the "the rig declares: …" half of one. */
|
|
2267
|
+
const constraintNamesOfKind = new Map<string, string[]>();
|
|
2268
|
+
const declareConstraint = (name: string, type: string): void => {
|
|
2269
|
+
constraintDeclared.add(constraintAt(type, name));
|
|
2270
|
+
constraintKinds.set(name, [...(constraintKinds.get(name) ?? []), type]);
|
|
2271
|
+
constraintNamesOfKind.set(type, [...(constraintNamesOfKind.get(type) ?? []), name]);
|
|
2272
|
+
};
|
|
2224
2273
|
/**
|
|
2225
2274
|
* ik constraint -> the booleans it declares that the timeline format would
|
|
2226
2275
|
* otherwise take away from it. Issue #273.
|
|
@@ -2262,8 +2311,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2262
2311
|
// wrote in it and the union above is a claim about a correct one.
|
|
2263
2312
|
for (const spec of (rig.constraints ?? []) as unknown as RigConstraintInput[]) {
|
|
2264
2313
|
constraints.push(buildRigConstraint(spec, constraintCtx));
|
|
2265
|
-
|
|
2266
|
-
constraintTypes.set(spec.name, spec.type);
|
|
2314
|
+
declareConstraint(spec.name, spec.type);
|
|
2267
2315
|
if (spec.type === 'ik') {
|
|
2268
2316
|
const carried: Record<string, boolean> = {};
|
|
2269
2317
|
for (const flag of CONSTRAINT_TIMELINES.ik.flags) {
|
|
@@ -2275,11 +2323,16 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2275
2323
|
}
|
|
2276
2324
|
withMotionSource(() => {
|
|
2277
2325
|
for (const [name, spec] of Object.entries(motion.physics ?? {})) {
|
|
2278
|
-
|
|
2279
|
-
|
|
2326
|
+
// Per kind, like everything else about a constraint name: a rig-declared
|
|
2327
|
+
// `ik` and a tuned `physics` constraint of one name are two objects, and
|
|
2328
|
+
// two PHYSICS constraints of one name are the pair no timeline can tell
|
|
2329
|
+
// apart.
|
|
2330
|
+
if (constraintDeclared.has(constraintAt('physics', name))) {
|
|
2331
|
+
throw new CompileError(
|
|
2332
|
+
`physics constraint "${name}" is declared in both the rig spec and the motion spec's physics table`,
|
|
2333
|
+
);
|
|
2280
2334
|
}
|
|
2281
|
-
|
|
2282
|
-
constraintTypes.set(name, 'physics');
|
|
2335
|
+
declareConstraint(name, 'physics');
|
|
2283
2336
|
if (!boneNames.has(spec.bone)) {
|
|
2284
2337
|
throw new CompileError(`physics constraint "${name}" targets unknown bone "${spec.bone}"`);
|
|
2285
2338
|
}
|
|
@@ -2353,6 +2406,12 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2353
2406
|
const animations: SpineSkeletonJson['animations'] = {};
|
|
2354
2407
|
const declaredDurations: Record<string, number> = {};
|
|
2355
2408
|
const slotNames = new Set(slots.map((s) => s.name));
|
|
2409
|
+
// Which slots an `rgba2` timeline may be keyed on: the ones the EMITTED file
|
|
2410
|
+
// gives a `dark`, for the same reason `attachmentIndex` is built off the
|
|
2411
|
+
// emitted skins — what the runtime does with a timeline depends on the file,
|
|
2412
|
+
// not on the spec that produced it. A slot whose `dark` never reached the
|
|
2413
|
+
// artifact is a slot the runtime allocates no dark colour for (issue #690).
|
|
2414
|
+
const darkSlots = new Set(slots.filter((s) => s.dark !== undefined).map((s) => s.name));
|
|
2356
2415
|
|
|
2357
2416
|
withMotionSource(() => {
|
|
2358
2417
|
checkMotionGroups(motion);
|
|
@@ -2382,22 +2441,24 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2382
2441
|
const resolved = perMember.get(target)!;
|
|
2383
2442
|
if (family !== null) {
|
|
2384
2443
|
const label = CONSTRAINT_TRACK_FAMILIES[family].label;
|
|
2385
|
-
if (!constraintNames.has(target)) {
|
|
2386
|
-
const known = [...constraintTypes.entries()].filter(([, t]) => t === family).map(([n]) => n);
|
|
2387
|
-
throw new CompileError(
|
|
2388
|
-
`animation "${animName}" keys unknown ${label} "${target}"` +
|
|
2389
|
-
(known.length ? ` (the rig declares: ${known.join(', ')})` : `, and the rig declares no ${family} constraint at all`),
|
|
2390
|
-
);
|
|
2391
|
-
}
|
|
2392
2444
|
// Resolved by name AND by type in the parser
|
|
2393
2445
|
// (`findConstraint(name, PathConstraintData)`), which returns null on
|
|
2394
2446
|
// a type mismatch and makes `readAnimation` throw in the consumer's
|
|
2395
|
-
// process. Named here instead, where the motion file can be named too
|
|
2396
|
-
|
|
2397
|
-
|
|
2447
|
+
// process. Named here instead, where the motion file can be named too
|
|
2448
|
+
// — and looked up the same way, so a rig that declares `leg` under two
|
|
2449
|
+
// kinds sends each track to its own constraint.
|
|
2450
|
+
if (!constraintDeclared.has(constraintAt(family, target))) {
|
|
2451
|
+
const kinds = constraintKinds.get(target) ?? [];
|
|
2452
|
+
if (kinds.length > 0) {
|
|
2453
|
+
throw new CompileError(
|
|
2454
|
+
`animation "${animName}" keys "${target}" as a ${label}, but the rig declares it as a "${kinds.join('"/"')}" ` +
|
|
2455
|
+
'constraint — a timeline group resolves its target by name AND type, misses, and the loader throws',
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
const known = constraintNamesOfKind.get(family) ?? [];
|
|
2398
2459
|
throw new CompileError(
|
|
2399
|
-
`animation "${animName}" keys
|
|
2400
|
-
|
|
2460
|
+
`animation "${animName}" keys unknown ${label} "${target}"` +
|
|
2461
|
+
(known.length ? ` (the rig declares: ${known.join(', ')})` : `, and the rig declares no ${family} constraint at all`),
|
|
2401
2462
|
);
|
|
2402
2463
|
}
|
|
2403
2464
|
} else if (isBoneTrack) {
|
|
@@ -2407,10 +2468,14 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2407
2468
|
} else if (!slotNames.has(target)) {
|
|
2408
2469
|
throw new CompileError(`animation "${animName}" targets unknown slot "${target}"`);
|
|
2409
2470
|
}
|
|
2410
|
-
|
|
2471
|
+
// A timeline is a kind, a name and a property — `physics.leg.mix` and
|
|
2472
|
+
// `path.leg.mix` are two timelines on two constraints, so the claim
|
|
2473
|
+
// carries the family the target was resolved in (issue #692). The
|
|
2474
|
+
// sentence still names the pair the author wrote.
|
|
2475
|
+
const claim = `${family ?? (isBoneTrack ? 'bone' : 'slot')} ${target}.${track.property}`;
|
|
2411
2476
|
if (claimed.has(claim)) {
|
|
2412
2477
|
throw new CompileError(
|
|
2413
|
-
`animation "${animName}" has two tracks on ${
|
|
2478
|
+
`animation "${animName}" has two tracks on ${target}.${track.property}; merge them into one track`,
|
|
2414
2479
|
);
|
|
2415
2480
|
}
|
|
2416
2481
|
claimed.add(claim);
|
|
@@ -2430,7 +2495,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2430
2495
|
)
|
|
2431
2496
|
: isBoneTrack
|
|
2432
2497
|
? compileValueTrack(resolved, motion, animName, anim.duration, target, shift, BONE_TRACKS, 'bone')
|
|
2433
|
-
: compileTrack(resolved, motion, animName, anim.duration, target, shift, attachmentIndex);
|
|
2498
|
+
: compileTrack(resolved, motion, animName, anim.duration, target, shift, attachmentIndex, darkSlots);
|
|
2434
2499
|
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time as number);
|
|
2435
2500
|
if (family !== null) (familyTimelines[family][target] ??= {})[track.property] = keys;
|
|
2436
2501
|
else if (isBoneTrack) (boneTimelines[target] ??= {})[track.property] = keys;
|
|
@@ -2456,9 +2521,15 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2456
2521
|
const tracks: Array<MotionIkTrack | MotionTransformTrack> = anim[group] ?? [];
|
|
2457
2522
|
for (const track of tracks) {
|
|
2458
2523
|
const name = track.constraint;
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2524
|
+
if (!constraintDeclared.has(constraintAt(group, name))) {
|
|
2525
|
+
const kinds = constraintKinds.get(name) ?? [];
|
|
2526
|
+
if (kinds.length > 0) {
|
|
2527
|
+
throw new CompileError(
|
|
2528
|
+
`animation "${animName}" keys "${name}" as ${group === 'ik' ? 'an' : 'a'} ${group} constraint, but the rig declares it as a ` +
|
|
2529
|
+
`"${kinds.join('"/"')}" constraint — the parser looks a timeline's target up by name AND type, misses, and throws`,
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
const known = constraintNamesOfKind.get(group) ?? [];
|
|
2462
2533
|
throw new CompileError(
|
|
2463
2534
|
`animation "${animName}" keys unknown ${group} constraint "${name}"; ` +
|
|
2464
2535
|
(known.length
|
|
@@ -2466,12 +2537,6 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2466
2537
|
: `the rig declares no ${group} constraint at all`),
|
|
2467
2538
|
);
|
|
2468
2539
|
}
|
|
2469
|
-
if (type !== group) {
|
|
2470
|
-
throw new CompileError(
|
|
2471
|
-
`animation "${animName}" keys "${name}" as ${group === 'ik' ? 'an' : 'a'} ${group} constraint, but the rig declares it as a ` +
|
|
2472
|
-
`"${type}" constraint — the parser looks a timeline's target up by name AND type, misses, and throws`,
|
|
2473
|
-
);
|
|
2474
|
-
}
|
|
2475
2540
|
if (constraintTimelines[group][name]) {
|
|
2476
2541
|
throw new CompileError(
|
|
2477
2542
|
`animation "${animName}" has two ${group} timelines on constraint "${name}"; ` +
|
|
@@ -2484,7 +2549,10 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2484
2549
|
motion,
|
|
2485
2550
|
animName,
|
|
2486
2551
|
anim.duration,
|
|
2487
|
-
|
|
2552
|
+
// The ik table, asked only by the ik group: `leg` may also be a
|
|
2553
|
+
// transform constraint, and a transform key set carries no flag for
|
|
2554
|
+
// the rig's booleans to be stamped onto (issue #692).
|
|
2555
|
+
group === 'ik' ? (ikRigFlags.get(name) ?? {}) : {},
|
|
2488
2556
|
);
|
|
2489
2557
|
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time as number);
|
|
2490
2558
|
constraintTimelines[group][name] = keys;
|
|
@@ -3055,8 +3123,14 @@ function nameSkinAttachment(att: SpineAttachment, name: string, placeholder: str
|
|
|
3055
3123
|
// Key order is the parser's reading order — `name`, then `path`, then the rest
|
|
3056
3124
|
// as the builder wrote it — for the same reason every other emitted object
|
|
3057
3125
|
// follows it: the file is read by people and diffed against references.
|
|
3058
|
-
|
|
3059
|
-
|
|
3126
|
+
//
|
|
3127
|
+
// ⚠️ The three kinds here are the three the parser gives a texture `path` to,
|
|
3128
|
+
// and `linkedmesh` is one of them (`SkeletonJson.ts:541`, `:570` — the mesh
|
|
3129
|
+
// branch is shared). Leaving it out would write a `name` and no `path`, and
|
|
3130
|
+
// `path` defaults to `name`, so a contested link would resolve the region
|
|
3131
|
+
// "<skin>/<placeholder>", which no atlas holds.
|
|
3132
|
+
if (kind !== 'region' && kind !== 'mesh' && kind !== 'linkedmesh') return { name, ...att };
|
|
3133
|
+
const { path, ...rest } = att as SpineRegionAttachment | SpineMeshAttachment | SpineLinkedMeshAttachment;
|
|
3060
3134
|
return { name, path: path ?? placeholder, ...rest } as SpineAttachment;
|
|
3061
3135
|
}
|
|
3062
3136
|
|
|
@@ -3093,11 +3167,37 @@ interface AttachmentContext {
|
|
|
3093
3167
|
depths: Map<string, number[]>;
|
|
3094
3168
|
/** Every slot the rig declares — a clipping attachment's `end` resolves here. */
|
|
3095
3169
|
slotNames: Set<string>;
|
|
3170
|
+
/**
|
|
3171
|
+
* Linked meshes whose `source` is still unresolved — `resolveLinkedMeshes`
|
|
3172
|
+
* empties it once every skin has been built.
|
|
3173
|
+
*
|
|
3174
|
+
* ⭐ Deferred for the reason spine-core defers its own (`this.linkedMeshes`,
|
|
3175
|
+
* `SkeletonJson.js:56` filled at `:581` and drained at `:427-449`): a link may
|
|
3176
|
+
* name a source in a skin or a slot this loop has not reached yet, so resolving
|
|
3177
|
+
* it in place would refuse a correct rig on nothing but declaration order.
|
|
3178
|
+
*/
|
|
3179
|
+
links: PendingLink[];
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
/** One linked mesh, with everything a refusal has to name once the skins exist. */
|
|
3183
|
+
interface PendingLink {
|
|
3184
|
+
where: string;
|
|
3185
|
+
/** What it asked for, the parser's defaults already applied. */
|
|
3186
|
+
skin: string;
|
|
3187
|
+
slot: string;
|
|
3188
|
+
source: string;
|
|
3189
|
+
/**
|
|
3190
|
+
* Whether the author wrote `skin`/`slot` or is taking the parser's default —
|
|
3191
|
+
* which is the half of the refusal that says where a name came from, and the
|
|
3192
|
+
* one an author who wrote neither key needs most.
|
|
3193
|
+
*/
|
|
3194
|
+
skinStated: boolean;
|
|
3195
|
+
slotStated: boolean;
|
|
3096
3196
|
}
|
|
3097
3197
|
|
|
3098
3198
|
/**
|
|
3099
3199
|
* The seven `type` values `readAttachment` has a branch for (`:540-651`), and
|
|
3100
|
-
* the
|
|
3200
|
+
* the six rigc emits.
|
|
3101
3201
|
*
|
|
3102
3202
|
* ⚠️ The lists are separate because the refusals are separate. A `point` is a
|
|
3103
3203
|
* name the format HAS and rigc has not built; `sequence` is not a type at all —
|
|
@@ -3107,22 +3207,24 @@ interface AttachmentContext {
|
|
|
3107
3207
|
* One message said exactly that about every string it did not recognise.
|
|
3108
3208
|
*/
|
|
3109
3209
|
const SPINE_ATTACHMENT_TYPES = ['region', 'mesh', 'linkedmesh', 'boundingbox', 'path', 'point', 'clipping'] as const;
|
|
3110
|
-
const EMITTED_ATTACHMENT_TYPES = ['region', 'mesh', 'boundingbox', 'clipping', 'path'] as const;
|
|
3210
|
+
const EMITTED_ATTACHMENT_TYPES = ['region', 'mesh', 'linkedmesh', 'boundingbox', 'clipping', 'path'] as const;
|
|
3111
3211
|
|
|
3112
3212
|
/**
|
|
3113
3213
|
* What each deferred type is and what it would carry — `docs/SPEC_COVERAGE.md`
|
|
3114
|
-
* part 1-6's
|
|
3214
|
+
* part 1-6's row, restated where the refusal can print it.
|
|
3115
3215
|
*
|
|
3116
3216
|
* ⭐ The construct, not just its name. "attachment type X is in the Spine 4.3
|
|
3117
3217
|
* format and rigc does not emit it yet" tells an author who already knows what a
|
|
3118
|
-
*
|
|
3218
|
+
* point is that rigc will not do it, and tells an author who does not know
|
|
3119
3219
|
* nothing at all — and the second is the reader this repository writes for.
|
|
3220
|
+
*
|
|
3221
|
+
* ⚠️ `linkedmesh` was the other entry until issue #691 emitted it. The sentence
|
|
3222
|
+
* below no longer says *"neither a point nor a linked mesh appears anywhere in
|
|
3223
|
+
* the benchmark corpus"*, because half of that is now a statement about a type
|
|
3224
|
+
* rigc writes, and a refusal that argues from a construct it has since built is
|
|
3225
|
+
* a refusal nobody can act on.
|
|
3120
3226
|
*/
|
|
3121
3227
|
const DEFERRED_ATTACHMENTS: Record<string, string> = {
|
|
3122
|
-
linkedmesh:
|
|
3123
|
-
'a mesh that takes its geometry from another mesh instead of stating any — a region/mesh head, then ' +
|
|
3124
|
-
'"source" (the attachment it links to, and the key that MAKES it linked), "slot" and "skin" naming where ' +
|
|
3125
|
-
'that source lives, and "timelines" (default true) for whether it follows the source\'s deform keys',
|
|
3126
3228
|
point:
|
|
3127
3229
|
'a position and an angle with no geometry at all — "x", "y", "rotation" and "color", posed by its bone and ' +
|
|
3128
3230
|
'drawn by nothing; what reads it is game code asking where a muzzle or a hand is',
|
|
@@ -3171,9 +3273,10 @@ function buildRigAttachment(
|
|
|
3171
3273
|
// them (`:568-569`, `:582`; SPEC_COVERAGE part 1-6) — so a mesh carrying
|
|
3172
3274
|
// `source` is a linked mesh whatever its `type` says, and refusing it as "two
|
|
3173
3275
|
// keys this compiler does not read: source, skin" sent the author to delete
|
|
3174
|
-
// the one key that made it linked.
|
|
3175
|
-
|
|
3176
|
-
|
|
3276
|
+
// the one key that made it linked. Both spellings reach one builder for the
|
|
3277
|
+
// same reason they reach one parser branch.
|
|
3278
|
+
if (type === 'linkedmesh' || (type === 'mesh' && (att as { source?: unknown }).source !== undefined)) {
|
|
3279
|
+
return buildRigLinkedMesh(att as RigLinkedMeshAttachment, placeholder, where, ctx);
|
|
3177
3280
|
}
|
|
3178
3281
|
if (type === 'region') return buildRigRegion(att as RigRegionAttachment, placeholder, where, ctx);
|
|
3179
3282
|
if (type === 'mesh') return buildRigMesh(att as RigMeshAttachment, placeholder, where, ctx);
|
|
@@ -3200,9 +3303,9 @@ function buildRigAttachment(
|
|
|
3200
3303
|
function deferredAttachmentRefusal(type: string, where: string, how: string): string {
|
|
3201
3304
|
return (
|
|
3202
3305
|
`${where}: this attachment is a "${type}"${how} — ${DEFERRED_ATTACHMENTS[type]}. ` +
|
|
3203
|
-
`rigc does not emit it yet, deliberately: it emits ${EMITTED_ATTACHMENT_TYPES.join(', ')}, and
|
|
3204
|
-
'
|
|
3205
|
-
|
|
3306
|
+
`rigc does not emit it yet, deliberately: it emits ${EMITTED_ATTACHMENT_TYPES.join(', ')}, and a point ` +
|
|
3307
|
+
'appears nowhere in the benchmark corpus (docs/SPEC_COVERAGE.md parts 3-1 and 4-2), so it is not on the ' +
|
|
3308
|
+
"ladder's critical path. docs/SPEC_COVERAGE.md part 1-6 is the row this sentence reads " +
|
|
3206
3309
|
'from, and it is what an implementation would have to carry.'
|
|
3207
3310
|
);
|
|
3208
3311
|
}
|
|
@@ -3917,6 +4020,178 @@ function buildRigMesh(
|
|
|
3917
4020
|
return out;
|
|
3918
4021
|
}
|
|
3919
4022
|
|
|
4023
|
+
/**
|
|
4024
|
+
* A mesh that borrows another mesh's geometry. `type: "linkedmesh"`, or
|
|
4025
|
+
* `type: "mesh"` carrying `source` — one parser branch, one builder.
|
|
4026
|
+
*
|
|
4027
|
+
* 🚨 **Everything this refuses, the parser reads in silence**, which is the whole
|
|
4028
|
+
* reason the refusals exist. Measured on forged skeletons through spine-core
|
|
4029
|
+
* 4.3.13:
|
|
4030
|
+
*
|
|
4031
|
+
* - Geometry beside `source`: the branch returns at `:586`, BEFORE
|
|
4032
|
+
* `readVertices`. A link declaring 5 uvs, 3 triangles, `hull: 5` and
|
|
4033
|
+
* `edges: [0, 2]` beside a 4-vertex source loaded with the source's 8-long
|
|
4034
|
+
* `worldVerticesLength`, 6 triangles, `hullLength` 8 and 10 edges. Nothing
|
|
4035
|
+
* the author wrote was read and nothing said so.
|
|
4036
|
+
* - A chain — a link whose source is itself a link — resolves in the order
|
|
4037
|
+
* `linkedMeshes` was FILLED, which is the order the skins' JSON keys are
|
|
4038
|
+
* iterated in. Source-first, the chained link loaded the full geometry; with
|
|
4039
|
+
* the two keys swapped in the same file it loaded `worldVerticesLength` 0,
|
|
4040
|
+
* 0 triangles, 0 bones and a 0x0 size, silently. A construct whose meaning
|
|
4041
|
+
* depends on key order is a construct rigc will not write.
|
|
4042
|
+
* - A link to itself is that chain at length one, and loads the same nothing.
|
|
4043
|
+
*
|
|
4044
|
+
* What is NOT refused here is `source` naming something in a skin or a slot this
|
|
4045
|
+
* builder has not reached yet: that is `resolveLinkedMeshes`' job, and doing it
|
|
4046
|
+
* here would refuse a correct rig on declaration order — the very thing the
|
|
4047
|
+
* chain measurement above condemns.
|
|
4048
|
+
*/
|
|
4049
|
+
function buildRigLinkedMesh(
|
|
4050
|
+
att: RigLinkedMeshAttachment,
|
|
4051
|
+
placeholder: string,
|
|
4052
|
+
where: string,
|
|
4053
|
+
ctx: AttachmentContext,
|
|
4054
|
+
): SpineLinkedMeshAttachment {
|
|
4055
|
+
// The mesh keys the parser does not reach on this branch. Named one by one,
|
|
4056
|
+
// because "remove what does not belong" is not an instruction an author can
|
|
4057
|
+
// act on and the remedy for each of these is the same single sentence.
|
|
4058
|
+
const geometry = (['uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges', 'generator'] as const)
|
|
4059
|
+
.filter((key) => att[key] !== undefined);
|
|
4060
|
+
if (geometry.length > 0) {
|
|
4061
|
+
throw new CompileError(
|
|
4062
|
+
`${where}: a linked mesh states ${geometry.map((k) => JSON.stringify(k)).join(', ')}, and a linked mesh has ` +
|
|
4063
|
+
'no geometry of its own — it draws the geometry of the attachment "source" names. The parser returns from ' +
|
|
4064
|
+
'the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so these keys are read by nothing ' +
|
|
4065
|
+
"at all: a link declaring 5 uvs beside a 4-vertex source loads the SOURCE's 4 vertices and says nothing. " +
|
|
4066
|
+
`Remove ${geometry.length === 1 ? 'it' : 'them'}, or remove "source" and author this as a mesh of its own.`,
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
4069
|
+
const source = att.source;
|
|
4070
|
+
if (typeof source !== 'string' || source.length === 0) {
|
|
4071
|
+
throw new CompileError(
|
|
4072
|
+
`${where}: a linked mesh needs "source" — the PLACEHOLDER of the mesh whose geometry it draws (the key that ` +
|
|
4073
|
+
`attachment is filed under in its skin, not its "name"), and this one states ${JSON.stringify(source) ?? String(source)}. ` +
|
|
4074
|
+
'"source" is what MAKES a mesh linked: `getValue(map, "source", null)` is falsy-tested ' +
|
|
4075
|
+
'(`SkeletonJson.ts:582`), so an absent or empty one is read as an ordinary mesh, whose `uvs` this ' +
|
|
4076
|
+
'attachment does not have — the parser dereferences `map.uvs.length` and throws.',
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4079
|
+
if (att.slot !== undefined && !ctx.slotNames.has(att.slot)) {
|
|
4080
|
+
throw new CompileError(
|
|
4081
|
+
`${where}: "slot" is ${JSON.stringify(att.slot)}, which the rig does not declare as a slot. It names where ` +
|
|
4082
|
+
'the source lives and is resolved through `skeletonData.findSlot` (`SkeletonJson.ts:575`), which throws ' +
|
|
4083
|
+
`\`Source mesh slot not found\` on a miss. The rig's slots are ${[...ctx.slotNames].sort().map((s) => JSON.stringify(s)).join(', ')}. ` +
|
|
4084
|
+
"Leave it out and the source is looked for in this attachment's own slot, which is the parser's default.",
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
ctx.links.push({
|
|
4088
|
+
where,
|
|
4089
|
+
skin: att.skin ?? 'default',
|
|
4090
|
+
slot: att.slot ?? ctx.slotName,
|
|
4091
|
+
source,
|
|
4092
|
+
skinStated: att.skin !== undefined,
|
|
4093
|
+
slotStated: att.slot !== undefined,
|
|
4094
|
+
});
|
|
4095
|
+
// The art side is a mesh's, unchanged: a link draws its OWN region, which is
|
|
4096
|
+
// the reason the type exists — one triangulation, one outfit's pixels each.
|
|
4097
|
+
const img = att.image === undefined ? undefined : atlasedImage(att.image, where, ctx);
|
|
4098
|
+
const width = att.width ?? img?.width;
|
|
4099
|
+
const height = att.height ?? img?.height;
|
|
4100
|
+
if (width === undefined || height === undefined) {
|
|
4101
|
+
throw new CompileError(
|
|
4102
|
+
`${where}: a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
|
|
4103
|
+
);
|
|
4104
|
+
}
|
|
4105
|
+
const out: SpineLinkedMeshAttachment = { type: 'linkedmesh', source, width: r6(width), height: r6(height) };
|
|
4106
|
+
const path = attachmentPath(att, placeholder);
|
|
4107
|
+
if (path !== undefined) out.path = path;
|
|
4108
|
+
// Only where they differ from the parser's own defaults. Writing `timelines:
|
|
4109
|
+
// true`, or a `skin` of "default", would be a byte the editor's own export
|
|
4110
|
+
// does not carry.
|
|
4111
|
+
if (att.slot !== undefined && att.slot !== ctx.slotName) out.slot = att.slot;
|
|
4112
|
+
if (att.skin !== undefined && att.skin !== 'default') out.skin = att.skin;
|
|
4113
|
+
if (att.timelines === false) out.timelines = false;
|
|
4114
|
+
if (att.color !== undefined) out.color = att.color;
|
|
4115
|
+
// 🚫 NOT registered in `ctx.meshes`, and that is a decision rather than an
|
|
4116
|
+
// omission. `meshKinds` is keyed by SLOT and the commonest linked mesh shares
|
|
4117
|
+
// its source's slot from another skin, so an entry here would overwrite the
|
|
4118
|
+
// source's own `ring`/`ribbon`/`contour` kind and silence `A21_MESH_RIM_PINNED`
|
|
4119
|
+
// on the mesh rigc actually built — a gate turned off by a feature, which is
|
|
4120
|
+
// the failure mode issue #44 is about, pointed the other way. The validator
|
|
4121
|
+
// tells a link apart from the ARTIFACT instead — off the file's own `source`
|
|
4122
|
+
// keys, joined to the loaded attachment by (skin, slot, placeholder) — which
|
|
4123
|
+
// is the archetype rule's own words: an assertion reads the rig, never a name.
|
|
4124
|
+
return out;
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
/**
|
|
4128
|
+
* Resolve every `source` against the skins that were built, and refuse a miss by
|
|
4129
|
+
* name.
|
|
4130
|
+
*
|
|
4131
|
+
* Three of the four refusals are one per throw the runtime would have made
|
|
4132
|
+
* (`SkeletonJson.js:427-436`) — `Skin not found`, `Source mesh slot not found`,
|
|
4133
|
+
* `Source mesh not found`. Those are thrown `Error`s, so left to the round trip
|
|
4134
|
+
* they arrive as `A00_ROUNDTRIP_PARSE`'s report of the runtime's sentence, which
|
|
4135
|
+
* names neither the attachment that asked nor the skin and slot it searched. The
|
|
4136
|
+
* fourth has no runtime throw behind it at all: a source that is not a mesh is
|
|
4137
|
+
* read field by field off whatever was found, and `undefined` is not an error.
|
|
4138
|
+
*/
|
|
4139
|
+
function resolveLinkedMeshes(
|
|
4140
|
+
links: readonly PendingLink[],
|
|
4141
|
+
tables: Map<string, Record<string, Record<string, SpineAttachment>>>,
|
|
4142
|
+
): void {
|
|
4143
|
+
const skinNames = [...tables.keys()];
|
|
4144
|
+
for (const link of links) {
|
|
4145
|
+
// ⚠️ Only a STATED `skin` can miss here, and it is worth saying why rather
|
|
4146
|
+
// than leaving the other half to look like a branch nothing reaches: rigc
|
|
4147
|
+
// always emits a `default` skin, empty if it has to (`tableFor('default')`),
|
|
4148
|
+
// so the parser's own default always resolves to a table. An omitted `skin`
|
|
4149
|
+
// therefore fails one line down, at the source, and the sentence there is
|
|
4150
|
+
// what names the trap.
|
|
4151
|
+
const table = tables.get(link.skin);
|
|
4152
|
+
if (table === undefined) {
|
|
4153
|
+
throw new CompileError(
|
|
4154
|
+
`${link.where}: "skin" is ${JSON.stringify(link.skin)}, and the rig declares no such skin. The rig's skins ` +
|
|
4155
|
+
`are ${skinNames.map((s) => JSON.stringify(s)).join(', ')}. ` +
|
|
4156
|
+
"Left to the round trip this is the runtime's `Skin not found`, which names neither this attachment nor " +
|
|
4157
|
+
'where it was looking.',
|
|
4158
|
+
);
|
|
4159
|
+
}
|
|
4160
|
+
const slot = table[link.slot];
|
|
4161
|
+
const held = slot === undefined ? [] : Object.keys(slot).sort();
|
|
4162
|
+
const found = slot?.[link.source];
|
|
4163
|
+
if (found === undefined) {
|
|
4164
|
+
throw new CompileError(
|
|
4165
|
+
`${link.where}: "source" is ${JSON.stringify(link.source)}, and skin ${JSON.stringify(link.skin)}` +
|
|
4166
|
+
`${link.skinStated ? '' : ' (the default skin, because no "skin" was stated — never the skin this attachment is written in)'}` +
|
|
4167
|
+
` slot ${JSON.stringify(link.slot)}${link.slotStated ? '' : ' (this attachment\'s own slot, because no "slot" was stated)'} ` +
|
|
4168
|
+
`holds ${held.length === 0 ? 'no attachment at all' : `${held.length}: ${held.map((k) => JSON.stringify(k)).join(', ')}`}. ` +
|
|
4169
|
+
'"source" is the PLACEHOLDER the source is filed under — `skin.getAttachment(slotIndex, source)` ' +
|
|
4170
|
+
"(`SkeletonJson.ts:433`), whose table is keyed by the JSON key, not by the attachment's `name`. " +
|
|
4171
|
+
"Left to the round trip this is the runtime's `Source mesh not found`.",
|
|
4172
|
+
);
|
|
4173
|
+
}
|
|
4174
|
+
const type = (found as { type?: string }).type ?? 'region';
|
|
4175
|
+
if (type === 'linkedmesh') {
|
|
4176
|
+
throw new CompileError(
|
|
4177
|
+
`${link.where}: "source" is ${JSON.stringify(link.source)}, which is itself a linked mesh, and a chain of ` +
|
|
4178
|
+
'them is refused. Measured through spine-core: the runtime resolves links in the order they were read, ' +
|
|
4179
|
+
"so a link whose source is a link loads the source's geometry when the source comes first in the file " +
|
|
4180
|
+
'and loads NOTHING — `worldVerticesLength` 0, 0 triangles, a 0x0 size — when the two are swapped, in ' +
|
|
4181
|
+
'silence either way. Point "source" at the mesh itself.',
|
|
4182
|
+
);
|
|
4183
|
+
}
|
|
4184
|
+
if (type !== 'mesh') {
|
|
4185
|
+
throw new CompileError(
|
|
4186
|
+
`${link.where}: "source" is ${JSON.stringify(link.source)}, which is a ${JSON.stringify(type)} attachment ` +
|
|
4187
|
+
"and not a mesh. A linked mesh takes another MESH's `uvs`, `triangles`, `vertices`, `hull` and `edges` " +
|
|
4188
|
+
'(`MeshAttachment.setSourceMesh`); the runtime casts whatever it finds and reads those fields off it, ' +
|
|
4189
|
+
'which on any other type is `undefined` and no error.',
|
|
4190
|
+
);
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
|
|
3920
4195
|
/**
|
|
3921
4196
|
* Invoke a `src/mesh.ts` builder from rig-spec data.
|
|
3922
4197
|
*
|
|
@@ -6050,22 +6325,40 @@ function deformGeometryOf(
|
|
|
6050
6325
|
let worldVerticesLength: number;
|
|
6051
6326
|
if (type === 'mesh') {
|
|
6052
6327
|
worldVerticesLength = (att as SpineMeshAttachment).uvs.length;
|
|
6053
|
-
} else if (type === 'boundingbox' || type === 'clipping') {
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
//
|
|
6057
|
-
//
|
|
6058
|
-
//
|
|
6059
|
-
//
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6328
|
+
} else if (type === 'boundingbox' || type === 'clipping' || type === 'path') {
|
|
6329
|
+
// ⭐ A `path` reaches this line as of issue #696, and it is the SAME line the
|
|
6330
|
+
// other two vertex-and-no-triangles types take: the array the parser sizes
|
|
6331
|
+
// is `vertexCount * 2`, the two encodings below are the mesh's own, and a
|
|
6332
|
+
// path's vertices are its control points.
|
|
6333
|
+
//
|
|
6334
|
+
// ⚠️ What stood here was a `NotImplementedError` — *a path attachment does
|
|
6335
|
+
// have a vertex array, and rigc does not key it yet* — whose stated reason
|
|
6336
|
+
// was that a deformed path invalidates the `lengths` the attachment carries.
|
|
6337
|
+
// The reason is measured and it does not belong to rigc:
|
|
6338
|
+
//
|
|
6339
|
+
// - `lengths` is a field of the ATTACHMENT. The format has nowhere to put
|
|
6340
|
+
// a per-key length, so no export of any tool carries a re-measured one;
|
|
6341
|
+
// the editor's own is the setup measurement, which is what
|
|
6342
|
+
// `pathCurveLengths` reproduces digit for digit.
|
|
6343
|
+
// - `PathConstraint.computeWorldPositions` reads that field only under
|
|
6344
|
+
// `constantSpeed: false` (`PathConstraint.js:205`). Under the parser's
|
|
6345
|
+
// default, `true`, it re-measures the curve from the posed world
|
|
6346
|
+
// vertices every frame — and those come through
|
|
6347
|
+
// `VertexAttachment.computeWorldVertices`, which uses
|
|
6348
|
+
// `slot.appliedPose.deform` when the array is non-empty
|
|
6349
|
+
// (`attachments/Attachment.js:97-115`). So the constraint follows the
|
|
6350
|
+
// DEFORMED spline, and the stale field is not read at all.
|
|
6351
|
+
//
|
|
6352
|
+
// ⇒ refusing it was refusing correct data for behaviour the format has and
|
|
6353
|
+
// every runtime shares — the shape issues #44 and #262 already cost this
|
|
6354
|
+
// file twice. `docs/AUTHORING.md` §4.11 states the `constantSpeed: false`
|
|
6355
|
+
// reading instead, which is the honest home for it: a fact about the format
|
|
6356
|
+
// the author is choosing, not a fault rigc can measure.
|
|
6357
|
+
worldVerticesLength = (att as SpineBoundingBoxAttachment | SpinePathAttachment).vertexCount * 2;
|
|
6065
6358
|
} else {
|
|
6066
6359
|
throw new CompileError(
|
|
6067
6360
|
`${where}: a deform timeline keys the vertices of an attachment, and this one is a "${type}" — ` +
|
|
6068
|
-
'it has no vertex array to deform. Deformable types: mesh, boundingbox, clipping.',
|
|
6361
|
+
'it has no vertex array to deform. Deformable types: mesh, boundingbox, clipping, path.',
|
|
6069
6362
|
);
|
|
6070
6363
|
}
|
|
6071
6364
|
const vertices = (att as SpineMeshAttachment).vertices ?? [];
|
|
@@ -6910,6 +7203,7 @@ function compileTrack(
|
|
|
6910
7203
|
target: string,
|
|
6911
7204
|
shift: number,
|
|
6912
7205
|
attachments: SlotAttachmentIndex,
|
|
7206
|
+
darkSlots: ReadonlySet<string>,
|
|
6913
7207
|
): SpineTimelineKey[] {
|
|
6914
7208
|
const where = `animation "${animName}" slot "${target}" ${track.property}`;
|
|
6915
7209
|
// Before any key is shaped, and before the empty-track refusal: a property
|
|
@@ -6922,6 +7216,25 @@ function compileTrack(
|
|
|
6922
7216
|
`(it has: ${Object.keys(SLOT_TRACKS).join(', ')})`,
|
|
6923
7217
|
);
|
|
6924
7218
|
}
|
|
7219
|
+
// 🚨 Raised here, with the target and before the keys, for the same reason as
|
|
7220
|
+
// the refusal above: the fault is in the pairing of timeline and slot, not in
|
|
7221
|
+
// any key. A two-colour timeline poses `SlotPose.darkColor`, and the runtime
|
|
7222
|
+
// creates that object only for a slot whose setup pose HAS one — `Slot`'s
|
|
7223
|
+
// constructor reads `if (data.setupPose.darkColor != null)` before allocating
|
|
7224
|
+
// it. Measured on the emitted file with the refusal removed: `SkeletonJson`
|
|
7225
|
+
// loads it in silence, `SlotData.setupPose.darkColor` is `null`, and the first
|
|
7226
|
+
// `state.apply` throws `TypeError: null is not an object (evaluating 'dark.r =
|
|
7227
|
+
// …')` inside `RGBA2Timeline.apply1`. That is a crash in the consumer's
|
|
7228
|
+
// process, from a file every parser accepts — the exact silence this compiler
|
|
7229
|
+
// exists to convert into a name (issue #690).
|
|
7230
|
+
if (shape === 'rgba2' && !darkSlots.has(target)) {
|
|
7231
|
+
throw new CompileError(
|
|
7232
|
+
`${where}: slot "${target}" declares no setup "dark", and an "rgba2" timeline poses a slot's dark colour — ` +
|
|
7233
|
+
'the runtime allocates one only for a slot whose setup pose has it, so applying this animation throws ' +
|
|
7234
|
+
`instead of tinting. Give slot "${target}" a \`dark\` in the rig spec (the colour it holds at rest), or ` +
|
|
7235
|
+
'key "rgba" if only the light colour moves',
|
|
7236
|
+
);
|
|
7237
|
+
}
|
|
6925
7238
|
if (!track.keys.length) throw new CompileError(`${where}: no keys`);
|
|
6926
7239
|
|
|
6927
7240
|
const out: SpineTimelineKey[] = [];
|
|
@@ -6960,16 +7273,28 @@ function compileTrack(
|
|
|
6960
7273
|
continue;
|
|
6961
7274
|
}
|
|
6962
7275
|
|
|
6963
|
-
// rgba — the
|
|
6964
|
-
//
|
|
6965
|
-
|
|
6966
|
-
|
|
7276
|
+
// rgba / rgba2 — the two colour shapes `SLOT_TRACKS` names, and between them
|
|
7277
|
+
// the only ways left to reach here: `attachment` returned above and a
|
|
7278
|
+
// property the table does not carry was refused before any key was shaped.
|
|
7279
|
+
//
|
|
7280
|
+
// ⚠️ The two differ in three places and nowhere else, so they are read out of
|
|
7281
|
+
// `shape` rather than forked into two loops: how many channels a `v` carries,
|
|
7282
|
+
// which FIELDS the emitted key spells them as (`color`, or `light` + `dark`),
|
|
7283
|
+
// and therefore how long a raw curve array is. Everything else — the
|
|
7284
|
+
// ease/curve exclusivity, the stepped case, the hold test, the last-key
|
|
7285
|
+
// refusals — is one rule, and a second copy of it is a second thing to keep
|
|
7286
|
+
// in step.
|
|
7287
|
+
const spelling = shape === 'rgba2' ? '[lr,lg,lb,la,dr,dg,db]' : '[r,g,b,a]';
|
|
7288
|
+
const channels = shape === 'rgba2' ? 7 : 4;
|
|
7289
|
+
const colourOf = (v: number[]): Record<string, string> => (shape === 'rgba2' ? rgba2Hex(v) : { color: rgbaHex(v) });
|
|
7290
|
+
if (!Array.isArray(key.v)) throw new CompileError(`${where}: ${shape} key value must be ${spelling}`);
|
|
7291
|
+
const entry: SpineTimelineKey = { time, ...colourOf(key.v) };
|
|
6967
7292
|
if (key.ease !== undefined && key.curve !== undefined) {
|
|
6968
7293
|
throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
|
|
6969
7294
|
}
|
|
6970
7295
|
if (key.curve !== undefined) {
|
|
6971
7296
|
if (!next) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`);
|
|
6972
|
-
entry.curve = rawCurve(key.curve,
|
|
7297
|
+
entry.curve = rawCurve(key.curve, channels, where, String(key.t));
|
|
6973
7298
|
out.push(entry);
|
|
6974
7299
|
continue;
|
|
6975
7300
|
}
|
|
@@ -6980,14 +7305,18 @@ function compileTrack(
|
|
|
6980
7305
|
const handles = motion.easings?.[key.ease];
|
|
6981
7306
|
if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`);
|
|
6982
7307
|
if (!Array.isArray(next.v)) {
|
|
6983
|
-
throw new CompileError(`${where}:
|
|
7308
|
+
throw new CompileError(`${where}: ${shape} key value must be ${spelling}`);
|
|
6984
7309
|
}
|
|
6985
7310
|
const t2 = keyTime(next.t + shift);
|
|
6986
|
-
// 4 numbers per channel,
|
|
7311
|
+
// 4 numbers per channel, in the format's own channel order — 16 for
|
|
7312
|
+
// `rgba` (r g b a), 28 for `rgba2` (light r g b a, then dark r g b, which
|
|
7313
|
+
// is the order `readCurve` indexes them in). Short arrays become NaN
|
|
6987
7314
|
// curves with no error. The hold test reads the emitted hex rather than
|
|
6988
7315
|
// the authored floats: two colours that quantise to one byte are one
|
|
6989
|
-
// colour in the file
|
|
6990
|
-
|
|
7316
|
+
// colour in the file, and for `rgba2` that has to be true of BOTH — a key
|
|
7317
|
+
// whose light holds while its dark moves is not a hold.
|
|
7318
|
+
const held = JSON.stringify(colourOf(key.v)) === JSON.stringify(colourOf(next.v));
|
|
7319
|
+
entry.curve = easingCurve(handles, time, t2, key.v, next.v, held);
|
|
6991
7320
|
}
|
|
6992
7321
|
} else if (key.ease && !next) {
|
|
6993
7322
|
throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`);
|