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/validate.ts
CHANGED
|
@@ -22,16 +22,19 @@ import {
|
|
|
22
22
|
type BoneData,
|
|
23
23
|
BoundingBoxAttachment,
|
|
24
24
|
ClippingAttachment,
|
|
25
|
+
type ConstraintTimeline,
|
|
25
26
|
DeformTimeline,
|
|
26
27
|
isBoneTimeline,
|
|
27
28
|
isConstraintTimeline,
|
|
28
29
|
isSlotTimeline,
|
|
29
30
|
MeshAttachment,
|
|
31
|
+
MixFrom,
|
|
30
32
|
PathAttachment,
|
|
31
33
|
PathConstraintData,
|
|
32
34
|
Physics,
|
|
33
35
|
PhysicsConstraintData,
|
|
34
36
|
PhysicsConstraintPose,
|
|
37
|
+
PhysicsConstraintResetTimeline,
|
|
35
38
|
PhysicsConstraintTimeline,
|
|
36
39
|
Property,
|
|
37
40
|
RegionAttachment,
|
|
@@ -181,6 +184,7 @@ const ASSERTION_KIND: Record<string, 'validity' | 'renderer' | 'archetype'> = {
|
|
|
181
184
|
A39_DEFORM_KEEPS_TRIANGLE_WINDING: 'archetype',
|
|
182
185
|
A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET: 'validity',
|
|
183
186
|
A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP: 'validity',
|
|
187
|
+
A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER: 'validity',
|
|
184
188
|
};
|
|
185
189
|
|
|
186
190
|
/**
|
|
@@ -662,6 +666,176 @@ function deformArrayLength(att: Json): number | null {
|
|
|
662
666
|
return influences * 2;
|
|
663
667
|
}
|
|
664
668
|
|
|
669
|
+
/**
|
|
670
|
+
* What one timeline's own `apply` does with the `add` argument.
|
|
671
|
+
*
|
|
672
|
+
* - `accumulates` — a second application adds to the first, so two sliders both
|
|
673
|
+
* `additive` compose on it.
|
|
674
|
+
* - `overwrites` — it writes its value whatever `add` says, so the later slider
|
|
675
|
+
* owns the property and `"additive": true` is not the repair.
|
|
676
|
+
* - `inert` — applied with the arguments a slider passes it changes nothing a
|
|
677
|
+
* pose holds, so there is nothing for a second slider to erase.
|
|
678
|
+
*/
|
|
679
|
+
export type TimelineAddBehaviour = 'accumulates' | 'overwrites' | 'inert';
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* The displacement the second start state carries, chosen to be exact in binary
|
|
683
|
+
* floating point so the probe adds no rounding of its own.
|
|
684
|
+
*/
|
|
685
|
+
const PROBE_DISPLACEMENT = 0.375;
|
|
686
|
+
|
|
687
|
+
/** Every own value of a pose object a timeline could have written, as text. */
|
|
688
|
+
function poseReading(pose: object): string {
|
|
689
|
+
const parts: string[] = [];
|
|
690
|
+
for (const key of Object.keys(pose).sort()) {
|
|
691
|
+
const value = (pose as Record<string, unknown>)[key];
|
|
692
|
+
if (value === null || value === undefined || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') {
|
|
693
|
+
parts.push(`${key}=${String(value)}`);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (Array.isArray(value)) {
|
|
697
|
+
const entries: unknown[] = value;
|
|
698
|
+
if (entries.every((one) => typeof one === 'number')) parts.push(`${key}=[${entries.join(',')}]`);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (typeof value !== 'object') continue;
|
|
702
|
+
const record = value as Record<string, unknown>;
|
|
703
|
+
const keys = Object.keys(record).sort();
|
|
704
|
+
// A colour, or anything else built only of numbers. Everything with a
|
|
705
|
+
// structure — a bone back-reference, a slot's data — is either an identity
|
|
706
|
+
// (read by name below) or something no timeline writes.
|
|
707
|
+
if (keys.length > 0 && keys.every((one) => typeof record[one] === 'number')) {
|
|
708
|
+
parts.push(`${key}={${keys.map((one) => `${one}:${String(record[one])}`).join(',')}}`);
|
|
709
|
+
} else if (typeof record.name === 'string') {
|
|
710
|
+
parts.push(`${key}=@${record.name}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return parts.join(',');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** The whole of what a timeline could have changed, in one comparable string. */
|
|
717
|
+
function skeletonReading(skeleton: Skeleton): string {
|
|
718
|
+
const parts: string[] = [];
|
|
719
|
+
for (const bone of skeleton.bones) parts.push(poseReading(bone.appliedPose));
|
|
720
|
+
for (const slot of skeleton.slots) parts.push(poseReading(slot.appliedPose));
|
|
721
|
+
for (const constraint of skeleton.constraints) parts.push(poseReading(constraint.appliedPose as object));
|
|
722
|
+
parts.push(skeleton.drawOrder.appliedPose.map((slot) => slot.data.name).join('>'));
|
|
723
|
+
return parts.join('|');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* The two start states a probe is taken from, in order: the setup pose, and the
|
|
728
|
+
* setup pose displaced.
|
|
729
|
+
*
|
|
730
|
+
* ⚠️ Both are needed and neither is redundant. From the **setup** pose a
|
|
731
|
+
* `DeformTimeline` is live, because its `apply` is gated on the slot still
|
|
732
|
+
* holding the attachment the timeline names. From the **displaced** pose a
|
|
733
|
+
* timeline whose every key states its target's own setup value is still seen to
|
|
734
|
+
* write, which from the setup pose alone would read as `inert` — a later slider
|
|
735
|
+
* keying a slot's colour to exactly the colour it already has erases an earlier
|
|
736
|
+
* one just the same.
|
|
737
|
+
*/
|
|
738
|
+
function probeStartStates(skeleton: Skeleton): Array<() => void> {
|
|
739
|
+
return [
|
|
740
|
+
() => undefined,
|
|
741
|
+
() => {
|
|
742
|
+
for (const bone of skeleton.bones) displacePose(bone.appliedPose);
|
|
743
|
+
for (const slot of skeleton.slots) {
|
|
744
|
+
displacePose(slot.appliedPose);
|
|
745
|
+
slot.appliedPose.setAttachment(null);
|
|
746
|
+
}
|
|
747
|
+
for (const constraint of skeleton.constraints) displacePose(constraint.appliedPose as object);
|
|
748
|
+
skeleton.drawOrder.appliedPose.push(...skeleton.drawOrder.appliedPose.splice(0, 1));
|
|
749
|
+
},
|
|
750
|
+
];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Move every number a pose holds, so "it wrote nothing" cannot mean "it wrote what was there". */
|
|
754
|
+
function displacePose(pose: object): void {
|
|
755
|
+
for (const key of Object.keys(pose)) {
|
|
756
|
+
const value = (pose as Record<string, unknown>)[key];
|
|
757
|
+
if (typeof value === 'number') {
|
|
758
|
+
(pose as Record<string, number>)[key] = value + PROBE_DISPLACEMENT;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
762
|
+
const record = value as Record<string, unknown>;
|
|
763
|
+
const keys = Object.keys(record);
|
|
764
|
+
if (keys.length > 0 && keys.every((one) => typeof record[one] === 'number')) {
|
|
765
|
+
for (const one of keys) (record as Record<string, number>)[one] += PROBE_DISPLACEMENT;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Every time the timeline's own frames name, the midpoints between them, and one past the end. */
|
|
771
|
+
function probeTimes(timeline: Timeline): number[] {
|
|
772
|
+
const stride = timeline.getFrameEntries();
|
|
773
|
+
const frames = timeline.frames;
|
|
774
|
+
const at: number[] = [];
|
|
775
|
+
for (let i = 0; i < frames.length; i += stride) at.push(frames[i]);
|
|
776
|
+
const between: number[] = [];
|
|
777
|
+
for (let i = 1; i < at.length; i++) between.push((at[i - 1] + at[i]) / 2);
|
|
778
|
+
return [...at, ...between, (at.length ? at[at.length - 1] : 0) + 1];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* 🚨 **What `apply` does with `add`, posed rather than read off a flag.**
|
|
783
|
+
*
|
|
784
|
+
* `Timeline.additive` is the runtime's own declaration that a class "supports
|
|
785
|
+
* being applied additively", and for two classes it is not what the class does:
|
|
786
|
+
* `PathConstraintMixTimeline` and `SliderTimeline` declare `false` and pass the
|
|
787
|
+
* `add` argument straight through anyway. A40 read the flag, so it refused two
|
|
788
|
+
* correct rigs with a sentence about the runtime the runtime does not perform
|
|
789
|
+
* (issue #655, measured by `PS143` over all thirty spellings of the motion
|
|
790
|
+
* vocabulary).
|
|
791
|
+
*
|
|
792
|
+
* So the answer is taken from the object instead. The timeline is applied with
|
|
793
|
+
* exactly the arguments `Slider.update` passes — `firedEvents` null, `alpha` 1,
|
|
794
|
+
* `MixFrom.current`, `appliedPose` true — **twice**, at every time its own
|
|
795
|
+
* frames name. A second application that moves the pose again is a class that
|
|
796
|
+
* honours `add`; one that lands on the same value is a class that writes
|
|
797
|
+
* outright; one that never moves the pose at all writes nothing a second slider
|
|
798
|
+
* could erase, which is what an events timeline under a slider *is*.
|
|
799
|
+
*
|
|
800
|
+
* ⛔ Rejected: reading the source of `apply` (not a measurement of anything, and
|
|
801
|
+
* a class whose body changes under a runtime bump would still read the old
|
|
802
|
+
* answer), and a table of the two disagreeing classes written here (the same
|
|
803
|
+
* hand-kept list this repository has a judgment about — it would have been
|
|
804
|
+
* written after the census found two and been wrong at the third).
|
|
805
|
+
*
|
|
806
|
+
* ⚠️ **The skeleton wears every skin in turn, and that is not thoroughness.**
|
|
807
|
+
* `Skeleton.updateCache` leaves a `skinRequired` constraint inactive under the
|
|
808
|
+
* skins that do not list it, and an inactive target makes ANY timeline read
|
|
809
|
+
* `inert` — a fact about that skin, not about the class. Reading one skin would
|
|
810
|
+
* put a whole class in the third state and quietly stop refusing pairs that
|
|
811
|
+
* share it, which is this repository's worst failure shape: a gate that looks
|
|
812
|
+
* kept while checking nothing. The strongest verdict any skin produces wins.
|
|
813
|
+
*/
|
|
814
|
+
export function timelineAddBehaviour(data: ReturnType<SkeletonJson['readSkeletonData']>, timeline: Timeline): TimelineAddBehaviour {
|
|
815
|
+
let wrote = false;
|
|
816
|
+
for (const skin of [null, ...data.skins]) {
|
|
817
|
+
const skeleton = new Skeleton(data);
|
|
818
|
+
if (skin !== null) skeleton.setSkin(skin);
|
|
819
|
+
for (const displace of probeStartStates(skeleton)) {
|
|
820
|
+
for (const time of probeTimes(timeline)) {
|
|
821
|
+
skeleton.setupPose();
|
|
822
|
+
for (const bone of skeleton.bones) bone.resetConstrained();
|
|
823
|
+
for (const slot of skeleton.slots) slot.resetConstrained();
|
|
824
|
+
for (const constraint of skeleton.constraints) constraint.resetConstrained();
|
|
825
|
+
skeleton.drawOrder.resetConstrained();
|
|
826
|
+
displace();
|
|
827
|
+
const before = skeletonReading(skeleton);
|
|
828
|
+
timeline.apply(skeleton, time, time, null, 1, MixFrom.current, true, false, true);
|
|
829
|
+
const once = skeletonReading(skeleton);
|
|
830
|
+
timeline.apply(skeleton, time, time, null, 1, MixFrom.current, true, false, true);
|
|
831
|
+
if (skeletonReading(skeleton) !== once) return 'accumulates';
|
|
832
|
+
if (once !== before) wrote = true;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return wrote ? 'overwrites' : 'inert';
|
|
837
|
+
}
|
|
838
|
+
|
|
665
839
|
export function validate(input: ValidateInput): ValidateReport {
|
|
666
840
|
const failures: Failure[] = [];
|
|
667
841
|
const passed: string[] = [];
|
|
@@ -2855,17 +3029,43 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2855
3029
|
//
|
|
2856
3030
|
// ## The second clause: `additive: true` is not always available
|
|
2857
3031
|
//
|
|
2858
|
-
//
|
|
2859
|
-
//
|
|
2860
|
-
//
|
|
2861
|
-
// a slider's own mix. A slot colour, an attachment swap, a draw order or a
|
|
2862
|
-
// sequence IGNORES the `add` argument entirely (`RGBATimeline.apply1` takes
|
|
2863
|
-
// it and never reads it), so two sliders sharing one of those overwrite each
|
|
3032
|
+
// A slot colour, an attachment swap, a draw order, an ik mix and a path's
|
|
3033
|
+
// spacing IGNORE the `add` argument entirely (`RGBATimeline.apply1` takes it
|
|
3034
|
+
// and never reads it), so two sliders sharing one of those overwrite each
|
|
2864
3035
|
// other whatever the flags say. Refusing that case too is what keeps this
|
|
2865
3036
|
// message from teaching a fix that does not work: an author told to set
|
|
2866
3037
|
// `additive: true` on a shared rgba would get a green gate over the same
|
|
2867
|
-
// dead axis.
|
|
2868
|
-
//
|
|
3038
|
+
// dead axis.
|
|
3039
|
+
//
|
|
3040
|
+
// 🚨 **Which class is which is MEASURED, and reading the flag was wrong.**
|
|
3041
|
+
// This clause used to ask `Timeline.additive`, the runtime's own declaration
|
|
3042
|
+
// that a class supports additive application — and two classes declare
|
|
3043
|
+
// `false` and honour `add` anyway. `PathConstraintMixTimeline` and
|
|
3044
|
+
// `SliderTimeline` pass the argument straight through, so two additive
|
|
3045
|
+
// sliders keying one path constraint's `mix`, or one bone-less slider's
|
|
3046
|
+
// `time`, **do** compose as the plain sum, and this assertion refused them
|
|
3047
|
+
// by name with a sentence about the runtime the runtime does not perform
|
|
3048
|
+
// (issue #655; `PS143` poses all thirty spellings of the motion vocabulary
|
|
3049
|
+
// and `PS140` holds the `time` case to a grid). `timelineAddBehaviour`
|
|
3050
|
+
// above poses each shared timeline instead, so what is compared is what the
|
|
3051
|
+
// class does rather than what it says about itself.
|
|
3052
|
+
//
|
|
3053
|
+
// ## The events clause, removed rather than narrowed
|
|
3054
|
+
//
|
|
3055
|
+
// The same reading refused two sliders whose animations both fire events,
|
|
3056
|
+
// and that refusal was over a property no pose can distinguish: `Slider`
|
|
3057
|
+
// applies its animation with `firedEvents` **null**
|
|
3058
|
+
// (`Slider.js`: `animation.apply(skeleton, p.time, p.time, data.loop, null,
|
|
3059
|
+
// …)`), and `EventTimeline.apply` opens with `if (!firedEvents) return`, so
|
|
3060
|
+
// a slider fires no event at all. Neither slider has anything on that
|
|
3061
|
+
// property for the other to erase. The same is true of a physics `reset`
|
|
3062
|
+
// under a slider, where `lastTime === time` leaves its window empty.
|
|
3063
|
+
//
|
|
3064
|
+
// ⭐ Neither is named here, and that is the point: the probe's third state
|
|
3065
|
+
// is **a timeline that writes nothing at all**, so both fall out of one
|
|
3066
|
+
// measurement instead of two exceptions. A list of unobservable spellings
|
|
3067
|
+
// written into this file would have been the hand-kept table that produced
|
|
3068
|
+
// the defect above.
|
|
2869
3069
|
check('A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET', () => {
|
|
2870
3070
|
const sliders = data.constraints.filter((c) => c instanceof SliderData);
|
|
2871
3071
|
if (sliders.length < 2) {
|
|
@@ -2931,6 +3131,15 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2931
3131
|
}
|
|
2932
3132
|
}
|
|
2933
3133
|
}
|
|
3134
|
+
/** Posed once per timeline, because the answer is the class's and the rigs that reach here share timelines. */
|
|
3135
|
+
const behaviour = new Map<Timeline, TimelineAddBehaviour>();
|
|
3136
|
+
const addBehaviourOf = (timeline: Timeline): TimelineAddBehaviour => {
|
|
3137
|
+
const known = behaviour.get(timeline);
|
|
3138
|
+
if (known !== undefined) return known;
|
|
3139
|
+
const measured = timelineAddBehaviour(data, timeline);
|
|
3140
|
+
behaviour.set(timeline, measured);
|
|
3141
|
+
return measured;
|
|
3142
|
+
};
|
|
2934
3143
|
let shared = 0;
|
|
2935
3144
|
for (const [id, users] of byProperty) {
|
|
2936
3145
|
if (users.length < 2) continue;
|
|
@@ -2940,21 +3149,27 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2940
3149
|
const chain = users.map((u) => at(u.slider)).join(', ');
|
|
2941
3150
|
for (let j = 1; j < users.length; j++) {
|
|
2942
3151
|
const later = users[j];
|
|
2943
|
-
const composes = later.timeline
|
|
2944
|
-
if (composes && later.slider.additive) continue;
|
|
3152
|
+
const composes = addBehaviourOf(later.timeline);
|
|
3153
|
+
if (composes === 'accumulates' && later.slider.additive) continue;
|
|
3154
|
+
// Nothing a second slider could take away: applied with the arguments
|
|
3155
|
+
// a slider passes — `firedEvents` null among them — this timeline
|
|
3156
|
+
// moves no pose at all.
|
|
3157
|
+
if (composes === 'inert') continue;
|
|
2945
3158
|
const erased = users.slice(0, j).filter((e) => canOverlap(e.slider, later.slider));
|
|
2946
3159
|
if (!erased.length) continue;
|
|
2947
3160
|
const erasedNames = `${erased.map((e) => `"${e.slider.name}"`).join(', ')} contribute${erased.length === 1 ? 's' : ''}`;
|
|
2948
|
-
const why =
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
`
|
|
2957
|
-
|
|
3161
|
+
const why =
|
|
3162
|
+
composes === 'accumulates'
|
|
3163
|
+
? `slider "${later.slider.name}" applies animation "${later.slider.animation?.name}" with additive false, and at ` +
|
|
3164
|
+
'mix 1 a non-additive apply writes the value outright (`getRelativeValue` returns `setup + value`, ' +
|
|
3165
|
+
'`getAbsoluteValue` returns `value`) rather than adding to the pose it found. Set `"additive": true` on ' +
|
|
3166
|
+
`slider "${later.slider.name}" in the rig spec — rigc will not choose that flag for you — or key this ` +
|
|
3167
|
+
`property from one slider only. [measured] \`${later.timeline.constructor.name}.apply\` posed twice with ` +
|
|
3168
|
+
'`add` set accumulates, so that flag is the repair here'
|
|
3169
|
+
: `the ${Property[Number(id.split('|')[0])] ?? id} timeline they share writes its value outright whatever the ` +
|
|
3170
|
+
`flags say — [measured] \`${later.timeline.constructor.name}.apply\` posed twice with \`add\` set left the ` +
|
|
3171
|
+
'same value there rather than adding to it — so `"additive": true` would NOT compose these. Key this ' +
|
|
3172
|
+
'property from one slider only, or move both edits into the one animation a single slider applies';
|
|
2958
3173
|
fail(
|
|
2959
3174
|
'A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET',
|
|
2960
3175
|
`${describe(later.timeline, id)} is keyed by the animations of ${users.length} sliders — ${chain} — and every ` +
|
|
@@ -2966,6 +3181,174 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2966
3181
|
stats.sliderSharedTargets = shared;
|
|
2967
3182
|
});
|
|
2968
3183
|
|
|
3184
|
+
// --- A42: a dial that drives a constraint the array already ran ---------
|
|
3185
|
+
//
|
|
3186
|
+
// 🚨 The hole A40 leaves, and it leaves it BY CONSTRUCTION. A40's population
|
|
3187
|
+
// is the sliders at full authority whose own `mix` nothing keys, so the one
|
|
3188
|
+
// pair this rule is about — a slider whose `mix` IS keyed — is excluded
|
|
3189
|
+
// before any timeline is looked at. Between them the two rules ask different
|
|
3190
|
+
// questions about the same array: A40 asks who writes a shared property
|
|
3191
|
+
// last, this asks whether anybody reads what was written at all.
|
|
3192
|
+
//
|
|
3193
|
+
// `Skeleton.updateCache` walks `constraints` in order and each constraint's
|
|
3194
|
+
// `sort` pushes itself onto the update cache as it is reached — `sortBone`
|
|
3195
|
+
// pushes bones and never a constraint — so the array IS the update order,
|
|
3196
|
+
// for every kind. Every constraint then opens its `update` by reading its
|
|
3197
|
+
// own applied pose: `Slider.update` takes `mix` and `time` off it before
|
|
3198
|
+
// applying its animation, `PhysicsConstraint.update` returns on `mix === 0`
|
|
3199
|
+
// before reading `inertia`, `wind` and the rest, and the ik, transform and
|
|
3200
|
+
// path constraints read their mixes the same way. So a slider that keys any
|
|
3201
|
+
// property of a constraint is read by that constraint only when it comes
|
|
3202
|
+
// LATER in the array; written the other way round the value lands in a pose
|
|
3203
|
+
// whose only reader has already run, and `Skeleton.updateWorldTransform`
|
|
3204
|
+
// opens the next frame by putting `Posed.resetConstrained` over it.
|
|
3205
|
+
//
|
|
3206
|
+
// [measured, issue #665] One dial and one constraint, in both array orders,
|
|
3207
|
+
// read at 6 positions of the dial. The constraint's own pose holds the same
|
|
3208
|
+
// ramp in BOTH orders — the write happens either way — and what the
|
|
3209
|
+
// constraint drives is dead in one of them:
|
|
3210
|
+
//
|
|
3211
|
+
// ik shin worldY 0.000e+0 against 1.662e+1 the other way round
|
|
3212
|
+
// transform shin world x-angle 0.000e+0 against 4.000e+1
|
|
3213
|
+
// path rider worldX 0.000e+0 against 1.620e+2
|
|
3214
|
+
// physics tip worldX 0.000e+0 against 4.256e+2, over 30 frames, so
|
|
3215
|
+
// the one kind with state of its own is not rescued by it
|
|
3216
|
+
// slider flag rotate 0.000e+0 against 4.000e+1
|
|
3217
|
+
//
|
|
3218
|
+
// ⭐ The runtime repairs this for BONES and not for constraints, which is
|
|
3219
|
+
// why an author cannot reason it out from the bone case. `Slider.sort`
|
|
3220
|
+
// clears `sorted` on every bone its animation keys and re-sorts them, so
|
|
3221
|
+
// those bones always update after the slider; for a constraint it calls
|
|
3222
|
+
// `skeleton.constrained(constraints[t.constraintIndex])`, which only swaps
|
|
3223
|
+
// the pose the constraint will be read through and moves nothing in the
|
|
3224
|
+
// update cache.
|
|
3225
|
+
//
|
|
3226
|
+
// 🔒 Keying its OWN `mix` or `time` is the same failure with the indices
|
|
3227
|
+
// equal, and it is the one A37 cannot see: A37 asks whether any animation
|
|
3228
|
+
// keys the slider's `mix` and the slider's own animation is one of them, so
|
|
3229
|
+
// a slider muted at setup that keys its own `mix` up reports green and is
|
|
3230
|
+
// dead forever — `update` returns on `mix === 0` before the animation that
|
|
3231
|
+
// would raise it is ever applied. Only a slider can reach that shape: the
|
|
3232
|
+
// constraint at the driver's own index IS the driver.
|
|
3233
|
+
//
|
|
3234
|
+
// ⚠️ `physics` `reset` is NOT in this population, and the measurement is why
|
|
3235
|
+
// the card that asked for "every constraint a slider's animation keys" is
|
|
3236
|
+
// refused here on one spelling. `PhysicsConstraintResetTimeline` writes no
|
|
3237
|
+
// pose at all — `constraint.reset()` sets fields on the constraint object,
|
|
3238
|
+
// which `resetConstrained` does not touch — and it fires only when a frame
|
|
3239
|
+
// time is CROSSED. A slider applies its animation with `p.time` as both
|
|
3240
|
+
// `lastTime` and `time`, so nothing is ever crossed: [measured] 0 calls to
|
|
3241
|
+
// `PhysicsConstraint.reset` at 6 dial readings x 4 frames in BOTH array
|
|
3242
|
+
// orders, against 1 call when the same animation is applied over a span.
|
|
3243
|
+
// Refusing it would name a reorder that repairs nothing, so the SKIP says
|
|
3244
|
+
// what was found instead.
|
|
3245
|
+
check('A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER', () => {
|
|
3246
|
+
const sliders = data.constraints.filter((c) => c instanceof SliderData);
|
|
3247
|
+
if (!sliders.length) {
|
|
3248
|
+
return skip('A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER', 'the skeleton declares no slider constraint');
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* The runtime class behind a `*Data`, and the word a motion spec writes
|
|
3252
|
+
* for its kind. Taken structurally rather than through
|
|
3253
|
+
* `ConstraintData<T, P>`, whose two type arguments the runtime itself
|
|
3254
|
+
* fills with `any` — which `src/` may not write.
|
|
3255
|
+
*/
|
|
3256
|
+
const kindOf = (constraint: { constructor: { name: string } }): { word: string; runtime: string } => {
|
|
3257
|
+
const runtime = constraint.constructor.name.replace(/Data$/, '');
|
|
3258
|
+
return { word: runtime.replace(/Constraint$/, '').toLowerCase(), runtime };
|
|
3259
|
+
};
|
|
3260
|
+
/**
|
|
3261
|
+
* `physicsConstraintWind` -> `wind`, `pathConstraintMix` -> `mix`,
|
|
3262
|
+
* `sliderTime` -> `time`, `ikConstraint` -> `ik`: the `Property` name with
|
|
3263
|
+
* the constraint kind taken off the front, which is the word the motion
|
|
3264
|
+
* spec's own track or block carries. Derived rather than tabulated — a
|
|
3265
|
+
* table here would be one more hand-kept list of the runtime's enum.
|
|
3266
|
+
*/
|
|
3267
|
+
const keyedWord = (property: string, kind: string): string => {
|
|
3268
|
+
const rest = property.replace(/^(ik|transform|path|physics|slider)(Constraint)?/, '');
|
|
3269
|
+
return rest === '' ? kind : rest.charAt(0).toLowerCase() + rest.slice(1);
|
|
3270
|
+
};
|
|
3271
|
+
/**
|
|
3272
|
+
* Which constraints one timeline writes into.
|
|
3273
|
+
*
|
|
3274
|
+
* A physics timeline whose animation names no constraint carries
|
|
3275
|
+
* `constraintIndex -1`, and `PhysicsConstraintTimeline.apply` reads that
|
|
3276
|
+
* as every ACTIVE physics constraint whose own data declares that property
|
|
3277
|
+
* global. No rig spec reaches it — the compiler refuses a track naming an
|
|
3278
|
+
* unknown physics constraint, the empty name among them — but a file rigc
|
|
3279
|
+
* did not write can carry one, and this is the only assertion that would
|
|
3280
|
+
* have to guess at it.
|
|
3281
|
+
*/
|
|
3282
|
+
const drivenBy = (timeline: Timeline & ConstraintTimeline): number[] => {
|
|
3283
|
+
if (timeline.constraintIndex >= 0) return [timeline.constraintIndex];
|
|
3284
|
+
if (!(timeline instanceof PhysicsConstraintTimeline)) return [];
|
|
3285
|
+
return data.constraints.flatMap((one, index) => (one instanceof PhysicsConstraintData && timeline.global(one) ? [index] : []));
|
|
3286
|
+
};
|
|
3287
|
+
let pairs = 0;
|
|
3288
|
+
let resets = 0;
|
|
3289
|
+
for (const driver of sliders) {
|
|
3290
|
+
const driverIndex = data.constraints.indexOf(driver);
|
|
3291
|
+
for (const timeline of driver.animation?.timelines ?? []) {
|
|
3292
|
+
if (!isConstraintTimeline(timeline)) continue;
|
|
3293
|
+
if (timeline instanceof PhysicsConstraintResetTimeline) {
|
|
3294
|
+
resets++;
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3297
|
+
const property = String(Property[Number(timeline.propertyIds[0].split('|')[0])] ?? timeline.propertyIds[0]);
|
|
3298
|
+
const everyPhysics = timeline.constraintIndex < 0;
|
|
3299
|
+
for (const drivenIndex of drivenBy(timeline)) {
|
|
3300
|
+
const driven = data.constraints[drivenIndex];
|
|
3301
|
+
if (driven === undefined) continue;
|
|
3302
|
+
pairs++;
|
|
3303
|
+
if (drivenIndex > driverIndex) continue;
|
|
3304
|
+
const kind = kindOf(driven);
|
|
3305
|
+
const word = keyedWord(property, kind.word);
|
|
3306
|
+
const animation = `animation "${driver.animation?.name}"`;
|
|
3307
|
+
const reference = `\`${kind.word}.${driven.name}${word === kind.word ? '' : `.${word}`}\``;
|
|
3308
|
+
fail(
|
|
3309
|
+
'A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER',
|
|
3310
|
+
drivenIndex === driverIndex
|
|
3311
|
+
? `slider "${driver.name}" (constraints[${driverIndex}]) keys its own \`${word}\` in ${animation}. ` +
|
|
3312
|
+
'`Slider.update` reads `appliedPose.' +
|
|
3313
|
+
`${word}\` as the ${word === 'mix' ? 'alpha it applies that animation with' : 'time it applies that animation at'}, ` +
|
|
3314
|
+
'before the animation runs, so the key is written after its only reader and `Posed.resetConstrained` ' +
|
|
3315
|
+
`puts the pose back before the next frame${
|
|
3316
|
+
word === 'mix'
|
|
3317
|
+
? ' — and at `mix` 0 `update` returns before applying anything at all, so the key that would raise it is unreachable'
|
|
3318
|
+
: ''
|
|
3319
|
+
}. Key ${reference} from a slider EARLIER in \`constraints\`, or state the ` +
|
|
3320
|
+
`\`${word}\` this slider should start at in the rig spec`
|
|
3321
|
+
: `slider "${driver.name}" (constraints[${driverIndex}]) keys ${
|
|
3322
|
+
word === kind.word ? `the \`${word}\` timeline` : `\`${word}\``
|
|
3323
|
+
} of ${kind.word} constraint ` +
|
|
3324
|
+
`"${driven.name}" (constraints[${drivenIndex}]) in ${animation}${
|
|
3325
|
+
everyPhysics ? ' — the timeline names no constraint, which the runtime reads as every physics constraint declaring that property global —' : ''
|
|
3326
|
+
}, and "${driven.name}" updates FIRST. The \`constraints\` array is the update order ` +
|
|
3327
|
+
'(`Skeleton.updateCache` walks it and each constraint\'s `sort` pushes itself as it is reached) and ' +
|
|
3328
|
+
`\`${kind.runtime}.update\` reads its own \`appliedPose\` before applying anything, so that key is ` +
|
|
3329
|
+
'written after the only read of it and `Posed.resetConstrained` discards it before the next frame: ' +
|
|
3330
|
+
`what "${driven.name}" drives is dead at every reading of "${driver.name}"'s dial, although its pose ` +
|
|
3331
|
+
`still holds the number. Move "${driver.name}" before "${driven.name}" in \`constraints\`, or key ` +
|
|
3332
|
+
`${reference} from a slider that already is`,
|
|
3333
|
+
);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
if (pairs === 0) {
|
|
3338
|
+
return skip(
|
|
3339
|
+
'A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER',
|
|
3340
|
+
`no animation applied by one of the ${sliders.length} slider constraint${sliders.length === 1 ? '' : 's'} keys a ` +
|
|
3341
|
+
`property of a constraint, so no slider here drives a constraint${
|
|
3342
|
+
resets === 0
|
|
3343
|
+
? ''
|
|
3344
|
+
: ` — the ${resets} \`physics\` \`reset\` key(s) they do carry are not a pose write, and [measured] a slider ` +
|
|
3345
|
+
'applies its animation at one instant, so `PhysicsConstraintResetTimeline` never fires from one in either array order'
|
|
3346
|
+
}`,
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
stats.sliderDrivenConstraints = pairs;
|
|
3350
|
+
});
|
|
3351
|
+
|
|
2969
3352
|
// --- A38: a per-skin member list and its `skin: true` flag agree --------
|
|
2970
3353
|
//
|
|
2971
3354
|
// 🚨 Two halves of one switch, and either half alone is dead data in silence.
|