spine-rigc 1.9.1 → 1.9.2
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/cli.ts +39 -17
- package/docs/AUTHORING.md +1 -1
- package/package.json +1 -1
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/index.ts +21 -2
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/bonedist.ts +18 -6
- package/src/core/additive.ts +468 -0
- package/src/core/constraints.ts +12 -2
- package/src/core/constraints_physics.ts +31 -8
- package/src/core/raw.ts +77 -16
- package/src/core/walk.ts +202 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +151 -86
- package/src/render_core.ts +5 -0
- package/src/validate.ts +432 -457
package/src/validate.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { dirname, resolve } from 'node:path';
|
|
17
17
|
import {
|
|
18
|
+
type Animation,
|
|
18
19
|
AnimationState,
|
|
19
20
|
AnimationStateData,
|
|
20
21
|
AtlasAttachmentLoader,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
PhysicsConstraintTimeline,
|
|
41
42
|
Property,
|
|
42
43
|
RegionAttachment,
|
|
44
|
+
SequenceTimeline as RuntimeSequenceTimeline,
|
|
43
45
|
Skeleton,
|
|
44
46
|
SkeletonJson,
|
|
45
47
|
SliderData,
|
|
@@ -65,7 +67,7 @@ import {
|
|
|
65
67
|
TOPLEVEL_CONSTRAINT_ARRAYS,
|
|
66
68
|
type SpineGeneration,
|
|
67
69
|
} from './generation.ts';
|
|
68
|
-
import {
|
|
70
|
+
import { posedNumbersOf } from './render.ts';
|
|
69
71
|
import { BONE_INHERIT_KNOWN } from './rig.ts';
|
|
70
72
|
import {
|
|
71
73
|
CHANNELS_BY_KIND,
|
|
@@ -145,6 +147,12 @@ import { a39DeformKeepsTriangleWinding } from './assertions/bodies/a39.ts';
|
|
|
145
147
|
import { a09AnimationDurationMatchesSpec } from './assertions/bodies/a09.ts';
|
|
146
148
|
import { a43TwoColorTintLoadsAndPosesAsWritten } from './assertions/bodies/a43.ts';
|
|
147
149
|
import { a46SequenceAttachmentsShowTheFrameTheFileStates } from './assertions/bodies/a46.ts';
|
|
150
|
+
import type { SliderCompositionFacts, SliderFact, SliderTimelineFact } from './assertions/facts/slider_composition.ts';
|
|
151
|
+
import type { ConstraintTargetFacts, TargetAnimation, TargetKeyArray } from './assertions/facts/constraint_targets.ts';
|
|
152
|
+
import { a34ConstraintTimelineTargets } from './assertions/bodies/a34.ts';
|
|
153
|
+
import { a40SlidersComposeOnASharedTarget } from './assertions/bodies/a40.ts';
|
|
154
|
+
import { a10NoNanAfterStepping } from './assertions/bodies/a10.ts';
|
|
155
|
+
import type { SteppedFrame, SteppedPoseFacts } from './assertions/facts/stepped_poses.ts';
|
|
148
156
|
|
|
149
157
|
export type { Failure } from './assertions/harness.ts';
|
|
150
158
|
|
|
@@ -292,8 +300,6 @@ export interface ValidateReport {
|
|
|
292
300
|
// with, and A09's one-frame slack, are `./assertions/bodies/a39.ts`'s and
|
|
293
301
|
// `./assertions/bodies/a09.ts`'s since issue #1025 (cut 4c-3).
|
|
294
302
|
|
|
295
|
-
const STEP_FRAMES = 120;
|
|
296
|
-
|
|
297
303
|
/**
|
|
298
304
|
* The constraint groups that spell `group.<constraint>.<timeline>` — a
|
|
299
305
|
* constraint name, then named timelines under it (A34's second shape).
|
|
@@ -649,18 +655,18 @@ export type TimelineAddBehaviour = 'accumulates' | 'overwrites' | 'inert';
|
|
|
649
655
|
*/
|
|
650
656
|
const PROBE_DISPLACEMENT = 0.375;
|
|
651
657
|
|
|
652
|
-
/** Every own value of a pose object a timeline could have written, as text. */
|
|
653
|
-
function
|
|
654
|
-
const parts: string
|
|
658
|
+
/** Every own value of a pose object a timeline could have written, each as its key and its text. */
|
|
659
|
+
function poseParts(pose: object): Array<[string, string]> {
|
|
660
|
+
const parts: Array<[string, string]> = [];
|
|
655
661
|
for (const key of Object.keys(pose).sort()) {
|
|
656
662
|
const value = (pose as Record<string, unknown>)[key];
|
|
657
663
|
if (value === null || value === undefined || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') {
|
|
658
|
-
parts.push(
|
|
664
|
+
parts.push([key, String(value)]);
|
|
659
665
|
continue;
|
|
660
666
|
}
|
|
661
667
|
if (Array.isArray(value)) {
|
|
662
668
|
const entries: unknown[] = value;
|
|
663
|
-
if (entries.every((one) => typeof one === 'number')) parts.push(
|
|
669
|
+
if (entries.every((one) => typeof one === 'number')) parts.push([key, `[${entries.join(',')}]`]);
|
|
664
670
|
continue;
|
|
665
671
|
}
|
|
666
672
|
if (typeof value !== 'object') continue;
|
|
@@ -670,12 +676,17 @@ function poseReading(pose: object): string {
|
|
|
670
676
|
// structure — a bone back-reference, a slot's data — is either an identity
|
|
671
677
|
// (read by name below) or something no timeline writes.
|
|
672
678
|
if (keys.length > 0 && keys.every((one) => typeof record[one] === 'number')) {
|
|
673
|
-
parts.push(
|
|
679
|
+
parts.push([key, `{${keys.map((one) => `${one}:${String(record[one])}`).join(',')}}`]);
|
|
674
680
|
} else if (typeof record.name === 'string') {
|
|
675
|
-
parts.push(
|
|
681
|
+
parts.push([key, `@${record.name}`]);
|
|
676
682
|
}
|
|
677
683
|
}
|
|
678
|
-
return parts
|
|
684
|
+
return parts;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Every own value of a pose object a timeline could have written, as text. */
|
|
688
|
+
function poseReading(pose: object): string {
|
|
689
|
+
return poseParts(pose).map(([key, text]) => `${key}=${text}`).join(',');
|
|
679
690
|
}
|
|
680
691
|
|
|
681
692
|
/** The whole of what a timeline could have changed, in one comparable string. */
|
|
@@ -801,6 +812,73 @@ export function timelineAddBehaviour(data: ReturnType<SkeletonJson['readSkeleton
|
|
|
801
812
|
return wrote ? 'overwrites' : 'inert';
|
|
802
813
|
}
|
|
803
814
|
|
|
815
|
+
/** One skin under one start state of `timelineAddBehaviour`'s probe: what the cell reads, and every pose field the applications moved. */
|
|
816
|
+
export interface TimelineAddCell {
|
|
817
|
+
/** The skin worn, `(none)` for no skin set. */
|
|
818
|
+
view: string;
|
|
819
|
+
state: 'setup' | 'displaced';
|
|
820
|
+
/** `A` a second application moved the pose at some time, `W` only the first did, `-` neither ever did. */
|
|
821
|
+
letter: 'A' | 'W' | '-';
|
|
822
|
+
/** Each field the first or the second application moved, at each time: `bone:<name>.<key>`, `slot:<name>.<key>` or `constraint:<name>.<key>`, with its three readings. */
|
|
823
|
+
changed: Array<{ field: string; t: number; before: string; once: string; twice: string }>;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* `timelineAddBehaviour`'s probe, every cell kept: the same skins, start states,
|
|
828
|
+
* times and arguments, read per field rather than as one string — what the
|
|
829
|
+
* core suite's `CO27` holds the core's additive probe (`src/core/additive.ts`)
|
|
830
|
+
* to, cell by cell and value by value (issue #1025, cut 4c-5). The class the
|
|
831
|
+
* cells read is held to `timelineAddBehaviour`'s own answer on every timeline
|
|
832
|
+
* there, so the two cannot drift.
|
|
833
|
+
*/
|
|
834
|
+
export function timelineAddCells(data: ReturnType<SkeletonJson['readSkeletonData']>, timeline: Timeline): TimelineAddCell[] {
|
|
835
|
+
const fields = (skeleton: Skeleton): Map<string, string> => {
|
|
836
|
+
const out = new Map<string, string>();
|
|
837
|
+
const take = (kind: string, name: string, pose: object): void => {
|
|
838
|
+
for (const [key, text] of poseParts(pose)) out.set(`${kind}:${name}.${key}`, text);
|
|
839
|
+
};
|
|
840
|
+
for (const bone of skeleton.bones) take('bone', bone.data.name, bone.appliedPose);
|
|
841
|
+
for (const slot of skeleton.slots) take('slot', slot.data.name, slot.appliedPose);
|
|
842
|
+
for (const constraint of skeleton.constraints) take('constraint', constraint.data.name, constraint.appliedPose as object);
|
|
843
|
+
out.set('drawOrder', skeleton.drawOrder.appliedPose.map((slot) => slot.data.name).join('>'));
|
|
844
|
+
return out;
|
|
845
|
+
};
|
|
846
|
+
const cells: TimelineAddCell[] = [];
|
|
847
|
+
for (const skin of [null, ...data.skins]) {
|
|
848
|
+
const skeleton = new Skeleton(data);
|
|
849
|
+
if (skin !== null) skeleton.setSkin(skin);
|
|
850
|
+
probeStartStates(skeleton).forEach((displace, at) => {
|
|
851
|
+
const cell: TimelineAddCell = { view: skin?.name ?? '(none)', state: at === 0 ? 'setup' : 'displaced', letter: '-', changed: [] };
|
|
852
|
+
for (const time of probeTimes(timeline)) {
|
|
853
|
+
skeleton.setupPose();
|
|
854
|
+
for (const bone of skeleton.bones) bone.resetConstrained();
|
|
855
|
+
for (const slot of skeleton.slots) slot.resetConstrained();
|
|
856
|
+
for (const constraint of skeleton.constraints) constraint.resetConstrained();
|
|
857
|
+
skeleton.drawOrder.resetConstrained();
|
|
858
|
+
displace();
|
|
859
|
+
const before = fields(skeleton);
|
|
860
|
+
timeline.apply(skeleton, time, time, null, 1, MixFrom.current, true, false, true);
|
|
861
|
+
const once = fields(skeleton);
|
|
862
|
+
timeline.apply(skeleton, time, time, null, 1, MixFrom.current, true, false, true);
|
|
863
|
+
const twice = fields(skeleton);
|
|
864
|
+
let wrote = false;
|
|
865
|
+
let again = false;
|
|
866
|
+
for (const [field, b] of before) {
|
|
867
|
+
const o = once.get(field) ?? '';
|
|
868
|
+
const w = twice.get(field) ?? '';
|
|
869
|
+
if (o !== b) wrote = true;
|
|
870
|
+
if (w !== o) again = true;
|
|
871
|
+
if (o !== b || w !== o) cell.changed.push({ field, t: time, before: b, once: o, twice: w });
|
|
872
|
+
}
|
|
873
|
+
if (again) cell.letter = 'A';
|
|
874
|
+
else if (wrote && cell.letter === '-') cell.letter = 'W';
|
|
875
|
+
}
|
|
876
|
+
cells.push(cell);
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
return cells;
|
|
880
|
+
}
|
|
881
|
+
|
|
804
882
|
/**
|
|
805
883
|
* Where two texts first differ, as A18 names it: the 1-based line and each
|
|
806
884
|
* side's line, trimmed and cut to 120 characters.
|
|
@@ -1635,6 +1713,129 @@ export function spineConstraintFacts(data: ReturnType<SkeletonJson['readSkeleton
|
|
|
1635
1713
|
return { animations: data.animations.length, constraints, timelines, pathSlots };
|
|
1636
1714
|
}
|
|
1637
1715
|
|
|
1716
|
+
/**
|
|
1717
|
+
* The runtime's supply of `ConstraintTargetFacts` (issue #1025, cut 4c-5):
|
|
1718
|
+
* A34's walk of the raw JSON, as A34 always walked it — the `constraints`
|
|
1719
|
+
* array's objects, then each animation (`Object.entries`, the file's order)
|
|
1720
|
+
* that is an object, its `ik` and `transform` groups (each entry ONE key
|
|
1721
|
+
* array) and its `path`, `physics` and `slider` groups (each entry an object
|
|
1722
|
+
* of named key arrays, or a bare value), each where the group is an object.
|
|
1723
|
+
* `reach` builds the timeline the parser builds for a physics timeline NAME
|
|
1724
|
+
* (`unnamedPhysicsTimeline`) and asks it of the file's physics constraint
|
|
1725
|
+
* objects (`unnamedPhysicsReach`). The model side's supply is
|
|
1726
|
+
* `./assertions/model/constraint_targets.ts`.
|
|
1727
|
+
*/
|
|
1728
|
+
export function rawConstraintTargets(raw: Json): ConstraintTargetFacts {
|
|
1729
|
+
const objects = (Array.isArray(raw.constraints) ? (raw.constraints as unknown[]) : []).filter((entry): entry is Json => isObj(entry));
|
|
1730
|
+
const constraints = objects.map((entry) => ({ name: typeof entry.name === 'string' ? entry.name : null, type: String(entry.type), spelled: String(entry.name) }));
|
|
1731
|
+
const rawPhysics = objects.filter((entry) => entry.type === 'physics');
|
|
1732
|
+
const keyArray = (timeline: string, keys: unknown): TargetKeyArray => ({ timeline, keys: Array.isArray(keys) ? { count: keys.length } : { spelled: `${JSON.stringify(keys)}` } });
|
|
1733
|
+
let animations: TargetAnimation[] | null = null;
|
|
1734
|
+
if (isObj(raw.animations)) {
|
|
1735
|
+
animations = [];
|
|
1736
|
+
for (const [name, anim] of Object.entries(raw.animations)) {
|
|
1737
|
+
if (!isObj(anim)) continue;
|
|
1738
|
+
const groups: TargetAnimation['groups'][number][] = [];
|
|
1739
|
+
for (const group of ['ik', 'transform'] as const) {
|
|
1740
|
+
if (!isObj(anim[group])) continue;
|
|
1741
|
+
groups.push({ group, entries: Object.entries(anim[group] as Json).map(([target, keys]) => ({ name: target, bare: null, keyArrays: [keyArray('', keys)] })) });
|
|
1742
|
+
}
|
|
1743
|
+
for (const group of NAMED_TIMELINE_GROUPS) {
|
|
1744
|
+
if (!isObj(anim[group])) continue;
|
|
1745
|
+
groups.push({
|
|
1746
|
+
group,
|
|
1747
|
+
entries: Object.entries(anim[group] as Json).map(([target, timelines]) =>
|
|
1748
|
+
isObj(timelines) ? { name: target, bare: null, keyArrays: Object.entries(timelines).map(([timeline, keys]) => keyArray(timeline, keys)) } : { name: target, bare: JSON.stringify(timelines), keyArrays: [] },
|
|
1749
|
+
),
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
animations.push({ name, groups });
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return {
|
|
1756
|
+
constraints,
|
|
1757
|
+
groupsByAnimation: animations,
|
|
1758
|
+
reach: (name) => {
|
|
1759
|
+
const timeline = unnamedPhysicsTimeline(name);
|
|
1760
|
+
if (timeline === null) return null;
|
|
1761
|
+
return { resets: timeline instanceof PhysicsConstraintResetTimeline, reached: unnamedPhysicsReach(timeline, rawPhysics).map((one) => String(one.name)) };
|
|
1762
|
+
},
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* The runtime's supply of `SliderCompositionFacts` (issue #1025, cut 4c-5):
|
|
1768
|
+
* the loaded sliders in the `constraints` array's order, and each animation's
|
|
1769
|
+
* loaded timelines in the order the runtime built them, each with its class
|
|
1770
|
+
* name and its `propertyIds` — spelled as the facts spell an id (the property's
|
|
1771
|
+
* name off `Property`, the index after it as loaded, and a deform or sequence
|
|
1772
|
+
* timeline's attachment OBJECT replaced by the address a skin files it under,
|
|
1773
|
+
* first filing kept, as `spineSequenceFacts` maps it) — and the sentence's
|
|
1774
|
+
* words for each, `describe` as A40 always wrote it. `behaviour` is today's
|
|
1775
|
+
* probe, `timelineAddBehaviour`. The model side's supply is
|
|
1776
|
+
* `./assertions/model/slider_composition.ts`.
|
|
1777
|
+
*/
|
|
1778
|
+
export function spineSliderComposition(data: ReturnType<SkeletonJson['readSkeletonData']>): SliderCompositionFacts {
|
|
1779
|
+
let addressOf: Map<object, string> | null = null;
|
|
1780
|
+
const address = (attachment: object): string => {
|
|
1781
|
+
if (addressOf === null) {
|
|
1782
|
+
addressOf = new Map();
|
|
1783
|
+
for (const skin of data.skins) {
|
|
1784
|
+
for (const entry of skin.getAttachments()) {
|
|
1785
|
+
if (!addressOf.has(entry.attachment)) addressOf.set(entry.attachment, entryAddress(skin.name, data.slots[entry.slotIndex].name, entry.placeholder));
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return addressOf.get(attachment) ?? '';
|
|
1790
|
+
};
|
|
1791
|
+
/** What a property id points at, in the words the rig spec uses — A40's `describe`. */
|
|
1792
|
+
const describe = (timeline: Timeline, id: string): string => {
|
|
1793
|
+
const property = Property[Number(id.split('|')[0])] ?? id;
|
|
1794
|
+
if (isBoneTimeline(timeline)) return `bone "${data.bones[timeline.boneIndex]?.name ?? timeline.boneIndex}" ${property}`;
|
|
1795
|
+
if (timeline instanceof DeformTimeline) {
|
|
1796
|
+
return `slot "${data.slots[timeline.slotIndex]?.name ?? timeline.slotIndex}" deform of "${timeline.attachment.name}"`;
|
|
1797
|
+
}
|
|
1798
|
+
if (isSlotTimeline(timeline)) return `slot "${data.slots[timeline.slotIndex]?.name ?? timeline.slotIndex}" ${property}`;
|
|
1799
|
+
if (isConstraintTimeline(timeline) && timeline.constraintIndex >= 0) {
|
|
1800
|
+
return `constraint "${data.constraints[timeline.constraintIndex]?.name ?? timeline.constraintIndex}" ${property}`;
|
|
1801
|
+
}
|
|
1802
|
+
return `the skeleton's ${property}`;
|
|
1803
|
+
};
|
|
1804
|
+
/** An id as the facts spell it: the property's name in place of its number, an attachment's address in place of its serial. */
|
|
1805
|
+
const idOf = (timeline: Timeline, id: string): string => {
|
|
1806
|
+
const [head, ...rest] = id.split('|');
|
|
1807
|
+
const named = [Property[Number(head)] ?? head, ...rest];
|
|
1808
|
+
if (timeline instanceof DeformTimeline || timeline instanceof RuntimeSequenceTimeline) named[2] = address(timeline.attachment);
|
|
1809
|
+
return named.join('|');
|
|
1810
|
+
};
|
|
1811
|
+
const timelinesOf = (animation: Animation): SliderTimelineFact[] =>
|
|
1812
|
+
animation.timelines.map((timeline) => ({
|
|
1813
|
+
runtimeClass: timeline.constructor.name,
|
|
1814
|
+
properties: timeline.propertyIds.map((id) => ({ id: idOf(timeline, id), property: Property[Number(id.split('|')[0])] ?? id, names: describe(timeline, id) })),
|
|
1815
|
+
}));
|
|
1816
|
+
const sliders: SliderFact[] = [];
|
|
1817
|
+
data.constraints.forEach((c, index) => {
|
|
1818
|
+
if (!(c instanceof SliderData)) return;
|
|
1819
|
+
sliders.push({
|
|
1820
|
+
name: c.name,
|
|
1821
|
+
index,
|
|
1822
|
+
mix: c.setupPose.mix,
|
|
1823
|
+
additive: c.additive,
|
|
1824
|
+
skinRequired: c.skinRequired,
|
|
1825
|
+
skins: data.skins.filter((skin) => skin.constraints.includes(c)).map((skin) => skin.name),
|
|
1826
|
+
animation: c.animation === null ? null : { name: c.animation.name, timelines: timelinesOf(c.animation) },
|
|
1827
|
+
});
|
|
1828
|
+
});
|
|
1829
|
+
return {
|
|
1830
|
+
sliders,
|
|
1831
|
+
behaviour: (animationName, at) => {
|
|
1832
|
+
const timeline = data.findAnimation(animationName)?.timelines[at];
|
|
1833
|
+
if (timeline === undefined) throw new Error(`internal: animation "${animationName}" has no timeline ${at}`);
|
|
1834
|
+
return timelineAddBehaviour(data, timeline);
|
|
1835
|
+
},
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1638
1839
|
/**
|
|
1639
1840
|
* The facts cut 4c-2's bodies read, as `validate()` supplies them from a pair
|
|
1640
1841
|
* spine-core loads — or `null` when the load throws (A00's failure). For the
|
|
@@ -1660,7 +1861,98 @@ export function runtimeRigFacts(skeletonText: string, atlasText: string): { mesh
|
|
|
1660
1861
|
* `tools/verdict_gate.ts`, which hand them to the bodies and ask the model
|
|
1661
1862
|
* side's suppliers the same questions, value by value.
|
|
1662
1863
|
*/
|
|
1663
|
-
|
|
1864
|
+
/**
|
|
1865
|
+
* The runtime's supply of A10's facts (issue #1025, cut 4c-5a): every pose A10
|
|
1866
|
+
* reads, posed by spine-core exactly as A10 always posed it.
|
|
1867
|
+
*
|
|
1868
|
+
* - The setup pose: `setupPose()`, `update(0)`,
|
|
1869
|
+
* `updateWorldTransform(Physics.reset)`, no animation set.
|
|
1870
|
+
* - Each animation's walk: `setAnimation(0, name, true)` — a LOOPING track —
|
|
1871
|
+
* over a fresh skeleton posed as above, then per step `AnimationState.update
|
|
1872
|
+
* (step)`, `apply`, `Skeleton.update(step)`, `updateWorldTransform
|
|
1873
|
+
* (Physics.update)`, one pose read after each.
|
|
1874
|
+
* - A bone's mode at a key: a fresh, non-looping track, the setup pose reset,
|
|
1875
|
+
* the track stepped to the key's time and applied, then `update` and
|
|
1876
|
+
* `updateWorldTransform(Physics.update)` by the same time.
|
|
1877
|
+
*
|
|
1878
|
+
* A pose is read as `posedNumbersOf` reads it for `render` (every bone's world
|
|
1879
|
+
* transform, every shown region's and mesh's world vertices), with each bone's
|
|
1880
|
+
* `appliedPose.inherit` — `null` where it is a value `updateWorldTransform`'s
|
|
1881
|
+
* switch has a case for, read off the runtime's own enum — and every slot's
|
|
1882
|
+
* light and dark colour. The model side's supply is
|
|
1883
|
+
* `./assertions/model/stepped_poses.ts`.
|
|
1884
|
+
*/
|
|
1885
|
+
export function spineSteppedPoses(raw: Json | null, data: ReturnType<SkeletonJson['readSkeletonData']>): SteppedPoseFacts {
|
|
1886
|
+
/** Is this a mode `updateWorldTransform`'s switch has a case for? Read off the runtime's own enum. */
|
|
1887
|
+
const isMode = (inherit: unknown): boolean => typeof inherit === 'number' && Inherit[inherit] !== undefined;
|
|
1888
|
+
const frameOf = (skeleton: Skeleton): SteppedFrame => {
|
|
1889
|
+
const { bones, drawn } = posedNumbersOf(skeleton);
|
|
1890
|
+
return {
|
|
1891
|
+
bones: bones.map((b, i) => {
|
|
1892
|
+
const inherit = skeleton.bones[i].appliedPose.inherit;
|
|
1893
|
+
return { ...b, inherit: isMode(inherit) ? null : String(inherit) };
|
|
1894
|
+
}),
|
|
1895
|
+
drawn,
|
|
1896
|
+
slots: skeleton.slots.map((slot) => {
|
|
1897
|
+
const c = slot.appliedPose.color;
|
|
1898
|
+
const d = slot.appliedPose.darkColor;
|
|
1899
|
+
return { name: slot.data.name, colour: [c.r, c.g, c.b, c.a] as const, dark: d === null ? null : ([d.r, d.g, d.b] as const) };
|
|
1900
|
+
}),
|
|
1901
|
+
};
|
|
1902
|
+
};
|
|
1903
|
+
return {
|
|
1904
|
+
boneCount: data.bones.length,
|
|
1905
|
+
steppedAnimations: data.animations.map((anim) => ({ name: anim.name, duration: anim.duration })),
|
|
1906
|
+
hasAnimation: (name) => Boolean(data.findAnimation(name)),
|
|
1907
|
+
hasBone: (name) => Boolean(data.findBone(name)),
|
|
1908
|
+
posedInherit: (animName, boneName, time) => {
|
|
1909
|
+
const skeleton = new Skeleton(data);
|
|
1910
|
+
const state = new AnimationState(new AnimationStateData(data));
|
|
1911
|
+
state.setAnimation(0, animName, false);
|
|
1912
|
+
skeleton.setupPose();
|
|
1913
|
+
skeleton.update(0);
|
|
1914
|
+
skeleton.updateWorldTransform(Physics.reset);
|
|
1915
|
+
state.update(time);
|
|
1916
|
+
state.apply(skeleton);
|
|
1917
|
+
skeleton.update(time);
|
|
1918
|
+
skeleton.updateWorldTransform(Physics.update);
|
|
1919
|
+
const bone = skeleton.findBone(boneName);
|
|
1920
|
+
if (bone === null) return undefined;
|
|
1921
|
+
const posed = bone.appliedPose.inherit;
|
|
1922
|
+
return isMode(posed) ? null : String(posed);
|
|
1923
|
+
},
|
|
1924
|
+
statedInherit: (name) => {
|
|
1925
|
+
const rawBone = Array.isArray(raw?.bones) ? (raw.bones as unknown[]).find((b) => isObj(b) && b.name === name) : undefined;
|
|
1926
|
+
return `${JSON.stringify(isObj(rawBone) ? rawBone.inherit : undefined)}`;
|
|
1927
|
+
},
|
|
1928
|
+
setup: () => {
|
|
1929
|
+
const atRest = new Skeleton(data);
|
|
1930
|
+
atRest.setupPose();
|
|
1931
|
+
atRest.update(0);
|
|
1932
|
+
atRest.updateWorldTransform(Physics.reset);
|
|
1933
|
+
return frameOf(atRest);
|
|
1934
|
+
},
|
|
1935
|
+
walk: (animName, step, frames) => {
|
|
1936
|
+
const skeleton = new Skeleton(data);
|
|
1937
|
+
const state = new AnimationState(new AnimationStateData(data));
|
|
1938
|
+
state.setAnimation(0, animName, true);
|
|
1939
|
+
skeleton.setupPose();
|
|
1940
|
+
skeleton.update(0);
|
|
1941
|
+
skeleton.updateWorldTransform(Physics.reset);
|
|
1942
|
+
const out: SteppedFrame[] = [];
|
|
1943
|
+
for (let i = 0; i < frames; i++) {
|
|
1944
|
+
state.update(step);
|
|
1945
|
+
state.apply(skeleton);
|
|
1946
|
+
skeleton.update(step);
|
|
1947
|
+
skeleton.updateWorldTransform(Physics.update);
|
|
1948
|
+
out.push(frameOf(skeleton));
|
|
1949
|
+
}
|
|
1950
|
+
return out;
|
|
1951
|
+
},
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
export function runtimePosedFacts(skeletonText: string, atlasText: string): { deformSurvey: DeformSurveyFacts; animationDurations: AnimationDurationFacts; twoColour: TwoColourFacts; sequences: SequenceFacts; steppedPoses: SteppedPoseFacts; boneTimelines: BoneTimelineFacts } | null {
|
|
1664
1956
|
let data: ReturnType<SkeletonJson['readSkeletonData']>;
|
|
1665
1957
|
let raw: Json;
|
|
1666
1958
|
try {
|
|
@@ -1669,7 +1961,26 @@ export function runtimePosedFacts(skeletonText: string, atlasText: string): { de
|
|
|
1669
1961
|
} catch {
|
|
1670
1962
|
return null;
|
|
1671
1963
|
}
|
|
1672
|
-
return { deformSurvey: spineDeformSurvey(data), animationDurations: spineAnimationDurations(data), twoColour: spineTwoColourFacts(raw, data), sequences: spineSequenceFacts(raw, data) };
|
|
1964
|
+
return { deformSurvey: spineDeformSurvey(data), animationDurations: spineAnimationDurations(data), twoColour: spineTwoColourFacts(raw, data), sequences: spineSequenceFacts(raw, data), steppedPoses: spineSteppedPoses(raw, data), boneTimelines: rawBoneTimelines(raw) };
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* Cut 4c-5's facts (issue #1025) as `validate()` supplies them from a pair
|
|
1969
|
+
* spine-core loads — A40's sliders and A34's constraint groups, with the
|
|
1970
|
+
* constraint facts A40 reads beside them — or `null` when the load throws.
|
|
1971
|
+
* For the selftest's `VF14` and `tools/verdict_gate.ts`, which hand them to
|
|
1972
|
+
* the bodies and ask the model side's suppliers the same questions.
|
|
1973
|
+
*/
|
|
1974
|
+
export function runtimeCut4c5Facts(skeletonText: string, atlasText: string): { sliderComposition: SliderCompositionFacts; constraintTargets: ConstraintTargetFacts; constraints: ConstraintFacts } | null {
|
|
1975
|
+
let data: ReturnType<SkeletonJson['readSkeletonData']>;
|
|
1976
|
+
let raw: Json;
|
|
1977
|
+
try {
|
|
1978
|
+
raw = JSON.parse(skeletonText) as Json;
|
|
1979
|
+
data = new SkeletonJson(new AtlasAttachmentLoader(new TextureAtlas(atlasText))).readSkeletonData(JSON.parse(skeletonText));
|
|
1980
|
+
} catch {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
return { sliderComposition: spineSliderComposition(data), constraintTargets: rawConstraintTargets(raw), constraints: spineConstraintFacts(data) };
|
|
1673
1984
|
}
|
|
1674
1985
|
|
|
1675
1986
|
/** A fact supply computed on first use and kept: a supplier that throws throws inside the `check` that asked, as the body it feeds always did. */
|
|
@@ -1982,129 +2293,15 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1982
2293
|
// constant now, and the message that names the timelines a group takes reads
|
|
1983
2294
|
// them off `CHANNELS_BY_KIND` — it used to be a ternary over two groups,
|
|
1984
2295
|
// which is a sentence that cannot be extended without being rewritten.
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
const kindsOf = new Map<string, string[]>();
|
|
1995
|
-
for (const entry of Array.isArray(raw.constraints) ? (raw.constraints as unknown[]) : []) {
|
|
1996
|
-
if (isObj(entry) && typeof entry.name === 'string') {
|
|
1997
|
-
kindsOf.set(entry.name, [...(kindsOf.get(entry.name) ?? []), String(entry.type)]);
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
/** The file's physics constraints, which the unnamed physics group is read against. */
|
|
2001
|
-
const rawPhysics = (Array.isArray(raw.constraints) ? (raw.constraints as unknown[]) : []).filter(
|
|
2002
|
-
(entry): entry is Json => isObj(entry) && entry.type === 'physics',
|
|
2003
|
-
);
|
|
2004
|
-
let sawATimeline = false;
|
|
2005
|
-
/** One target of one group: the name resolves, the type matches, keys exist. */
|
|
2006
|
-
const checkTarget = (at: string, group: string, name: string, keyArrays: Array<[string, unknown]>): void => {
|
|
2007
|
-
sawATimeline = true;
|
|
2008
|
-
const declared = kindsOf.get(name) ?? [];
|
|
2009
|
-
if (declared.length === 0) {
|
|
2010
|
-
const known = [...kindsOf.entries()].filter(([, kinds]) => kinds.includes(group)).map(([n]) => n);
|
|
2011
|
-
fail(
|
|
2012
|
-
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
2013
|
-
`${at}: the skeleton's constraints array has no "${name}"` +
|
|
2014
|
-
(known.length ? ` (${group} constraints: ${known.join(', ')})` : `, and no ${group} constraint at all`),
|
|
2015
|
-
);
|
|
2016
|
-
return;
|
|
2017
|
-
}
|
|
2018
|
-
if (!declared.includes(group)) {
|
|
2019
|
-
fail(
|
|
2020
|
-
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
2021
|
-
`${at}: "${name}" is declared as a "${declared.join('"/"')}" constraint, so the ${group} lookup misses it and the loader throws`,
|
|
2022
|
-
);
|
|
2023
|
-
return;
|
|
2024
|
-
}
|
|
2025
|
-
checkKeys(at, keyArrays);
|
|
2026
|
-
};
|
|
2027
|
-
/** The key arrays of one group entry that resolved: each one is walked, so each has to hold a key. */
|
|
2028
|
-
const checkKeys = (at: string, keyArrays: Array<[string, unknown]>): void => {
|
|
2029
|
-
if (keyArrays.length === 0) {
|
|
2030
|
-
fail(
|
|
2031
|
-
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
2032
|
-
`${at}: the constraint is named and carries no timeline at all; the group is walked and nothing happens`,
|
|
2033
|
-
);
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
for (const [timeline, keys] of keyArrays) {
|
|
2037
|
-
if (Array.isArray(keys) && keys.length > 0) continue;
|
|
2038
|
-
fail(
|
|
2039
|
-
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
2040
|
-
`${at}${timeline ? ` timeline "${timeline}"` : ''}: the key array is ` +
|
|
2041
|
-
`${Array.isArray(keys) ? 'empty' : JSON.stringify(keys)}; the parser reads key 0, finds nothing and ` +
|
|
2042
|
-
'skips the whole timeline without a word',
|
|
2043
|
-
);
|
|
2044
|
-
}
|
|
2045
|
-
};
|
|
2046
|
-
for (const [animName, anim] of Object.entries(raw.animations as Json)) {
|
|
2047
|
-
if (!isObj(anim)) continue;
|
|
2048
|
-
// group.<constraint> = keys[]
|
|
2049
|
-
for (const group of ['ik', 'transform'] as const) {
|
|
2050
|
-
if (!isObj(anim[group])) continue;
|
|
2051
|
-
for (const [name, keys] of Object.entries(anim[group] as Json)) {
|
|
2052
|
-
checkTarget(`animation "${animName}" ${group} timeline "${name}"`, group, name, [['', keys]]);
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
// group.<constraint>.<timeline> = keys[]
|
|
2056
|
-
for (const group of NAMED_TIMELINE_GROUPS) {
|
|
2057
|
-
if (!isObj(anim[group])) continue;
|
|
2058
|
-
for (const [name, timelines] of Object.entries(anim[group] as Json)) {
|
|
2059
|
-
const at = `animation "${animName}" ${group} constraint "${name}"`;
|
|
2060
|
-
if (!isObj(timelines)) {
|
|
2061
|
-
sawATimeline = true;
|
|
2062
|
-
fail(
|
|
2063
|
-
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
2064
|
-
`${at}: this group maps a constraint to NAMED timelines ` +
|
|
2065
|
-
`(${Object.keys(CHANNELS_BY_KIND[group]).join('/')}), and this one holds ${JSON.stringify(timelines)} — ` +
|
|
2066
|
-
'a bare key array here is the ik/transform shape and is walked as an object',
|
|
2067
|
-
);
|
|
2068
|
-
continue;
|
|
2069
|
-
}
|
|
2070
|
-
// The empty name is not a miss: it is the physics group's global form,
|
|
2071
|
-
// which `SkeletonJson` loads as `constraintIndex -1` rather than looking
|
|
2072
|
-
// anything up (`:1048-1054`), and which writes every physics constraint
|
|
2073
|
-
// that declares the timeline's property global (issue #726). Refusing
|
|
2074
|
-
// it as "no constraint called ''" was a sentence that is false about
|
|
2075
|
-
// the file. What CAN be wrong with it is that it reaches nobody: the
|
|
2076
|
-
// parser accepts it, the runtime walks every constraint, and none of
|
|
2077
|
-
// them takes the key.
|
|
2078
|
-
if (group === 'physics' && name === '') {
|
|
2079
|
-
sawATimeline = true;
|
|
2080
|
-
for (const [timelineName] of Object.entries(timelines)) {
|
|
2081
|
-
const timeline = unnamedPhysicsTimeline(timelineName);
|
|
2082
|
-
// A name the parser skips is skipped here too: no timeline exists
|
|
2083
|
-
// to reach anybody, and whether the NAME is right is not a target
|
|
2084
|
-
// question.
|
|
2085
|
-
if (timeline === null || unnamedPhysicsReach(timeline, rawPhysics).length > 0) continue;
|
|
2086
|
-
const resets = timeline instanceof PhysicsConstraintResetTimeline;
|
|
2087
|
-
fail(
|
|
2088
|
-
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
2089
|
-
`${at} timeline "${timelineName}": a physics group that names no constraint writes every physics ` +
|
|
2090
|
-
`constraint ${resets ? 'the skeleton has' : `declaring "${timelineName}Global": true`}, and ` +
|
|
2091
|
-
(rawPhysics.length === 0
|
|
2092
|
-
? 'the skeleton has no physics constraint'
|
|
2093
|
-
: `none of ${rawPhysics.map((one) => `"${String(one.name)}"`).join(', ')} does`) +
|
|
2094
|
-
' — the parser loads it, the runtime walks every constraint, and no constraint takes the key',
|
|
2095
|
-
);
|
|
2096
|
-
}
|
|
2097
|
-
checkKeys(at, Object.entries(timelines));
|
|
2098
|
-
continue;
|
|
2099
|
-
}
|
|
2100
|
-
checkTarget(at, group, name, Object.entries(timelines));
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
if (!sawATimeline) {
|
|
2105
|
-
return skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'no animation carries a constraint timeline');
|
|
2106
|
-
}
|
|
2107
|
-
});
|
|
2296
|
+
//
|
|
2297
|
+
// ✂️ Moved whole since issue #1025 (cut 4c-5): the body is
|
|
2298
|
+
// `./assertions/bodies/a34.ts`, over this file's constraint groups as the
|
|
2299
|
+
// raw JSON states them (`rawConstraintTargets`), and whom a physics timeline
|
|
2300
|
+
// naming no constraint reaches is still the parser's timeline asked of the
|
|
2301
|
+
// file's constraint objects, through the facts.
|
|
2302
|
+
check('A34_CONSTRAINT_TIMELINE_TARGETS', () =>
|
|
2303
|
+
raw ? a34ConstraintTimelineTargets(verdicts, rawConstraintTargets(raw)) : skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'the skeleton JSON did not parse (A00 owns that failure)'),
|
|
2304
|
+
);
|
|
2108
2305
|
|
|
2109
2306
|
// --- A35: a deform key's run lands inside the attachment it edits ---------
|
|
2110
2307
|
//
|
|
@@ -2757,127 +2954,13 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2757
2954
|
// measurement instead of two exceptions. A list of unobservable spellings
|
|
2758
2955
|
// written into this file would have been the hand-kept table that produced
|
|
2759
2956
|
// the defect above.
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
}
|
|
2768
|
-
// Clause 1 of the "could this be correct?" list above.
|
|
2769
|
-
// "Keyed at all" rather than "keyed live", and on purpose: this clause asks
|
|
2770
|
-
// whether the mix can MOVE from its setup value, so any mix timeline
|
|
2771
|
-
// disqualifies it — the question `keyedBy` answered here, through the one
|
|
2772
|
-
// reading `switchedOn` gives (`./assertions/constraint_words.ts`, over
|
|
2773
|
-
// the same facts the moved constraint bodies read — issue #1025), and
|
|
2774
|
-
// unchanged by issue #752.
|
|
2775
|
-
const mixKeyed = switchedOn(constraintFacts(), (timeline) => timeline.kind === 'slider' && timeline.word === 'mix', 1, () => true);
|
|
2776
|
-
const authoritative = sliders.filter((s) => s.setupPose.mix >= 1 && !mixKeyed.has(data.constraints.indexOf(s)));
|
|
2777
|
-
if (authoritative.length < 2) {
|
|
2778
|
-
return skip(
|
|
2779
|
-
'A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET',
|
|
2780
|
-
`${authoritative.length} of the ${sliders.length} slider constraints apply at full authority; below mix 1 an ` +
|
|
2781
|
-
'apply is a lerp from the current pose rather than an overwrite, so what the others do to a shared property is a weighting',
|
|
2782
|
-
);
|
|
2783
|
-
}
|
|
2784
|
-
/** Which skins switch a slider on, or null when it is active under every skin. */
|
|
2785
|
-
const skinsOf = new Map<SliderData, Set<string> | null>();
|
|
2786
|
-
for (const slider of authoritative) {
|
|
2787
|
-
if (!slider.skinRequired) {
|
|
2788
|
-
skinsOf.set(slider, null);
|
|
2789
|
-
continue;
|
|
2790
|
-
}
|
|
2791
|
-
const names = new Set<string>();
|
|
2792
|
-
for (const skin of data.skins) {
|
|
2793
|
-
if (skin.constraints.includes(slider)) names.add(skin.name);
|
|
2794
|
-
}
|
|
2795
|
-
skinsOf.set(slider, names);
|
|
2796
|
-
}
|
|
2797
|
-
/** Clause 2: can these two ever run in the same frame? */
|
|
2798
|
-
const canOverlap = (a: SliderData, b: SliderData): boolean => {
|
|
2799
|
-
const skinsA = skinsOf.get(a) ?? null;
|
|
2800
|
-
const skinsB = skinsOf.get(b) ?? null;
|
|
2801
|
-
if (!skinsA || !skinsB) return true;
|
|
2802
|
-
for (const name of skinsA) {
|
|
2803
|
-
if (skinsB.has(name)) return true;
|
|
2804
|
-
}
|
|
2805
|
-
return false;
|
|
2806
|
-
};
|
|
2807
|
-
/** What a property id points at, in the words the rig spec uses. */
|
|
2808
|
-
const describe = (timeline: Timeline, id: string): string => {
|
|
2809
|
-
const property = Property[Number(id.split('|')[0])] ?? id;
|
|
2810
|
-
if (isBoneTimeline(timeline)) return `bone "${data.bones[timeline.boneIndex]?.name ?? timeline.boneIndex}" ${property}`;
|
|
2811
|
-
if (timeline instanceof DeformTimeline) {
|
|
2812
|
-
return `slot "${data.slots[timeline.slotIndex]?.name ?? timeline.slotIndex}" deform of "${timeline.attachment.name}"`;
|
|
2813
|
-
}
|
|
2814
|
-
if (isSlotTimeline(timeline)) return `slot "${data.slots[timeline.slotIndex]?.name ?? timeline.slotIndex}" ${property}`;
|
|
2815
|
-
if (isConstraintTimeline(timeline) && timeline.constraintIndex >= 0) {
|
|
2816
|
-
return `constraint "${data.constraints[timeline.constraintIndex]?.name ?? timeline.constraintIndex}" ${property}`;
|
|
2817
|
-
}
|
|
2818
|
-
return `the skeleton's ${property}`;
|
|
2819
|
-
};
|
|
2820
|
-
/** property id -> the sliders whose animation keys it, in constraints-array order. */
|
|
2821
|
-
const byProperty = new Map<string, Array<{ slider: SliderData; timeline: Timeline }>>();
|
|
2822
|
-
for (const slider of authoritative) {
|
|
2823
|
-
const seen = new Set<string>();
|
|
2824
|
-
for (const timeline of slider.animation?.timelines ?? []) {
|
|
2825
|
-
for (const id of timeline.propertyIds) {
|
|
2826
|
-
if (seen.has(id)) continue;
|
|
2827
|
-
seen.add(id);
|
|
2828
|
-
byProperty.set(id, [...(byProperty.get(id) ?? []), { slider, timeline }]);
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
/** Posed once per timeline, because the answer is the class's and the rigs that reach here share timelines. */
|
|
2833
|
-
const behaviour = new Map<Timeline, TimelineAddBehaviour>();
|
|
2834
|
-
const addBehaviourOf = (timeline: Timeline): TimelineAddBehaviour => {
|
|
2835
|
-
const known = behaviour.get(timeline);
|
|
2836
|
-
if (known !== undefined) return known;
|
|
2837
|
-
const measured = timelineAddBehaviour(data, timeline);
|
|
2838
|
-
behaviour.set(timeline, measured);
|
|
2839
|
-
return measured;
|
|
2840
|
-
};
|
|
2841
|
-
let shared = 0;
|
|
2842
|
-
for (const [id, users] of byProperty) {
|
|
2843
|
-
if (users.length < 2) continue;
|
|
2844
|
-
shared++;
|
|
2845
|
-
const at = (slider: SliderData): string =>
|
|
2846
|
-
`"${slider.name}" (constraints[${data.constraints.indexOf(slider)}], additive: ${String(slider.additive)})`;
|
|
2847
|
-
const chain = users.map((u) => at(u.slider)).join(', ');
|
|
2848
|
-
for (let j = 1; j < users.length; j++) {
|
|
2849
|
-
const later = users[j];
|
|
2850
|
-
const composes = addBehaviourOf(later.timeline);
|
|
2851
|
-
if (composes === 'accumulates' && later.slider.additive) continue;
|
|
2852
|
-
// Nothing a second slider could take away: applied with the arguments
|
|
2853
|
-
// a slider passes — `firedEvents` null among them — this timeline
|
|
2854
|
-
// moves no pose at all.
|
|
2855
|
-
if (composes === 'inert') continue;
|
|
2856
|
-
const erased = users.slice(0, j).filter((e) => canOverlap(e.slider, later.slider));
|
|
2857
|
-
if (!erased.length) continue;
|
|
2858
|
-
const erasedNames = `${erased.map((e) => `"${e.slider.name}"`).join(', ')} contribute${erased.length === 1 ? 's' : ''}`;
|
|
2859
|
-
const why =
|
|
2860
|
-
composes === 'accumulates'
|
|
2861
|
-
? `slider "${later.slider.name}" applies animation "${later.slider.animation?.name}" with additive false, and at ` +
|
|
2862
|
-
'mix 1 a non-additive apply writes the value outright (`getRelativeValue` returns `setup + value`, ' +
|
|
2863
|
-
'`getAbsoluteValue` returns `value`) rather than adding to the pose it found. Set `"additive": true` on ' +
|
|
2864
|
-
`slider "${later.slider.name}" in the rig spec — rigc will not choose that flag for you — or key this ` +
|
|
2865
|
-
`property from one slider only. [measured] \`${later.timeline.constructor.name}.apply\` posed twice with ` +
|
|
2866
|
-
'`add` set accumulates, so that flag is the repair here'
|
|
2867
|
-
: `the ${Property[Number(id.split('|')[0])] ?? id} timeline they share writes its value outright whatever the ` +
|
|
2868
|
-
`flags say — [measured] \`${later.timeline.constructor.name}.apply\` posed twice with \`add\` set left the ` +
|
|
2869
|
-
'same value there rather than adding to it — so `"additive": true` would NOT compose these. Key this ' +
|
|
2870
|
-
'property from one slider only, or move both edits into the one animation a single slider applies';
|
|
2871
|
-
fail(
|
|
2872
|
-
'A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET',
|
|
2873
|
-
`${describe(later.timeline, id)} is keyed by the animations of ${users.length} sliders — ${chain} — and every ` +
|
|
2874
|
-
`one of them applies at mix 1. Today ${at(later.slider)} wins that property and ${erasedNames} ` +
|
|
2875
|
-
`nothing to it: ${why}.`,
|
|
2876
|
-
);
|
|
2877
|
-
}
|
|
2878
|
-
}
|
|
2879
|
-
stats.sliderSharedTargets = shared;
|
|
2880
|
-
});
|
|
2957
|
+
//
|
|
2958
|
+
// ✂️ Moved whole since issue #1025 (cut 4c-5): the body is
|
|
2959
|
+
// `./assertions/bodies/a40.ts`, over the sliders and their animations'
|
|
2960
|
+
// timelines (`spineSliderComposition`) and the constraint facts, and what a
|
|
2961
|
+
// timeline does with `add` is still this probe, `timelineAddBehaviour`,
|
|
2962
|
+
// asked by the body through the facts.
|
|
2963
|
+
check('A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET', () => a40SlidersComposeOnASharedTarget(verdicts, spineSliderComposition(data), constraintFacts()));
|
|
2881
2964
|
|
|
2882
2965
|
// --- A42: a dial that drives a constraint the array already ran ---------
|
|
2883
2966
|
//
|
|
@@ -2967,206 +3050,98 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2967
3050
|
check('A09_ANIMATION_DURATION_MATCHES_SPEC', () => a09AnimationDurationMatchesSpec(verdicts, spineAnimationDurations(data), input.declaredDurations));
|
|
2968
3051
|
|
|
2969
3052
|
// --- A10: step every animation and look for NaN ------------------------
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
// gate, and `render` then refused the same file by the bone. So the gate
|
|
3063
|
-
// said the skeleton was fine and the renderer said it was not, which is two
|
|
3064
|
-
// definitions of one word. The scan reads the six terms of every bone, then
|
|
3065
|
-
// the vertices of every region and mesh shown — because a bone can be
|
|
3066
|
-
// finite over a vertex that is not (a `scaleX` chain whose product stays
|
|
3067
|
-
// under the largest double while every corner of the child's region
|
|
3068
|
-
// passes it) — and names the bone or the vertex, the term and the frame.
|
|
3069
|
-
//
|
|
3070
|
-
// ⚠️ The setup pose is its own surface and is read as such: every frame
|
|
3071
|
-
// below is posed AFTER `state.apply`, so a bone the animation keys from
|
|
3072
|
-
// t=0 never shows its setup value to the loop, and a runtime that shows
|
|
3073
|
-
// the rig at rest does show it.
|
|
3074
|
-
//
|
|
3075
|
-
// 🔸 Posed once, before any animation is set, rather than once per
|
|
3076
|
-
// animation as it was until #902: `setAnimation` does not touch the
|
|
3077
|
-
// skeleton, so every animation's copy of this pose was the same pose, and
|
|
3078
|
-
// the first of them was the only one ever read.
|
|
3079
|
-
if (data.animations.length === 0) stats.nanStepping = 'skipped';
|
|
3080
|
-
const atRest = new Skeleton(data);
|
|
3081
|
-
atRest.setupPose();
|
|
3082
|
-
atRest.update(0);
|
|
3083
|
-
atRest.updateWorldTransform(Physics.reset);
|
|
3084
|
-
const atSetup = nonFiniteOfPosed('the setup pose', atRest);
|
|
3085
|
-
if (atSetup !== null) {
|
|
3086
|
-
fail('A10_NO_NAN_AFTER_STEPPING', atSetup);
|
|
3087
|
-
return;
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
/**
|
|
3091
|
-
* What a posed frame shows that the world-transform scan does not read: a
|
|
3092
|
-
* bone posing no inheritance mode, and a slot colour that is not finite.
|
|
3093
|
-
* `where` leads the sentence — the animation's name at a stepped frame,
|
|
3094
|
-
* as it always has, or `the setup pose` on a skeleton with no animation.
|
|
3095
|
-
*/
|
|
3096
|
-
const poseDefect = (where: string, skeleton: Skeleton): string | null => {
|
|
3097
|
-
for (const bone of skeleton.bones) {
|
|
3098
|
-
const pose = bone.appliedPose;
|
|
3099
|
-
// The setup half of the inherit-key clause above: a bone's own
|
|
3100
|
-
// `inherit` goes through the same lookup, and a miss there loads
|
|
3101
|
-
// `undefined` into the setup pose, which every frame copies. Only
|
|
3102
|
-
// reachable on a file rigc did not write — `parseRigSpec` refuses the
|
|
3103
|
-
// spelling.
|
|
3104
|
-
if (!isMode(pose.inherit)) {
|
|
3105
|
-
const rawBone = Array.isArray(raw?.bones)
|
|
3106
|
-
? (raw.bones as unknown[]).find((b) => isObj(b) && b.name === bone.data.name)
|
|
3107
|
-
: undefined;
|
|
3108
|
-
return (
|
|
3109
|
-
`${where}: bone "${bone.data.name}" poses inheritance mode ${String(pose.inherit)} — its setup ` +
|
|
3110
|
-
`spells inherit ${JSON.stringify(isObj(rawBone) ? rawBone.inherit : undefined)}, which the ` +
|
|
3111
|
-
`runtime's mode lookup does not resolve (${BONE_INHERIT_KNOWN}), so its world rotation, scale and shear ` +
|
|
3112
|
-
'are never computed — they stay 0 and everything the bone carries collapses to a point'
|
|
3113
|
-
);
|
|
3114
|
-
}
|
|
3115
|
-
}
|
|
3116
|
-
for (const slot of skeleton.slots) {
|
|
3117
|
-
const c = slot.appliedPose.color;
|
|
3118
|
-
if (![c.r, c.g, c.b, c.a].every(Number.isFinite)) return `${where}: slot "${slot.data.name}" colour is non-finite`;
|
|
3119
|
-
// The other colour a slot poses, and it was outside this loop until
|
|
3120
|
-
// issue #690 for the reason every gap here has: nothing emitted one.
|
|
3121
|
-
// `null` is the ordinary case — a slot with no `dark` allocates no
|
|
3122
|
-
// dark colour at all — and is not a reading to make, so it is
|
|
3123
|
-
// skipped rather than treated as zero.
|
|
3124
|
-
const d = slot.appliedPose.darkColor;
|
|
3125
|
-
if (d !== null && ![d.r, d.g, d.b].every(Number.isFinite)) {
|
|
3126
|
-
return `${where}: slot "${slot.data.name}" dark colour is non-finite`;
|
|
3127
|
-
}
|
|
3128
|
-
}
|
|
3129
|
-
return null;
|
|
3130
|
-
};
|
|
3131
|
-
|
|
3132
|
-
// The stepping half, and on a static rig there is nothing to step: the
|
|
3133
|
-
// setup pose is then the only frame the skeleton has, and the two readings
|
|
3134
|
-
// a stepped frame gets are made on it instead, so a colour or an
|
|
3135
|
-
// inheritance mode the rig shows at rest is not left to `render`.
|
|
3136
|
-
if (data.animations.length === 0) {
|
|
3137
|
-
const atRestDefect = poseDefect('the setup pose', atRest);
|
|
3138
|
-
if (atRestDefect !== null) fail('A10_NO_NAN_AFTER_STEPPING', atRestDefect);
|
|
3139
|
-
return;
|
|
3140
|
-
}
|
|
3141
|
-
for (const anim of data.animations) {
|
|
3142
|
-
const skeleton = new Skeleton(data);
|
|
3143
|
-
const state = new AnimationState(new AnimationStateData(data));
|
|
3144
|
-
state.setAnimation(0, anim.name, true);
|
|
3145
|
-
skeleton.setupPose();
|
|
3146
|
-
skeleton.update(0);
|
|
3147
|
-
skeleton.updateWorldTransform(Physics.reset);
|
|
3148
|
-
const step = Math.max(anim.duration, 1) / STEP_FRAMES;
|
|
3149
|
-
for (let i = 0; i < STEP_FRAMES; i++) {
|
|
3150
|
-
state.update(step);
|
|
3151
|
-
state.apply(skeleton);
|
|
3152
|
-
skeleton.update(step);
|
|
3153
|
-
skeleton.updateWorldTransform(Physics.update);
|
|
3154
|
-
const found = nonFiniteOfPosed(
|
|
3155
|
-
`animation ${JSON.stringify(anim.name)} frame ${i + 1} of ${STEP_FRAMES} (t=${((i + 1) * step).toFixed(4)}s)`,
|
|
3156
|
-
skeleton,
|
|
3157
|
-
);
|
|
3158
|
-
if (found !== null) {
|
|
3159
|
-
fail('A10_NO_NAN_AFTER_STEPPING', found);
|
|
3160
|
-
return;
|
|
3161
|
-
}
|
|
3162
|
-
const defect = poseDefect(anim.name, skeleton);
|
|
3163
|
-
if (defect !== null) {
|
|
3164
|
-
fail('A10_NO_NAN_AFTER_STEPPING', defect);
|
|
3165
|
-
return;
|
|
3166
|
-
}
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
|
-
});
|
|
3053
|
+
// The body is `./assertions/bodies/a10.ts` since issue #1025 (cut 4c-5a); what it
|
|
3054
|
+
// reads off the loaded skeleton is `spineSteppedPoses` (and the raw JSON's bone
|
|
3055
|
+
// timelines, `rawBoneTimelines`). The argument for each clause, as it stood
|
|
3056
|
+
// inside the body:
|
|
3057
|
+
//
|
|
3058
|
+
// Two clauses, and since issue #902 each is read on its own subject. The
|
|
3059
|
+
// SETUP POSE is posed from the bones, so it has something to read on any
|
|
3060
|
+
// skeleton that carries one; the STEPPED FRAMES are posed once per
|
|
3061
|
+
// animation, so a skeleton with none has nothing to step. This used to
|
|
3062
|
+
// skip the whole rule on the second fact alone, which was A15's pattern
|
|
3063
|
+
// (#580) for a rule whose only clause was the stepping — and #882 gave it
|
|
3064
|
+
// the setup clause without moving the skip. Measured on the static probe
|
|
3065
|
+
// with a leaf bone at `rotation: 1e309` in its emitted file: `validate`
|
|
3066
|
+
// printed this rule as SKIP and the run green under both profiles, and
|
|
3067
|
+
// `render` refused the same file by the bone. A static rig's setup pose
|
|
3068
|
+
// is the whole of what it shows, so that was the one pose nothing read.
|
|
3069
|
+
//
|
|
3070
|
+
// ⇒ The multi-clause rule of #580 now governs, the shape A09, A13, A33
|
|
3071
|
+
// and A38 carry: the rule SKIPs only when neither clause has anything to
|
|
3072
|
+
// measure, the clause that ran decides PASS or FAIL, and the clause that
|
|
3073
|
+
// had nothing is named on the stats line (`nanStepping=skipped`, beside
|
|
3074
|
+
// `animations=0`) the way A47 names a constraint it did not measure. A
|
|
3075
|
+
// PASS row carries no detail, which is why the stats line is where.
|
|
3076
|
+
//
|
|
3077
|
+
// ⚠️ A09 does not follow, and that is its own name read at its word: a
|
|
3078
|
+
// declared duration is a fact about an animation and nothing else, so a
|
|
3079
|
+
// static rig still gives it nothing at all.
|
|
3080
|
+
//
|
|
3081
|
+
// -- the bone's inheritance mode, posed at every `inherit` key ---------
|
|
3082
|
+
//
|
|
3083
|
+
// 🚨 The one bone timeline whose value is a NAME, and the only one that
|
|
3084
|
+
// can pose a NaN with a finite world: `SkeletonJson` resolves a key's mode
|
|
3085
|
+
// through `Utils.enumValue`, which folds the first letter's case and
|
|
3086
|
+
// nothing else, so `"NOSCALE"` resolves to `undefined` and the timeline's
|
|
3087
|
+
// `Float32Array` frame stores NaN. `InheritTimeline.apply` then sets
|
|
3088
|
+
// `pose.inherit = NaN`, `updateWorldTransform`'s switch matches no case,
|
|
3089
|
+
// and the bone keeps whatever world matrix it had — measured on a forged
|
|
3090
|
+
// two-bone chain: at the key the child's `a,b,c,d` equal the setup
|
|
3091
|
+
// Normal-mode pose to the last digit while the file says `noScale`. The
|
|
3092
|
+
// world position stays finite, so the loop below never saw it (#733).
|
|
3093
|
+
//
|
|
3094
|
+
// ⚠️ Posed AT each key rather than read off the stepping loop, because a
|
|
3095
|
+
// stepped value lives from its key to the next one and a sampling grid
|
|
3096
|
+
// can step over a short span entirely. What is judged is whether the
|
|
3097
|
+
// posed value IS a mode — which is this assertion's name exactly — and
|
|
3098
|
+
// not which mode: for any spelling the lookup resolves, the mode posed
|
|
3099
|
+
// is the mode written by construction of the same lookup, so an equality
|
|
3100
|
+
// here would be the parser agreeing with itself.
|
|
3101
|
+
//
|
|
3102
|
+
// -- the whole world transform, at the setup pose and every stepped frame
|
|
3103
|
+
//
|
|
3104
|
+
// 🚨 Read through `firstNonFinite` (`./nonfinite.ts`) over `posedNumbersOf`,
|
|
3105
|
+
// the scan `render` refuses on, and not a second opinion of it (issue #882). This loop read `worldX` and
|
|
3106
|
+
// `worldY` alone until then, and a bone at `rotation: 1e309` — in its setup
|
|
3107
|
+
// pose or as a `rotate` key — poses a finite position over a NaN `a`, `b`,
|
|
3108
|
+
// `c` and `d` whenever it has no child offset from it: measured on every
|
|
3109
|
+
// leaf bone of the three generated probes, 20 of 20 green through the whole
|
|
3110
|
+
// gate, and `render` then refused the same file by the bone. So the gate
|
|
3111
|
+
// said the skeleton was fine and the renderer said it was not, which is two
|
|
3112
|
+
// definitions of one word. The scan reads the six terms of every bone, then
|
|
3113
|
+
// the vertices of every region and mesh shown — because a bone can be
|
|
3114
|
+
// finite over a vertex that is not (a `scaleX` chain whose product stays
|
|
3115
|
+
// under the largest double while every corner of the child's region
|
|
3116
|
+
// passes it) — and names the bone or the vertex, the term and the frame.
|
|
3117
|
+
//
|
|
3118
|
+
// ⚠️ The setup pose is its own surface and is read as such: every frame
|
|
3119
|
+
// below is posed AFTER `state.apply`, so a bone the animation keys from
|
|
3120
|
+
// t=0 never shows its setup value to the loop, and a runtime that shows
|
|
3121
|
+
// the rig at rest does show it.
|
|
3122
|
+
//
|
|
3123
|
+
// 🔸 Posed once, before any animation is set, rather than once per
|
|
3124
|
+
// animation as it was until #902: `setAnimation` does not touch the
|
|
3125
|
+
// skeleton, so every animation's copy of this pose was the same pose, and
|
|
3126
|
+
// the first of them was the only one ever read.
|
|
3127
|
+
//
|
|
3128
|
+
// The setup half of the inherit-key clause above: a bone's own
|
|
3129
|
+
// `inherit` goes through the same lookup, and a miss there loads
|
|
3130
|
+
// `undefined` into the setup pose, which every frame copies. Only
|
|
3131
|
+
// reachable on a file rigc did not write — `parseRigSpec` refuses the
|
|
3132
|
+
// spelling.
|
|
3133
|
+
//
|
|
3134
|
+
// The other colour a slot poses, and it was outside this loop until
|
|
3135
|
+
// issue #690 for the reason every gap here has: nothing emitted one.
|
|
3136
|
+
// `null` is the ordinary case — a slot with no `dark` allocates no
|
|
3137
|
+
// dark colour at all — and is not a reading to make, so it is
|
|
3138
|
+
// skipped rather than treated as zero.
|
|
3139
|
+
//
|
|
3140
|
+
// The stepping half, and on a static rig there is nothing to step: the
|
|
3141
|
+
// setup pose is then the only frame the skeleton has, and the two readings
|
|
3142
|
+
// a stepped frame gets are made on it instead, so a colour or an
|
|
3143
|
+
// inheritance mode the rig shows at rest is not left to `render`.
|
|
3144
|
+
check('A10_NO_NAN_AFTER_STEPPING', () => a10NoNanAfterStepping(verdicts, rawBoneTimelines(raw), spineSteppedPoses(raw, data)));
|
|
3170
3145
|
|
|
3171
3146
|
// --- A43: the two-colour tint, read back off the runtime ----------------
|
|
3172
3147
|
//
|