spine-rigc 0.25.3 → 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/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] },
@@ -4718,6 +4732,42 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
4718
4732
  // time = to + (value - from) * 0, so the slider holds one frame forever.
4719
4733
  throw new CompileError(`${where}: scale is 0, so the bone's property cannot move the slider's time at all`);
4720
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);
4721
4771
  // ⚠️ `local: false` reads the bone's WORLD rotation, and `FromRotate.value`
4722
4772
  // (`TransformConstraintData.js`) ends
4723
4773
  //
@@ -4773,15 +4823,6 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
4773
4823
  // or the whole turn 0°..360° — is how you write this axis under
4774
4824
  // `local: false` and it works.
4775
4825
  if (property === 'rotate' && spec.local !== true) {
4776
- const fromValue = spec.from === undefined ? 0 : needNumber(spec.from, 'from');
4777
- const toTime = spec.to === undefined ? 0 : needNumber(spec.to, 'to');
4778
- const perUnit = spec.scale === undefined ? 1 : needNumber(spec.scale, 'scale');
4779
- const duration = ctx.animationDurations.get(animation) ?? 0;
4780
- /** The driving value that maps to the animation's first frame, and to its last. */
4781
- const atStart = fromValue - toTime / perUnit;
4782
- const atEnd = fromValue + (duration - toTime) / perUnit;
4783
- const lowest = Math.min(atStart, atEnd);
4784
- const highest = Math.max(atStart, atEnd);
4785
4826
  // Which end of the circle the range leaves. One record, so the end, its
4786
4827
  // reading, the time it lands on and the two clauses that name the side
4787
4828
  // are derived once rather than twice.
@@ -4837,20 +4878,6 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
4837
4878
  * tracks the closed form to 3.3e-8s.
4838
4879
  */
4839
4880
  const readAs = ((dead.end % 360) + 360) % 360;
4840
- /**
4841
- * The time this mapping puts a driving value at, before the runtime
4842
- * touches it — the one arithmetic every figure below comes off.
4843
- *
4844
- * ⚠️ **Computed, not asserted** (issue #423). This clause used to end
4845
- * *"— outside the animation's Ds. With `loop`: false that is
4846
- * `Math.max(0, time)` holding the last frame; with `loop`: true it
4847
- * wraps to some other frame"*, which states a consequence rather than
4848
- * measuring one — and is flatly false for a range spanning a full
4849
- * turn, where the wrapped reading lands INSIDE the animation. It also
4850
- * printed both loop modes and left the reader to pick. A message that
4851
- * hedges is a message that has not measured.
4852
- */
4853
- const timeAt = (value: number): number => toTime + (value - fromValue) * perUnit;
4854
4881
  const lands = timeAt(readAs);
4855
4882
  // `[0, 360)` is the whole of what `FromRotate.value` returns (issue
4856
4883
  // #417), so the readings that reach the animation at all are that
@@ -4861,7 +4888,6 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
4861
4888
  const reachLo = Math.max(0, lowest);
4862
4889
  const reachHi = Math.min(360, highest);
4863
4890
  const reaches = reachLo <= reachHi;
4864
- const intoFrame = (time: number): number => Math.min(Math.max(time, 0), duration);
4865
4891
  const reachA = intoFrame(timeAt(reachLo));
4866
4892
  const reachB = intoFrame(timeAt(reachHi));
4867
4893
  const span = `${Math.min(reachA, reachB).toFixed(3)}s..${Math.max(reachA, reachB).toFixed(3)}s`;
@@ -4955,6 +4981,78 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): S
4955
4981
  );
4956
4982
  }
4957
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
+ }
4958
5056
  for (const field of timeSide) {
4959
5057
  if (spec[field] !== undefined) {
4960
5058
  throw new CompileError(
@@ -6403,18 +6501,59 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
6403
6501
  throw new CompileError(`animation "${animName}": "${track.property}" is a bone track but no bone is named`);
6404
6502
  }
6405
6503
  if (!isBoneTrack && track.bone) {
6406
- throw new CompileError(`animation "${animName}": bone "${track.bone}" cannot take slot property "${track.property}"`);
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
+ );
6407
6518
  }
6408
6519
  if (track.bone) return [track.bone];
6409
6520
  if (track.slot) return [track.slot];
6410
6521
  if (track.group) {
6411
- // A group's members are bones or slots depending on the property, which is
6412
- // what lets `stagger` express the ring lag: four grips, one track, a few
6413
- // frames apart. Plan 02 section 4-2 calls that lag the real detail of the
6414
- // stroke, and it is the difference between a ring following the part and two
6415
- // objects moving together (which reads as a composite).
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.
6416
6527
  const members = motion.groups?.[track.group];
6417
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
+ }
6418
6557
  return members;
6419
6558
  }
6420
6559
  throw new CompileError(`animation "${animName}": a track targets neither slot nor group`);