spine-rigc 1.9.0 → 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 +48 -17
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/bone_timelines.ts +56 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +34 -16
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/index.ts +83 -2
- package/src/assertions/model/linked_meshes.ts +2 -2
- package/src/assertions/model/mesh_attachments.ts +1 -1
- package/src/assertions/model/region_joins.ts +20 -12
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +18 -8
- package/src/assertions/model/skin_members.ts +5 -3
- package/src/assertions/model/skin_meshes.ts +1 -1
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/slot_colour.ts +5 -4
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +9 -8
- package/src/bonedist.ts +18 -6
- package/src/compile.ts +114 -14
- 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/deformmeasure.ts +35 -2270
- package/src/deformstructure.ts +60 -18
- package/src/deformsurvey.ts +2295 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +200 -59
- package/src/render_core.ts +5 -0
- package/src/validate.ts +791 -1558
package/src/nonfinite.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one sentence for a pose number that is not finite (issue #882, #873) —
|
|
3
|
+
* moved here from `src/render.ts` by issue #1025 (cut 4c-5a of step 4c of
|
|
4
|
+
* #380), unchanged, because it now has a reader that may not link the
|
|
5
|
+
* runtime: `A10_NO_NAN_AFTER_STEPPING`'s body (`./assertions/bodies/a10.ts`),
|
|
6
|
+
* which reads it over the poses spine-core gives and over the poses rigc's
|
|
7
|
+
* core gives. `render` refuses on the same sentence, so the gate and the
|
|
8
|
+
* renderer still hold one definition of "not finite" — the six terms of a
|
|
9
|
+
* bone's world transform, then the vertices computed from them.
|
|
10
|
+
*
|
|
11
|
+
* Pure: no import at all.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The bone fields a world transform is made of — what `firstNonFinite` reads off a bone. */
|
|
15
|
+
export interface WorldTransform {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly a: number;
|
|
18
|
+
readonly b: number;
|
|
19
|
+
readonly c: number;
|
|
20
|
+
readonly d: number;
|
|
21
|
+
readonly worldX: number;
|
|
22
|
+
readonly worldY: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The fields of an attachment entry `firstNonFinite` reads — a frame's posed attachment or a rest entry. */
|
|
26
|
+
export interface PosedVertices {
|
|
27
|
+
readonly slot: string;
|
|
28
|
+
readonly attachment: string;
|
|
29
|
+
readonly vertices: readonly number[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The sentence for the first number at `where` that is not finite — bones before
|
|
34
|
+
* vertices, since a bone that overflowed is the cause and its vertices the
|
|
35
|
+
* symptom — or `null` when every number there is finite.
|
|
36
|
+
*/
|
|
37
|
+
export function firstNonFinite(where: string, entries: readonly PosedVertices[], bones: readonly WorldTransform[]): string | null {
|
|
38
|
+
for (const bone of bones) {
|
|
39
|
+
for (const field of ['a', 'b', 'c', 'd', 'worldX', 'worldY'] as const) {
|
|
40
|
+
if (!Number.isFinite(bone[field])) {
|
|
41
|
+
return `${where}: bone ${JSON.stringify(bone.name)} has ${field} ${String(bone[field])}; a world transform is finite`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const bad = entry.vertices.findIndex((value) => !Number.isFinite(value));
|
|
47
|
+
if (bad === -1) continue;
|
|
48
|
+
return (
|
|
49
|
+
`${where}: slot ${JSON.stringify(entry.slot)} attachment ${JSON.stringify(entry.attachment)} vertex ` +
|
|
50
|
+
`${Math.floor(bad / 2)} has ${bad % 2 === 0 ? 'x' : 'y'} ${String(entry.vertices[bad])}; a posed vertex is finite`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -126,6 +126,7 @@ import {
|
|
|
126
126
|
type SubsetRoster,
|
|
127
127
|
} from './render_core.ts';
|
|
128
128
|
import { walkTimelines } from './timelines.ts';
|
|
129
|
+
import { firstNonFinite, type PosedVertices, type WorldTransform } from './nonfinite.ts';
|
|
129
130
|
|
|
130
131
|
/** Opaque, and light: both of rung 3's parts are dark slate, so is every ground. */
|
|
131
132
|
export const BACKGROUND: RGBA = [232, 232, 232, 255];
|
|
@@ -891,10 +892,20 @@ function skeletonDurations(root: JsonObject): Array<{ name: string; duration: nu
|
|
|
891
892
|
});
|
|
892
893
|
}
|
|
893
894
|
|
|
894
|
-
/**
|
|
895
|
-
|
|
895
|
+
/**
|
|
896
|
+
* Every page named, read from `atlasDir` by that name — the images a candidate
|
|
897
|
+
* is drawn from, whichever poser draws it. A page that is not there is
|
|
898
|
+
* `absent`'s refusal, handed the page's absolute path (issues #1033, #1042):
|
|
899
|
+
* left to `readPlate`, it surfaced as an ENOENT and a stack. A page that is
|
|
900
|
+
* there and is not a PNG says so itself (`readPlate`).
|
|
901
|
+
*/
|
|
902
|
+
function pagesAt(names: readonly string[], atlasDir: string, absent: (page: string) => Error): Map<string, Plate> {
|
|
896
903
|
const pages = new Map<string, Plate>();
|
|
897
|
-
for (const name of names)
|
|
904
|
+
for (const name of names) {
|
|
905
|
+
const path = join(atlasDir, name);
|
|
906
|
+
if (!existsSync(path)) throw absent(resolve(path));
|
|
907
|
+
pages.set(name, readPlate(path));
|
|
908
|
+
}
|
|
898
909
|
return pages;
|
|
899
910
|
}
|
|
900
911
|
|
|
@@ -945,6 +956,140 @@ function requireSpineRuntime(label: string, why: string): void {
|
|
|
945
956
|
}
|
|
946
957
|
}
|
|
947
958
|
|
|
959
|
+
/**
|
|
960
|
+
* A candidate whose skeleton spine-core cannot load against the atlas beside
|
|
961
|
+
* it — refused naming both files, why the runtime was drawing them, and what
|
|
962
|
+
* the runtime could not resolve, in its own words (issue #1033).
|
|
963
|
+
*
|
|
964
|
+
* On a rigc build this is a directory whose files are not one build: a
|
|
965
|
+
* `skeleton.json` or a `skeleton.atlas` from another build put beside this
|
|
966
|
+
* one's `skeleton.model.json`. The core refuses the pair first (the digest, or
|
|
967
|
+
* the atlas's pages, on the poser line's reason), the fallback hands it to
|
|
968
|
+
* spine-core, and the runtime stops at the first name it cannot resolve —
|
|
969
|
+
* which surfaced as its own uncaught error and a stack. A class of its own so
|
|
970
|
+
* `cli.ts` refuses it as an invocation (exit 2, nothing written), as it
|
|
971
|
+
* refuses a missing atlas on an export: the files the command was pointed at
|
|
972
|
+
* have to change, not the rig.
|
|
973
|
+
*
|
|
974
|
+
* Since issue #1042 it is also `bonedist`'s (and `bench --bones`'), on either
|
|
975
|
+
* side (`loadPosedSkeleton`), and the refusal for a page a candidate is drawn
|
|
976
|
+
* from that is not there on every poser's path — the core's included, where a
|
|
977
|
+
* build moved whole away from where it was built died on `readPlate`'s ENOENT.
|
|
978
|
+
*/
|
|
979
|
+
export class CandidatePairError extends Error {}
|
|
980
|
+
|
|
981
|
+
/** Where a candidate's files sit, as the refusals below name them: the atlas, and the directory when it holds a model document. */
|
|
982
|
+
function pairPlace(paths: { skeleton: string; atlas: string } | null): { atlas: string; build: string | null } {
|
|
983
|
+
const dir = paths === null ? null : dirname(resolve(paths.skeleton));
|
|
984
|
+
return { atlas: paths === null ? 'the atlas handed over with it' : resolve(paths.atlas), build: dir !== null && existsSync(join(dir, MODEL_DOCUMENT_FILE)) ? dir : null };
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** What a directory whose files are not one build is told to do. */
|
|
988
|
+
function notOneBuild(dir: string): string {
|
|
989
|
+
return (
|
|
990
|
+
`${dir} is not one build: ${MODEL_DOCUMENT_FILE}, skeleton.json and skeleton.atlas are written together by one \`rigc build\`, ` +
|
|
991
|
+
"and these are not one build's — build it again, or put that build's own files back beside each other"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* The refusal for a skeleton the runtime could not load against its atlas:
|
|
997
|
+
* `runtime` is the runtime's own message, and `does` what spine-core was
|
|
998
|
+
* loading it to do — `draws` for `render` and `check`, `poses` for a command
|
|
999
|
+
* that reads the posed bones and draws nothing (`bonedist`, issue #1042).
|
|
1000
|
+
*/
|
|
1001
|
+
function pairRefusal(
|
|
1002
|
+
label: string,
|
|
1003
|
+
paths: { skeleton: string; atlas: string } | null,
|
|
1004
|
+
why: string,
|
|
1005
|
+
runtime: string,
|
|
1006
|
+
does: 'draws' | 'poses' = 'draws',
|
|
1007
|
+
): CandidatePairError {
|
|
1008
|
+
const { atlas, build } = pairPlace(paths);
|
|
1009
|
+
const head = `${label} does not load against ${atlas}: spine-core ${does} this pair (${why}) and could not resolve it — ${JSON.stringify(runtime)}. `;
|
|
1010
|
+
return new CandidatePairError(head + (build === null ? 'The skeleton and the atlas are not one pair: the atlas has to be the one the skeleton was exported or built with' : notOneBuild(build)));
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* The refusal for a page a candidate is drawn from that is not there — the
|
|
1015
|
+
* page's path is relative to the directory of the file that names it, so the
|
|
1016
|
+
* file was written somewhere else. What the last clause may claim depends on
|
|
1017
|
+
* what is known about the directory:
|
|
1018
|
+
*
|
|
1019
|
+
* - an export (no model document): only that the page has to be there;
|
|
1020
|
+
* - a rigc build whose files the core REFUSED (issue #1033): an atlas copied
|
|
1021
|
+
* from another build's directory, and the directory is not one build — the
|
|
1022
|
+
* core's own reason, on the poser line's words, says so;
|
|
1023
|
+
* - a rigc build the core ACCEPTED, or one `--poser spine` never asked the
|
|
1024
|
+
* core about (issue #1042): the build was moved or copied away from where it
|
|
1025
|
+
* was built. "Not one build" would be a guess there, and on a build moved
|
|
1026
|
+
* whole — measured — a false one.
|
|
1027
|
+
*
|
|
1028
|
+
* `by` is the file that names the page (`null`: the atlas), `draws` what is
|
|
1029
|
+
* drawn from it.
|
|
1030
|
+
*/
|
|
1031
|
+
function pageRefusal(
|
|
1032
|
+
page: string,
|
|
1033
|
+
paths: { skeleton: string; atlas: string } | null,
|
|
1034
|
+
why: string,
|
|
1035
|
+
reading: { by: string | null; draws: string; refusedBuild: boolean },
|
|
1036
|
+
): CandidatePairError {
|
|
1037
|
+
const { atlas, build } = pairPlace(paths);
|
|
1038
|
+
// The poser line's reason, unless it is only the document's path — which the sentence has just named.
|
|
1039
|
+
const because = why === reading.by ? '' : ` (${why})`;
|
|
1040
|
+
const head =
|
|
1041
|
+
`nothing at ${page}: ${reading.by ?? atlas} names it as a page, and ${reading.draws}${because}. ` +
|
|
1042
|
+
`A page path is relative to the ${reading.by === null ? "atlas's own" : "build's"} directory`;
|
|
1043
|
+
const tail =
|
|
1044
|
+
build === null
|
|
1045
|
+
? ', and the page has to be there'
|
|
1046
|
+
: reading.refusedBuild
|
|
1047
|
+
? `, so an atlas copied from another build's directory names pages that are not here. ${notOneBuild(build)}`
|
|
1048
|
+
: `, so a build moved or copied away from the directory it was built in names pages that are not here — ` +
|
|
1049
|
+
'build it again where it is, or build it with --copy-images, which writes its pages beside it';
|
|
1050
|
+
return new CandidatePairError(head + tail);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* The one place a candidate's skeleton is handed to the runtime's parser
|
|
1055
|
+
* against its atlas (issues #1033, #1042): a pair it cannot load is `refuse`'s
|
|
1056
|
+
* refusal, given the runtime's own message, rather than the runtime's throw
|
|
1057
|
+
* and a stack. The JSON is parsed outside the catch: a file that is not JSON
|
|
1058
|
+
* is not something the runtime said, and the CLI refuses it by name before it
|
|
1059
|
+
* gets here (`readSkeletonText`).
|
|
1060
|
+
*/
|
|
1061
|
+
function spineSkeletonData(skeletonText: string, atlasText: string, refuse: (runtime: string) => Error): SkeletonData {
|
|
1062
|
+
const json = JSON.parse(skeletonText);
|
|
1063
|
+
const reader = new SkeletonJson(new AtlasAttachmentLoader(new TextureAtlas(atlasText)));
|
|
1064
|
+
try {
|
|
1065
|
+
return reader.readSkeletonData(json);
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
throw refuse(err instanceof Error ? err.message : String(err));
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* A skeleton a user named, posed through spine-core for what its bones do —
|
|
1073
|
+
* `bonedist`'s two sides (issue #1042). A pair that does not load is refused
|
|
1074
|
+
* by name (`CandidatePairError`, `pairRefusal` with `poses`), `why` saying
|
|
1075
|
+
* which side and what the pose is read for.
|
|
1076
|
+
*
|
|
1077
|
+
* ⭐ The skeleton data alone, and no page: `bonedist` reads bone world
|
|
1078
|
+
* transforms and samples no pixel, and the atlas's regions — which the
|
|
1079
|
+
* runtime resolves every attachment against — are in the atlas text. Reading
|
|
1080
|
+
* the page PNGs as `loadPosable` does made a pair whose pages are elsewhere
|
|
1081
|
+
* die on an ENOENT for images the command never looks at; the figures are the
|
|
1082
|
+
* same with or without them (the PR of #1042 measures it).
|
|
1083
|
+
*
|
|
1084
|
+
* `loadPosable` and `posableFromText` stay the unguarded loaders, for pairs
|
|
1085
|
+
* the caller wrote itself (the tools and the selftest), where the runtime's
|
|
1086
|
+
* own message is what the caller reads.
|
|
1087
|
+
*/
|
|
1088
|
+
export function loadPosedSkeleton(skeletonPath: string, atlasPath: string, why: string): SkeletonData {
|
|
1089
|
+
const paths = { skeleton: skeletonPath, atlas: atlasPath };
|
|
1090
|
+
return spineSkeletonData(readFileSync(skeletonPath, 'utf8'), readFileSync(atlasPath, 'utf8'), (runtime) => pairRefusal(skeletonPath, paths, why, runtime, 'poses'));
|
|
1091
|
+
}
|
|
1092
|
+
|
|
948
1093
|
/** A candidate as `render` and `check` read it: the posers, the facts and the pages (issue #1014). */
|
|
949
1094
|
export interface Candidate {
|
|
950
1095
|
choice: PoserChoice;
|
|
@@ -983,12 +1128,21 @@ export function loadCandidate(
|
|
|
983
1128
|
if (input.atlasText === null) throw atlasAbsent(paths?.atlas ?? '(no atlas)', input.label, why);
|
|
984
1129
|
return input.atlasText;
|
|
985
1130
|
};
|
|
1131
|
+
// A pair the runtime cannot load — a skeleton.json or an atlas from another build beside this one's document — is
|
|
1132
|
+
// refused by name rather than surfacing the runtime's own throw and stack (issue #1033); `spineSkeletonData` is
|
|
1133
|
+
// where the catch is.
|
|
1134
|
+
// Under `--poser core` the runtime is loaded only for the facts the flags are checked against, and nothing would be
|
|
1135
|
+
// drawn through it: the refusal that is true there is the flag's, the one `refuseUnchosen` would have said next.
|
|
1136
|
+
const refusing = (refusal: CandidatePairError): Error =>
|
|
1137
|
+
forced === 'core' && choice !== null && choice.core === null ? new PoserChoiceError(`--poser core: ${choice.why}`) : refusal;
|
|
1138
|
+
const spineLoad = (text: string, why: string): SkeletonData =>
|
|
1139
|
+
spineSkeletonData(input.skeletonText, text, (runtime) => refusing(pairRefusal(input.label, paths, why, runtime)));
|
|
986
1140
|
const spineData = (): SkeletonData => {
|
|
987
1141
|
if (data === null) {
|
|
988
1142
|
const why = choice === null || choice.core !== null ? 'a fallback from the core poser' : choice.why;
|
|
989
1143
|
const text = atlasText(why);
|
|
990
1144
|
requireSpineRuntime(input.label, why);
|
|
991
|
-
data =
|
|
1145
|
+
data = spineLoad(text, why);
|
|
992
1146
|
}
|
|
993
1147
|
return data;
|
|
994
1148
|
};
|
|
@@ -999,14 +1153,31 @@ export function loadCandidate(
|
|
|
999
1153
|
choice = chosen.choice;
|
|
1000
1154
|
if (choice.core !== null && chosen.document !== null) {
|
|
1001
1155
|
// A rigc-compiled/2 document names its pages; a /1 document is posed only with its atlas beside it, which names them (`choosePosers`).
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1156
|
+
const named = chosen.document.pageNames;
|
|
1157
|
+
const names = named ?? parseAtlasText(atlasText(choice.why)).pages.map((page) => page.name);
|
|
1158
|
+
// A page the build names that is not here is a build moved away from where it was built (issue #1042): the
|
|
1159
|
+
// core accepted these files as one build, so the refusal does not say they are not one.
|
|
1160
|
+
const by = named === null || paths === null ? null : join(dirname(resolve(paths.skeleton)), MODEL_DOCUMENT_FILE);
|
|
1161
|
+
const reading = { by, draws: 'the core draws this build from it', refusedBuild: false };
|
|
1162
|
+
const accepted = choice.why;
|
|
1163
|
+
const pages = pagesAt(names, input.atlasDir, (page) => pageRefusal(page, paths, accepted, reading));
|
|
1164
|
+
return { choice, facts: coreFacts(input.skeletonText, input.atlasText, choice.core, chosen.document), pages };
|
|
1004
1165
|
}
|
|
1005
1166
|
const text = atlasText(choice.why);
|
|
1006
1167
|
requireSpineRuntime(input.label, choice.why);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1168
|
+
// `posableFromText`'s two steps, in its order — every page the atlas declares, then the skeleton — each refusing
|
|
1169
|
+
// by name where the pair is not one: a page that is not there (`pageRefusal`), a skeleton the runtime cannot load
|
|
1170
|
+
// against the atlas (`spineLoad`). The directory is called "not one build" only where the core refused its files —
|
|
1171
|
+
// `--poser spine` never asked the core, and a build moved whole is one build whose pages are elsewhere.
|
|
1172
|
+
const reading = { by: null, draws: 'spine-core draws this pair through that atlas', refusedBuild: forced !== 'spine' };
|
|
1173
|
+
const why = choice.why;
|
|
1174
|
+
const pages = pagesAt(
|
|
1175
|
+
new TextureAtlas(text).pages.map((page) => page.name),
|
|
1176
|
+
input.atlasDir,
|
|
1177
|
+
(page) => refusing(pageRefusal(page, paths, why, reading)),
|
|
1178
|
+
);
|
|
1179
|
+
data = spineLoad(text, choice.why);
|
|
1180
|
+
return { choice, facts: spineFacts(data, text), pages };
|
|
1010
1181
|
}
|
|
1011
1182
|
|
|
1012
1183
|
/**
|
|
@@ -1066,8 +1237,8 @@ function coreFacts(skeletonText: string, atlasText: string | null, poser: Poser,
|
|
|
1066
1237
|
// 🔒 What stays spine-core's and outside the seam, deliberately: an export's
|
|
1067
1238
|
// atlas (`posableFromText`'s pages, and `substituteTexture`'s region lookup on
|
|
1068
1239
|
// anything spine-core poses — a rigc build the core poses reads both through
|
|
1069
|
-
// rigc's own reader since issue #1020) and `
|
|
1070
|
-
// skeleton it stepped itself (A10), so the round trip keeps its spine-core
|
|
1240
|
+
// rigc's own reader since issue #1020) and `posedNumbersOf`, which `validate.ts` calls on a spine-core
|
|
1241
|
+
// skeleton it stepped itself (A10's runtime supplier), so the round trip keeps its spine-core
|
|
1071
1242
|
// entry whatever poses the renders.
|
|
1072
1243
|
|
|
1073
1244
|
/** What a sampler hands a posed frame's draw walk — `PoseOptions` with the subset already resolved. */
|
|
@@ -1979,66 +2150,36 @@ function restOf(data: SkeletonData, skin: string | undefined, shown: readonly At
|
|
|
1979
2150
|
return out;
|
|
1980
2151
|
}
|
|
1981
2152
|
|
|
1982
|
-
/** The bone fields a world transform is made of — what `firstNonFinite` reads off a bone. */
|
|
1983
|
-
type WorldTransform = Pick<GeometryBone, 'name' | 'a' | 'b' | 'c' | 'd' | 'worldX' | 'worldY'>;
|
|
1984
|
-
|
|
1985
|
-
/** The fields of an attachment entry `firstNonFinite` reads — a frame's `AttachmentPose` or a rest entry. */
|
|
1986
|
-
type PosedVertices = Pick<AttachmentPose, 'slot' | 'attachment' | 'vertices'>;
|
|
1987
|
-
|
|
1988
2153
|
/**
|
|
1989
|
-
*
|
|
1990
|
-
*
|
|
1991
|
-
*
|
|
1992
|
-
|
|
1993
|
-
function firstNonFinite(where: string, entries: readonly PosedVertices[], bones: readonly WorldTransform[]): string | null {
|
|
1994
|
-
for (const bone of bones) {
|
|
1995
|
-
for (const field of ['a', 'b', 'c', 'd', 'worldX', 'worldY'] as const) {
|
|
1996
|
-
if (!Number.isFinite(bone[field])) {
|
|
1997
|
-
return `${where}: bone ${JSON.stringify(bone.name)} has ${field} ${String(bone[field])}; a world transform is finite`;
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
for (const entry of entries) {
|
|
2002
|
-
const bad = entry.vertices.findIndex((value) => !Number.isFinite(value));
|
|
2003
|
-
if (bad === -1) continue;
|
|
2004
|
-
return (
|
|
2005
|
-
`${where}: slot ${JSON.stringify(entry.slot)} attachment ${JSON.stringify(entry.attachment)} vertex ` +
|
|
2006
|
-
`${Math.floor(bad / 2)} has ${bad % 2 === 0 ? 'x' : 'y'} ${String(entry.vertices[bad])}; a posed vertex is finite`
|
|
2007
|
-
);
|
|
2008
|
-
}
|
|
2009
|
-
return null;
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
/**
|
|
2013
|
-
* The same sentence for one skeleton as it stands posed now — its bones, then
|
|
2014
|
-
* the vertices of every region and mesh its slots show, in draw order — or
|
|
2015
|
-
* `null` when every one of those numbers is finite.
|
|
2154
|
+
* One skeleton as it stands posed now, as the numbers `firstNonFinite`
|
|
2155
|
+
* (`./nonfinite.ts`) reads: every bone's six world-transform terms, in skeleton
|
|
2156
|
+
* order, then the world vertices of every region and mesh its slots show, in
|
|
2157
|
+
* draw order — the runtime's reading of each (`worldVerticesOf`).
|
|
2016
2158
|
*
|
|
2017
|
-
* ⭐ This is how `A10_NO_NAN_AFTER_STEPPING` reads each pose
|
|
2018
|
-
* #882
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
*
|
|
2159
|
+
* ⭐ This is how `A10_NO_NAN_AFTER_STEPPING` reads each pose spine-core steps
|
|
2160
|
+
* (issue #882; its runtime supplier in `validate.ts` since issue #1025), so the
|
|
2161
|
+
* gate and the renderer hold one definition of "not finite": the six terms
|
|
2162
|
+
* `firstNonFinite` reads off a bone, and the vertices the runtime computes from
|
|
2163
|
+
* them. Before #882 A10 read the world POSITION alone, and a bone at `rotation:
|
|
2164
|
+
* 1e309` — finite position, NaN `a`, `b`, `c`, `d` — was gated green and then
|
|
2165
|
+
* refused here. The vertices are read too because a bone can be finite and
|
|
2166
|
+
* still carry one that is not: a two-bone `scaleX` chain of 1e154 × 1e154
|
|
2024
2167
|
* leaves the child's `a` at 1e308, finite, and every vertex of its attachment
|
|
2025
|
-
* past the largest double.
|
|
2026
|
-
*
|
|
2168
|
+
* past the largest double. `firstNonFinite` reads the bones first, so a broken
|
|
2169
|
+
* bone is named as the cause rather than through its vertices.
|
|
2027
2170
|
*/
|
|
2028
|
-
export function
|
|
2171
|
+
export function posedNumbersOf(skeleton: Skeleton): { bones: WorldTransform[]; drawn: PosedVertices[] } {
|
|
2029
2172
|
const bones: WorldTransform[] = skeleton.bones.map((bone) => {
|
|
2030
2173
|
const { a, b, c, d, worldX, worldY } = bone.appliedPose;
|
|
2031
2174
|
return { name: bone.data.name, a, b, c, d, worldX, worldY };
|
|
2032
2175
|
});
|
|
2033
|
-
const
|
|
2034
|
-
if (found !== null) return found;
|
|
2035
|
-
const entries: PosedVertices[] = [];
|
|
2176
|
+
const drawn: PosedVertices[] = [];
|
|
2036
2177
|
for (const slot of skeleton.drawOrder.appliedPose) {
|
|
2037
2178
|
const attachment = slot.appliedPose.attachment;
|
|
2038
2179
|
if (!(attachment instanceof MeshAttachment) && !(attachment instanceof RegionAttachment)) continue;
|
|
2039
|
-
|
|
2180
|
+
drawn.push({ slot: slot.data.name, attachment: attachment.name, vertices: worldVerticesOf(skeleton, slot, attachment) });
|
|
2040
2181
|
}
|
|
2041
|
-
return
|
|
2182
|
+
return { bones, drawn };
|
|
2042
2183
|
}
|
|
2043
2184
|
|
|
2044
2185
|
/**
|
package/src/render_core.ts
CHANGED
|
@@ -282,6 +282,11 @@ interface CoreInput {
|
|
|
282
282
|
* Exported for the model side of the validator (issue #1025), which poses a
|
|
283
283
|
* slot the way `validate()` does — a fresh skeleton, no skin set — and must
|
|
284
284
|
* resolve that state by this rule rather than by a copy of it.
|
|
285
|
+
*
|
|
286
|
+
* ⚠️ The first refusal — skins declared and none of them `default` — is issue
|
|
287
|
+
* #1051: the model side's A10, which always poses, fails such a rig the round
|
|
288
|
+
* trip passes; `tools/verdict_gate.ts` and the selftest's `VF02` count those
|
|
289
|
+
* line sets apart as this documented class rather than as a difference.
|
|
285
290
|
*/
|
|
286
291
|
export function noSkinView(doc: CompiledDocument): CompiledDocument {
|
|
287
292
|
if (doc.skins.length === 0) return underSkin(doc, CORE_ALL_SKINS);
|