spine-rigc 0.25.2 → 0.25.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/docs/AUTHORING.md +262 -16
- package/docs/FACE.md +127 -0
- package/package.json +1 -1
- package/src/compile.ts +214 -32
- package/src/ingest.ts +13 -3
- package/src/validate.ts +403 -20
package/src/compile.ts
CHANGED
|
@@ -761,6 +761,20 @@ interface ValueTrackShape {
|
|
|
761
761
|
* The defaults matter more than they look: Spine omits a field that equals the
|
|
762
762
|
* setup value, and `scale` defaults to 1 while `translate` defaults to 0. Emit
|
|
763
763
|
* `x: 0` on a scale key and the bone collapses to nothing, silently.
|
|
764
|
+
*
|
|
765
|
+
* ⭐ **The table IS the dispatch**, the same way `SLOT_TRACKS` is: `resolveTargets`
|
|
766
|
+
* asks `property in BONE_TRACKS` to decide the family, `compileValueTrack` writes
|
|
767
|
+
* a key out of the shape it finds here, and the refusal for a property that is not
|
|
768
|
+
* in it prints `Object.keys` of the same object — so the ten an author is handed
|
|
769
|
+
* cannot disagree with the ten the emitter has, because there is only one list.
|
|
770
|
+
*
|
|
771
|
+
* 🚨 It printed nothing until issue #656. The dispatch was already this table, but
|
|
772
|
+
* a bone track whose property missed it was refused as *"bone X cannot take slot
|
|
773
|
+
* property Y"* — the wrong family for a spelling that usually belongs to none, and
|
|
774
|
+
* the one target family whose refusal offered no way forward. That is also why the
|
|
775
|
+
* selftest's spelling census had to STATE these ten (`PS143`, `PS144`): four
|
|
776
|
+
* families printed their own vocabulary in their refusals and the bone family
|
|
777
|
+
* printed nothing, so there was nothing to read it off.
|
|
764
778
|
*/
|
|
765
779
|
const BONE_TRACKS: Record<string, ValueTrackShape> = {
|
|
766
780
|
translate: { fields: ['x', 'y'], identity: [0, 0] },
|
|
@@ -853,6 +867,38 @@ const SLIDER_TRACKS: Record<string, ValueTrackShape> = {
|
|
|
853
867
|
mix: { fields: ['value'], identity: [1] },
|
|
854
868
|
};
|
|
855
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Slot timelines (`animations.<a>.slots.<slot>.<timeline>`): the two
|
|
872
|
+
* `compileTrack` writes, and which key shape each one is written with.
|
|
873
|
+
*
|
|
874
|
+
* ⭐ **The table IS the dispatch.** `compileTrack` reads the shape out of here
|
|
875
|
+
* to pick its branch, and the refusal for a property that is not in it prints
|
|
876
|
+
* `Object.keys` of the same object — so the list an author is given cannot
|
|
877
|
+
* disagree with the list the emitter has, because there is only one.
|
|
878
|
+
*
|
|
879
|
+
* 🚨 It exists because the dispatch used to be one `if` on `attachment` and a
|
|
880
|
+
* fall-through to rgba, which made **every** other property name a legal
|
|
881
|
+
* spelling of an rgba timeline: `{"slot": "x", "property": "sequence", "v": [0,
|
|
882
|
+
* 0, 0, 0]}` compiled and wrote `slots.x.sequence` with rgba-shaped keys. The
|
|
883
|
+
* gate caught the file (`A00_ROUNDTRIP_PARSE: threw: Invalid timeline type for
|
|
884
|
+
* a slot`, and `A05_CURVE_ARRAY_LENGTH`) and the one-channel spelling of the
|
|
885
|
+
* same mistake was refused at compile as *"rgba value needs 4 channels, got
|
|
886
|
+
* 1"* — a message about a key nobody wrote. It is the shape `A21`'s
|
|
887
|
+
* `meshKinds[slot] || 'ring'` had (issue #44): a default that turns "nothing to
|
|
888
|
+
* emit" into an emission of the wrong thing (issue #650).
|
|
889
|
+
*
|
|
890
|
+
* ⚠️ The KEY is the timeline name as the file carries it, and the VALUE names
|
|
891
|
+
* the branch below that writes its keys — so an entry added here without a
|
|
892
|
+
* branch to write it is an entry emitted in some other timeline's shape, which
|
|
893
|
+
* is the defect this table closed rather than a new affordance. The format has
|
|
894
|
+
* four more (`rgb`, `alpha`, `rgba2`, `rgb2`); rigc emits none of them, and
|
|
895
|
+
* `A12_NO_DARK_COLOR` refuses the last two outright.
|
|
896
|
+
*/
|
|
897
|
+
export const SLOT_TRACKS: Record<string, 'attachment' | 'rgba'> = {
|
|
898
|
+
attachment: 'attachment',
|
|
899
|
+
rgba: 'rgba',
|
|
900
|
+
};
|
|
901
|
+
|
|
856
902
|
/**
|
|
857
903
|
* The three constraint families a `MotionTrack` can target, and the table of
|
|
858
904
|
* timelines each one accepts.
|
|
@@ -4686,6 +4732,42 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
4686
4732
|
// time = to + (value - from) * 0, so the slider holds one frame forever.
|
|
4687
4733
|
throw new CompileError(`${where}: scale is 0, so the bone's property cannot move the slider's time at all`);
|
|
4688
4734
|
}
|
|
4735
|
+
// The window of driving values that can reach a frame at all, and the two
|
|
4736
|
+
// functions every figure in the two reader clauses below comes off.
|
|
4737
|
+
//
|
|
4738
|
+
// ⭐ **One copy, read by both readers** (issue #657). The circle owned
|
|
4739
|
+
// this arithmetic while it was the only reader with a bound; the world
|
|
4740
|
+
// `scale` readers have a floor at 0 and the same mapping carries them
|
|
4741
|
+
// there, so the second clause reads these numbers instead of computing its
|
|
4742
|
+
// own. That is the rule this clause has already paid for twice — #417
|
|
4743
|
+
// tested one end because its fixture only left the circle at that end, and
|
|
4744
|
+
// #431 wrapped by one subtraction because every fixture sat within a turn
|
|
4745
|
+
// — and a second copy with a sign edited is exactly the shape both took.
|
|
4746
|
+
const fromValue = spec.from === undefined ? 0 : needNumber(spec.from, 'from');
|
|
4747
|
+
const toTime = spec.to === undefined ? 0 : needNumber(spec.to, 'to');
|
|
4748
|
+
const perUnit = spec.scale === undefined ? 1 : needNumber(spec.scale, 'scale');
|
|
4749
|
+
const duration = ctx.animationDurations.get(animation) ?? 0;
|
|
4750
|
+
/** The driving value that maps to the animation's first frame, and to its last. */
|
|
4751
|
+
const atStart = fromValue - toTime / perUnit;
|
|
4752
|
+
const atEnd = fromValue + (duration - toTime) / perUnit;
|
|
4753
|
+
const lowest = Math.min(atStart, atEnd);
|
|
4754
|
+
const highest = Math.max(atStart, atEnd);
|
|
4755
|
+
/**
|
|
4756
|
+
* The time this mapping puts a driving value at, before the runtime
|
|
4757
|
+
* touches it — the one arithmetic every figure below comes off.
|
|
4758
|
+
*
|
|
4759
|
+
* ⚠️ **Computed, not asserted** (issue #423). The circle's clause used to
|
|
4760
|
+
* end *"— outside the animation's Ds. With `loop`: false that is
|
|
4761
|
+
* `Math.max(0, time)` holding the last frame; with `loop`: true it wraps
|
|
4762
|
+
* to some other frame"*, which states a consequence rather than measuring
|
|
4763
|
+
* one — and is flatly false for a range spanning a full turn, where the
|
|
4764
|
+
* wrapped reading lands INSIDE the animation. It also printed both loop
|
|
4765
|
+
* modes and left the reader to pick. A message that hedges is a message
|
|
4766
|
+
* that has not measured.
|
|
4767
|
+
*/
|
|
4768
|
+
const timeAt = (value: number): number => toTime + (value - fromValue) * perUnit;
|
|
4769
|
+
/** That time clamped into the animation — the frame a pose actually holds. */
|
|
4770
|
+
const intoFrame = (time: number): number => Math.min(Math.max(time, 0), duration);
|
|
4689
4771
|
// ⚠️ `local: false` reads the bone's WORLD rotation, and `FromRotate.value`
|
|
4690
4772
|
// (`TransformConstraintData.js`) ends
|
|
4691
4773
|
//
|
|
@@ -4741,15 +4823,6 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
4741
4823
|
// or the whole turn 0°..360° — is how you write this axis under
|
|
4742
4824
|
// `local: false` and it works.
|
|
4743
4825
|
if (property === 'rotate' && spec.local !== true) {
|
|
4744
|
-
const fromValue = spec.from === undefined ? 0 : needNumber(spec.from, 'from');
|
|
4745
|
-
const toTime = spec.to === undefined ? 0 : needNumber(spec.to, 'to');
|
|
4746
|
-
const perUnit = spec.scale === undefined ? 1 : needNumber(spec.scale, 'scale');
|
|
4747
|
-
const duration = ctx.animationDurations.get(animation) ?? 0;
|
|
4748
|
-
/** The driving value that maps to the animation's first frame, and to its last. */
|
|
4749
|
-
const atStart = fromValue - toTime / perUnit;
|
|
4750
|
-
const atEnd = fromValue + (duration - toTime) / perUnit;
|
|
4751
|
-
const lowest = Math.min(atStart, atEnd);
|
|
4752
|
-
const highest = Math.max(atStart, atEnd);
|
|
4753
4826
|
// Which end of the circle the range leaves. One record, so the end, its
|
|
4754
4827
|
// reading, the time it lands on and the two clauses that name the side
|
|
4755
4828
|
// are derived once rather than twice.
|
|
@@ -4805,20 +4878,6 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
4805
4878
|
* tracks the closed form to 3.3e-8s.
|
|
4806
4879
|
*/
|
|
4807
4880
|
const readAs = ((dead.end % 360) + 360) % 360;
|
|
4808
|
-
/**
|
|
4809
|
-
* The time this mapping puts a driving value at, before the runtime
|
|
4810
|
-
* touches it — the one arithmetic every figure below comes off.
|
|
4811
|
-
*
|
|
4812
|
-
* ⚠️ **Computed, not asserted** (issue #423). This clause used to end
|
|
4813
|
-
* *"— outside the animation's Ds. With `loop`: false that is
|
|
4814
|
-
* `Math.max(0, time)` holding the last frame; with `loop`: true it
|
|
4815
|
-
* wraps to some other frame"*, which states a consequence rather than
|
|
4816
|
-
* measuring one — and is flatly false for a range spanning a full
|
|
4817
|
-
* turn, where the wrapped reading lands INSIDE the animation. It also
|
|
4818
|
-
* printed both loop modes and left the reader to pick. A message that
|
|
4819
|
-
* hedges is a message that has not measured.
|
|
4820
|
-
*/
|
|
4821
|
-
const timeAt = (value: number): number => toTime + (value - fromValue) * perUnit;
|
|
4822
4881
|
const lands = timeAt(readAs);
|
|
4823
4882
|
// `[0, 360)` is the whole of what `FromRotate.value` returns (issue
|
|
4824
4883
|
// #417), so the readings that reach the animation at all are that
|
|
@@ -4829,7 +4888,6 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
4829
4888
|
const reachLo = Math.max(0, lowest);
|
|
4830
4889
|
const reachHi = Math.min(360, highest);
|
|
4831
4890
|
const reaches = reachLo <= reachHi;
|
|
4832
|
-
const intoFrame = (time: number): number => Math.min(Math.max(time, 0), duration);
|
|
4833
4891
|
const reachA = intoFrame(timeAt(reachLo));
|
|
4834
4892
|
const reachB = intoFrame(timeAt(reachHi));
|
|
4835
4893
|
const span = `${Math.min(reachA, reachB).toFixed(3)}s..${Math.max(reachA, reachB).toFixed(3)}s`;
|
|
@@ -4923,6 +4981,78 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
|
|
|
4923
4981
|
);
|
|
4924
4982
|
}
|
|
4925
4983
|
}
|
|
4984
|
+
// ⚠️ `local: false` reads the bone's WORLD scale, and `FromScaleX.value`
|
|
4985
|
+
// / `FromScaleY.value` (`TransformConstraintData.js`) are
|
|
4986
|
+
//
|
|
4987
|
+
// const a = source.a / skeleton.scaleX, c = source.c / skeleton.scaleY;
|
|
4988
|
+
// return Math.sqrt(a * a + c * c) + offsets[TransformConstraintData.SCALEX];
|
|
4989
|
+
//
|
|
4990
|
+
// — a MAGNITUDE. The driven field is in both terms, so the reading is
|
|
4991
|
+
// `|value|` and `[0, ∞)` is the whole of what that reader can return. The
|
|
4992
|
+
// floor is REACHED rather than approached (a bone whose own scale, or
|
|
4993
|
+
// whose parent's, is 0 reads exactly 0), which is why a range whose bottom
|
|
4994
|
+
// is exactly 0 stays legal and only one that dips below it is refused.
|
|
4995
|
+
//
|
|
4996
|
+
// 🚨 **Below the floor the axis does not go dead, it FOLDS** (issue #657),
|
|
4997
|
+
// and that is why this is a second clause rather than the circle with
|
|
4998
|
+
// another property name in it. A reading the range cannot reach is one
|
|
4999
|
+
// frame pinned; a reading it reaches TWICE is two dial positions posing
|
|
5000
|
+
// the same face, so the message has to name the mirror rather than a dead
|
|
5001
|
+
// arc. Measured through spine-core on `from: 0, to: 0.5, scale: 0.25` over
|
|
5002
|
+
// a 1 s animation — driving window −2..+2 — the applied time at −2.000,
|
|
5003
|
+
// −1.500, −1.000 and −0.500 is 1.000000s, 0.875000s, 0.750000s and
|
|
5004
|
+
// 0.625000s: the same six decimals the dial at +2.000, +1.500, +1.000 and
|
|
5005
|
+
// +0.500 applies, with the posed bone matching to the digit. The same rig
|
|
5006
|
+
// read `local: true` sweeps −2 → +2 monotonically from 0.000000s, which is
|
|
5007
|
+
// what makes that repair worth naming.
|
|
5008
|
+
//
|
|
5009
|
+
// ⭐ **No loop branch, and that is measured rather than economised.** The
|
|
5010
|
+
// circle's consequence turns on `Slider.loop` because a held frame is held
|
|
5011
|
+
// only under `Math.max(0, time)`; a fold is not a clamp, so `loop: true`
|
|
5012
|
+
// cannot undo it — the same ±1.500 pair applies 1.875000s either way.
|
|
5013
|
+
if ((property === 'scaleX' || property === 'scaleY') && spec.local !== true && lowest < -SLIDER_WRAP_SLACK) {
|
|
5014
|
+
/** What the reader returns for a bone parked at the bottom of the range: the magnitude. */
|
|
5015
|
+
const readAs = Math.abs(lowest);
|
|
5016
|
+
const lands = timeAt(readAs);
|
|
5017
|
+
// The readings that reach the animation are `[0, ∞)` met with the
|
|
5018
|
+
// driving window — the circle's `reachLo`/`reachHi` with the ceiling
|
|
5019
|
+
// taken out, off the same two numbers the message has already printed.
|
|
5020
|
+
const reaches = highest >= 0;
|
|
5021
|
+
/**
|
|
5022
|
+
* How much of the range lies below the floor: the WIDTH of
|
|
5023
|
+
* `[lowest, highest]` under 0, not the distance to its far end.
|
|
5024
|
+
*
|
|
5025
|
+
* 🚨 `-lowest` is that distance, and the two are equal only while the
|
|
5026
|
+
* range STRADDLES the floor — the same trap issue #434 paid for one
|
|
5027
|
+
* reader over, where a range lying wholly outside was told a width wider
|
|
5028
|
+
* than itself. A `-6..-2` window is 4.000 below the floor and `-lowest`
|
|
5029
|
+
* would print 6.000, which is `readAs` again with a different name on it.
|
|
5030
|
+
*/
|
|
5031
|
+
const below = Math.min(0, highest) - lowest;
|
|
5032
|
+
// The arc an author writes twice: the part of the range above 0 that the
|
|
5033
|
+
// part below 0 mirrors onto. Both ends are inside `[lowest, highest]` —
|
|
5034
|
+
// 0 because the range straddles it and this is the `reaches` branch, the
|
|
5035
|
+
// top because it is `highest` or less — so both map INTO the animation
|
|
5036
|
+
// and neither needs clamping.
|
|
5037
|
+
const mirrorTop = Math.min(highest, -lowest);
|
|
5038
|
+
const mirrorA = timeAt(0);
|
|
5039
|
+
const mirrorB = timeAt(mirrorTop);
|
|
5040
|
+
const consequence = reaches
|
|
5041
|
+
? `${below.toFixed(3)} of the range below 0 repeats 0.000..${mirrorTop.toFixed(3)}, which is ` +
|
|
5042
|
+
`${Math.min(mirrorA, mirrorB).toFixed(3)}s..${Math.max(mirrorA, mirrorB).toFixed(3)}s of the animation, in reverse.`
|
|
5043
|
+
: `the whole ${below.toFixed(3)} of this range is below 0, so it reaches none of the animation's ${duration}s — ` +
|
|
5044
|
+
`every value the reader can return maps past it and the pose holds the frame at ${intoFrame(timeAt(0)).toFixed(3)}s.`;
|
|
5045
|
+
throw new CompileError(
|
|
5046
|
+
`${where}: drives off bone "${String(spec.bone)}" ${property} with "local": false, and the driving values ` +
|
|
5047
|
+
`that reach animation "${animation}" (0s..${duration}s) run from ${lowest.toFixed(3)} to ${highest.toFixed(3)}. ` +
|
|
5048
|
+
`A world scale is read through \`From${property === 'scaleX' ? 'ScaleX' : 'ScaleY'}.value\` as ` +
|
|
5049
|
+
`\`Math.sqrt(${property === 'scaleX' ? 'a² + c²' : 'b² + d²'})\`, a magnitude, so the bone at ` +
|
|
5050
|
+
`${lowest.toFixed(3)} is read as ${readAs.toFixed(3)} and maps to time ${lands.toFixed(3)}s — the time the ` +
|
|
5051
|
+
`bone at ${readAs.toFixed(3)} maps to. Positions below 0 read as the mirror of positions above it: ` +
|
|
5052
|
+
`${consequence} Nothing at runtime reports it. Add \`"local": true\` to read the bone's own scale signed and ` +
|
|
5053
|
+
'unfolded — that is the form a squash axis wants — or move the range so it does not dip below 0.',
|
|
5054
|
+
);
|
|
5055
|
+
}
|
|
4926
5056
|
for (const field of timeSide) {
|
|
4927
5057
|
if (spec[field] !== undefined) {
|
|
4928
5058
|
throw new CompileError(
|
|
@@ -6371,18 +6501,59 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
|
|
|
6371
6501
|
throw new CompileError(`animation "${animName}": "${track.property}" is a bone track but no bone is named`);
|
|
6372
6502
|
}
|
|
6373
6503
|
if (!isBoneTrack && track.bone) {
|
|
6374
|
-
|
|
6504
|
+
// The bone family's sibling of `compileTrack`'s slot refusal, raised here for
|
|
6505
|
+
// the same reason it is raised there: before any key is shaped, and printed
|
|
6506
|
+
// from the dispatch table itself. What it replaces named the SLOT family for
|
|
6507
|
+
// a property that is usually in no family at all, and enumerated nothing
|
|
6508
|
+
// (issue #656). The tail clause is the one case where the old sentence was
|
|
6509
|
+
// true — `rgba` and `attachment` really are slot timelines — so the redirect
|
|
6510
|
+
// survives as a clause instead of as the whole message, and it is read off
|
|
6511
|
+
// `SLOT_TRACKS` rather than spelled again. A constraint property never
|
|
6512
|
+
// reaches here: `owning` above refuses it with the field that carries it.
|
|
6513
|
+
throw new CompileError(
|
|
6514
|
+
`animation "${animName}" bone "${track.bone}" has no timeline "${track.property}" ` +
|
|
6515
|
+
`(it has: ${Object.keys(BONE_TRACKS).join(', ')})` +
|
|
6516
|
+
(track.property in SLOT_TRACKS ? `. "${track.property}" is a slot timeline — put the name in "slot"` : ''),
|
|
6517
|
+
);
|
|
6375
6518
|
}
|
|
6376
6519
|
if (track.bone) return [track.bone];
|
|
6377
6520
|
if (track.slot) return [track.slot];
|
|
6378
6521
|
if (track.group) {
|
|
6379
|
-
// A group's members are bones or
|
|
6380
|
-
// what lets `stagger` express
|
|
6381
|
-
//
|
|
6382
|
-
//
|
|
6383
|
-
//
|
|
6522
|
+
// A group's members are bones, slots or physics constraints depending on the
|
|
6523
|
+
// property, which is what lets `stagger` express a lag across them: four
|
|
6524
|
+
// members, one track, a few frames apart — the difference between a ring
|
|
6525
|
+
// following the part and two objects moving together, which reads as a
|
|
6526
|
+
// composite.
|
|
6384
6527
|
const members = motion.groups?.[track.group];
|
|
6385
6528
|
if (!members) throw new CompileError(`animation "${animName}": unknown group "${track.group}"`);
|
|
6529
|
+
// 🚨 The group is the one target shape whose FAMILY is decided by the
|
|
6530
|
+
// property, and three tables decide it: `constraintFamilyOf` asks
|
|
6531
|
+
// `property in PHYSICS_TRACKS`, this function asks `property in BONE_TRACKS`,
|
|
6532
|
+
// and what neither claims falls through to the slot branch. So a property in
|
|
6533
|
+
// no table left the dispatch with no family at all and the track was read as
|
|
6534
|
+
// a slot track — the first member was then refused for not being a slot
|
|
6535
|
+
// (`animation "A" targets unknown slot "rim_grip_a"` on a group of bones and
|
|
6536
|
+
// a misspelled bone property), or, when the members really were slots, with
|
|
6537
|
+
// the slot table alone on a family nothing had determined (issue #661).
|
|
6538
|
+
//
|
|
6539
|
+
// Raised AFTER the group's own existence check, because "group G has no
|
|
6540
|
+
// timeline P" would otherwise assert a group the file does not declare, and
|
|
6541
|
+
// BEFORE the members are resolved against the rig or any key is shaped: the
|
|
6542
|
+
// property is what the dispatch reads first, so it is what the refusal is
|
|
6543
|
+
// about. All three lists come from the objects the dispatch reads, so none
|
|
6544
|
+
// of them can drift from the emitter that owns it. A property that IS in
|
|
6545
|
+
// `PHYSICS_TRACKS` never reaches this line — `constraintFamilyOf` returns
|
|
6546
|
+
// `physics` for a group track that names one, and the family branch above
|
|
6547
|
+
// has already returned — and a path or slider property is refused further up
|
|
6548
|
+
// with the field its constraint's name goes in.
|
|
6549
|
+
if (!(track.property in BONE_TRACKS) && !(track.property in SLOT_TRACKS)) {
|
|
6550
|
+
throw new CompileError(
|
|
6551
|
+
`animation "${animName}" group "${track.group}" has no timeline "${track.property}" ` +
|
|
6552
|
+
`(a bone group has: ${Object.keys(BONE_TRACKS).join(', ')}; ` +
|
|
6553
|
+
`a slot group has: ${Object.keys(SLOT_TRACKS).join(', ')}; ` +
|
|
6554
|
+
`a ${CONSTRAINT_TRACK_FAMILIES.physics.label} group has: ${Object.keys(PHYSICS_TRACKS).join(', ')})`,
|
|
6555
|
+
);
|
|
6556
|
+
}
|
|
6386
6557
|
return members;
|
|
6387
6558
|
}
|
|
6388
6559
|
throw new CompileError(`animation "${animName}": a track targets neither slot nor group`);
|
|
@@ -6601,6 +6772,16 @@ function compileTrack(
|
|
|
6601
6772
|
skinAttachments: Record<string, Record<string, SpineAttachment>>,
|
|
6602
6773
|
): SpineTimelineKey[] {
|
|
6603
6774
|
const where = `animation "${animName}" slot "${target}" ${track.property}`;
|
|
6775
|
+
// Before any key is shaped, and before the empty-track refusal: a property
|
|
6776
|
+
// the emitter has no branch for is the fault, and a track that names one has
|
|
6777
|
+
// no shape to be missing keys from (issue #650).
|
|
6778
|
+
const shape = SLOT_TRACKS[track.property];
|
|
6779
|
+
if (shape === undefined) {
|
|
6780
|
+
throw new CompileError(
|
|
6781
|
+
`animation "${animName}" slot "${target}" has no timeline "${track.property}" ` +
|
|
6782
|
+
`(it has: ${Object.keys(SLOT_TRACKS).join(', ')})`,
|
|
6783
|
+
);
|
|
6784
|
+
}
|
|
6604
6785
|
if (!track.keys.length) throw new CompileError(`${where}: no keys`);
|
|
6605
6786
|
|
|
6606
6787
|
const out: SpineTimelineKey[] = [];
|
|
@@ -6613,7 +6794,7 @@ function compileTrack(
|
|
|
6613
6794
|
}
|
|
6614
6795
|
checkKeyTime(where, time, key.t, duration);
|
|
6615
6796
|
|
|
6616
|
-
if (
|
|
6797
|
+
if (shape === 'attachment') {
|
|
6617
6798
|
if (key.v !== null && typeof key.v !== 'string') {
|
|
6618
6799
|
throw new CompileError(`${where}: attachment key value must be a string or null`);
|
|
6619
6800
|
}
|
|
@@ -6626,7 +6807,8 @@ function compileTrack(
|
|
|
6626
6807
|
continue;
|
|
6627
6808
|
}
|
|
6628
6809
|
|
|
6629
|
-
// rgba
|
|
6810
|
+
// rgba — the other shape `SLOT_TRACKS` names, and now the only way to reach
|
|
6811
|
+
// this branch: a property the table does not carry was refused above.
|
|
6630
6812
|
if (!Array.isArray(key.v)) throw new CompileError(`${where}: rgba key value must be [r,g,b,a]`);
|
|
6631
6813
|
const entry: SpineTimelineKey = { time, color: rgbaHex(key.v) };
|
|
6632
6814
|
if (key.ease !== undefined && key.curve !== undefined) {
|
package/src/ingest.ts
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* break `A18_DETERMINISTIC_EMIT` the first time anybody rebuilt from an ingested
|
|
45
45
|
* spec.
|
|
46
46
|
*/
|
|
47
|
-
import { SPINE_VERSION } from './compile.ts';
|
|
47
|
+
import { SLOT_TRACKS as EMITTED_SLOT_TRACKS, SPINE_VERSION } from './compile.ts';
|
|
48
48
|
import { MOTION_SPEC_VERSION, parseMotionSpec } from './motion.ts';
|
|
49
49
|
import { parseRigSpec, RIG_KEYS, RIG_SPEC_VERSION, type RigSpec } from './rig.ts';
|
|
50
50
|
import type { MotionSpec } from './types.ts';
|
|
@@ -347,8 +347,18 @@ const HEADER_REDERIVED = ['spine'];
|
|
|
347
347
|
/** The attachment types this module inverts. Everything else is refused by name. */
|
|
348
348
|
const ATTACHMENT_TYPES = ['region', 'mesh', 'boundingbox', 'clipping', 'path'];
|
|
349
349
|
|
|
350
|
-
/**
|
|
351
|
-
|
|
350
|
+
/**
|
|
351
|
+
* The slot timelines the motion spec carries — `compileTrack`'s own table,
|
|
352
|
+
* rather than a second list of the same two names.
|
|
353
|
+
*
|
|
354
|
+
* ⚠️ It was that second list until issue #650, spelled `['rgba', 'attachment']`
|
|
355
|
+
* with a comment saying where it had been copied from. The copy was true, which
|
|
356
|
+
* is the point: the emitter had no list at all — one `if` and a fall-through —
|
|
357
|
+
* so this module's blocker was the only place in `src/` that said what a slot
|
|
358
|
+
* track may be, and it said it about a compiler that accepted anything. Now
|
|
359
|
+
* there is one list and both sides read it.
|
|
360
|
+
*/
|
|
361
|
+
const SLOT_TRACKS = Object.keys(EMITTED_SLOT_TRACKS);
|
|
352
362
|
|
|
353
363
|
/**
|
|
354
364
|
* Everything this module has a branch for, as the branches themselves state it.
|