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.
Files changed (62) hide show
  1. package/cli.ts +48 -17
  2. package/docs/AUTHORING.md +2 -2
  3. package/package.json +1 -1
  4. package/src/assertions/bodies/a09.ts +82 -0
  5. package/src/assertions/bodies/a10.ts +116 -0
  6. package/src/assertions/bodies/a12.ts +30 -0
  7. package/src/assertions/bodies/a24.ts +68 -0
  8. package/src/assertions/bodies/a25.ts +39 -0
  9. package/src/assertions/bodies/a26.ts +61 -0
  10. package/src/assertions/bodies/a29.ts +34 -0
  11. package/src/assertions/bodies/a30.ts +50 -0
  12. package/src/assertions/bodies/a32.ts +44 -0
  13. package/src/assertions/bodies/a34.ts +133 -0
  14. package/src/assertions/bodies/a39.ts +303 -0
  15. package/src/assertions/bodies/a40.ts +128 -0
  16. package/src/assertions/bodies/a43.ts +181 -0
  17. package/src/assertions/bodies/a46.ts +224 -0
  18. package/src/assertions/facts/animation_durations.ts +37 -0
  19. package/src/assertions/facts/bone_timelines.ts +37 -0
  20. package/src/assertions/facts/constraint_targets.ts +56 -0
  21. package/src/assertions/facts/deform_survey.ts +27 -0
  22. package/src/assertions/facts/event_keys.ts +55 -0
  23. package/src/assertions/facts/sequences.ts +85 -0
  24. package/src/assertions/facts/skeleton_roster.ts +45 -0
  25. package/src/assertions/facts/slider_composition.ts +78 -0
  26. package/src/assertions/facts/stepped_poses.ts +74 -0
  27. package/src/assertions/facts/two_colour.ts +52 -0
  28. package/src/assertions/inward_advance.ts +58 -0
  29. package/src/assertions/model/animation_durations.ts +57 -0
  30. package/src/assertions/model/bone_timelines.ts +56 -0
  31. package/src/assertions/model/constraint_targets.ts +82 -0
  32. package/src/assertions/model/constraints.ts +34 -16
  33. package/src/assertions/model/deform_survey.ts +24 -0
  34. package/src/assertions/model/event_keys.ts +45 -0
  35. package/src/assertions/model/index.ts +83 -2
  36. package/src/assertions/model/linked_meshes.ts +2 -2
  37. package/src/assertions/model/mesh_attachments.ts +1 -1
  38. package/src/assertions/model/region_joins.ts +20 -12
  39. package/src/assertions/model/sequences.ts +157 -0
  40. package/src/assertions/model/skeleton_roster.ts +23 -0
  41. package/src/assertions/model/skin_entries.ts +18 -8
  42. package/src/assertions/model/skin_members.ts +5 -3
  43. package/src/assertions/model/skin_meshes.ts +1 -1
  44. package/src/assertions/model/slider_composition.ts +191 -0
  45. package/src/assertions/model/slot_colour.ts +5 -4
  46. package/src/assertions/model/stepped_poses.ts +89 -0
  47. package/src/assertions/model/two_colour.ts +61 -0
  48. package/src/assertions/model/vertex_polygons.ts +9 -8
  49. package/src/bonedist.ts +18 -6
  50. package/src/compile.ts +114 -14
  51. package/src/core/additive.ts +468 -0
  52. package/src/core/constraints.ts +12 -2
  53. package/src/core/constraints_physics.ts +31 -8
  54. package/src/core/raw.ts +77 -16
  55. package/src/core/walk.ts +202 -0
  56. package/src/deformmeasure.ts +35 -2270
  57. package/src/deformstructure.ts +60 -18
  58. package/src/deformsurvey.ts +2295 -0
  59. package/src/nonfinite.ts +54 -0
  60. package/src/render.ts +200 -59
  61. package/src/render_core.ts +5 -0
  62. package/src/validate.ts +791 -1558
@@ -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
- /** Every page named, read from `atlasDir` by that name — the images a candidate the core poses is drawn from. */
895
- function pagesNamed(names: readonly string[], atlasDir: string): Map<string, Plate> {
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) pages.set(name, readPlate(join(atlasDir, name)));
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 = new SkeletonJson(new AtlasAttachmentLoader(new TextureAtlas(text))).readSkeletonData(JSON.parse(input.skeletonText));
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 names = chosen.document.pageNames ?? parseAtlasText(atlasText(choice.why)).pages.map((page) => page.name);
1003
- return { choice, facts: coreFacts(input.skeletonText, input.atlasText, choice.core, chosen.document), pages: pagesNamed(names, input.atlasDir) };
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
- const posable = posableFromText(input.skeletonText, text, input.atlasDir);
1008
- data = posable.data;
1009
- return { choice, facts: spineFacts(posable.data, text), pages: posable.pages };
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 `nonFiniteOfPosed`, which `validate.ts` calls on a spine-core
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
- * The sentence for the first number at `where` that is not finite — bones before
1990
- * vertices, since a bone that overflowed is the cause and its vertices the
1991
- * symptom — or `null` when every number there is finite.
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 it steps (issue
2018
- * #882), so the gate and the renderer hold one definition of "not finite": the
2019
- * six terms `firstNonFinite` reads off a bone, and the vertices the runtime
2020
- * computes from them. Before it A10 read the world POSITION alone, and a bone at
2021
- * `rotation: 1e309` — finite position, NaN `a`, `b`, `c`, `d` — was gated green
2022
- * and then refused here. The vertices are read too because a bone can be finite
2023
- * and still carry one that is not: a two-bone `scaleX` chain of 1e154 × 1e154
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. Only computed once every bone is finite, so a
2026
- * broken bone is named as the cause rather than through its vertices.
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 nonFiniteOfPosed(where: string, skeleton: Skeleton): string | null {
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 found = firstNonFinite(where, [], bones);
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
- entries.push({ slot: slot.data.name, attachment: attachment.name, vertices: worldVerticesOf(skeleton, slot, attachment) });
2180
+ drawn.push({ slot: slot.data.name, attachment: attachment.name, vertices: worldVerticesOf(skeleton, slot, attachment) });
2040
2181
  }
2041
- return firstNonFinite(where, entries, []);
2182
+ return { bones, drawn };
2042
2183
  }
2043
2184
 
2044
2185
  /**
@@ -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);