spine-rigc 1.7.0 → 1.9.0

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 (79) hide show
  1. package/cli.ts +88 -21
  2. package/docs/AUTHORING.md +18 -12
  3. package/package.json +1 -1
  4. package/src/assertions/bodies/a03.ts +27 -0
  5. package/src/assertions/bodies/a04.ts +40 -0
  6. package/src/assertions/bodies/a06.ts +195 -0
  7. package/src/assertions/bodies/a08.ts +76 -0
  8. package/src/assertions/bodies/a11.ts +15 -0
  9. package/src/assertions/bodies/a13.ts +51 -0
  10. package/src/assertions/bodies/a14.ts +35 -0
  11. package/src/assertions/bodies/a15.ts +97 -0
  12. package/src/assertions/bodies/a17.ts +26 -0
  13. package/src/assertions/bodies/a19.ts +295 -0
  14. package/src/assertions/bodies/a20.ts +122 -0
  15. package/src/assertions/bodies/a21.ts +190 -0
  16. package/src/assertions/bodies/a22.ts +39 -0
  17. package/src/assertions/bodies/a23.ts +255 -0
  18. package/src/assertions/bodies/a27.ts +33 -0
  19. package/src/assertions/bodies/a28.ts +70 -0
  20. package/src/assertions/bodies/a33.ts +110 -0
  21. package/src/assertions/bodies/a36.ts +81 -0
  22. package/src/assertions/bodies/a37.ts +77 -0
  23. package/src/assertions/bodies/a38.ts +73 -0
  24. package/src/assertions/bodies/a41.ts +83 -0
  25. package/src/assertions/bodies/a42.ts +97 -0
  26. package/src/assertions/bodies/a44.ts +23 -0
  27. package/src/assertions/bodies/a45.ts +172 -0
  28. package/src/assertions/bodies/a47.ts +59 -0
  29. package/src/assertions/bodies/a48.ts +83 -0
  30. package/src/assertions/constraint_words.ts +177 -0
  31. package/src/assertions/facts/animated_bones.ts +28 -0
  32. package/src/assertions/facts/atlas_pages.ts +19 -0
  33. package/src/assertions/facts/atlas_regions.ts +52 -0
  34. package/src/assertions/facts/constraints.ts +131 -0
  35. package/src/assertions/facts/linked_meshes.ts +38 -0
  36. package/src/assertions/facts/mesh_attachments.ts +71 -0
  37. package/src/assertions/facts/region_joins.ts +34 -0
  38. package/src/assertions/facts/skin_entries.ts +37 -0
  39. package/src/assertions/facts/skin_members.ts +53 -0
  40. package/src/assertions/facts/skin_meshes.ts +49 -0
  41. package/src/assertions/facts/slot_colour.ts +43 -0
  42. package/src/assertions/facts/stage.ts +24 -0
  43. package/src/assertions/facts/vertex_polygons.ts +53 -0
  44. package/src/assertions/harness.ts +109 -0
  45. package/src/assertions/kinds.ts +102 -0
  46. package/src/assertions/mesh_kinds.ts +56 -0
  47. package/src/assertions/model/animated_bones.ts +30 -0
  48. package/src/assertions/model/atlas_pages.ts +15 -0
  49. package/src/assertions/model/atlas_regions.ts +76 -0
  50. package/src/assertions/model/constraints.ts +207 -0
  51. package/src/assertions/model/given.ts +45 -0
  52. package/src/assertions/model/index.ts +247 -0
  53. package/src/assertions/model/linked_meshes.ts +24 -0
  54. package/src/assertions/model/mesh_attachments.ts +90 -0
  55. package/src/assertions/model/parse.ts +138 -0
  56. package/src/assertions/model/region_joins.ts +59 -0
  57. package/src/assertions/model/skin_entries.ts +59 -0
  58. package/src/assertions/model/skin_members.ts +62 -0
  59. package/src/assertions/model/skin_meshes.ts +90 -0
  60. package/src/assertions/model/slot_colour.ts +80 -0
  61. package/src/assertions/model/stage.ts +28 -0
  62. package/src/assertions/model/vertex_polygons.ts +71 -0
  63. package/src/assertions/reasons.ts +127 -0
  64. package/src/assertions/region_lookups.ts +61 -0
  65. package/src/assertions/values.ts +39 -0
  66. package/src/check.ts +36 -9
  67. package/src/compile.ts +204 -57
  68. package/src/core/index.ts +161 -29
  69. package/src/core/world.ts +21 -20
  70. package/src/deformmeasure.ts +377 -154
  71. package/src/deformreport.ts +3 -0
  72. package/src/deformstructure.ts +344 -0
  73. package/src/emit_spine.ts +21 -41
  74. package/src/keyorder.ts +40 -0
  75. package/src/model.ts +188 -22
  76. package/src/render.ts +243 -54
  77. package/src/render_core.ts +101 -13
  78. package/src/transform.ts +79 -30
  79. package/src/validate.ts +742 -2781
@@ -0,0 +1,247 @@
1
+ /**
2
+ * The model side of the validator (issue #1025, step 4c of #380): the
3
+ * assertions that moved out of `src/validate.ts`, run over the model document
4
+ * and rigc's core instead of over what spine-core loaded.
5
+ *
6
+ * ⭐ **One body, two suppliers.** Every moved assertion is one function under
7
+ * `../bodies/`, written against a fact interface under `../facts/`.
8
+ * `validate()` supplies the facts from spine-core's loaded objects and calls
9
+ * the body inside its own `check`; this entry supplies them from the document
10
+ * (`./parse.ts`, `./skin_entries.ts`, `./atlas_pages.ts`, `./slot_colour.ts`;
11
+ * cut 4c-2's `./mesh_attachments.ts`, `./vertex_polygons.ts`,
12
+ * `./linked_meshes.ts`, `./constraints.ts`)
13
+ * and calls the same body inside the same harness (`../harness.ts`). The
14
+ * selftest holds the two to the same lines on every call it makes with a
15
+ * model in hand, and `tools/verdict_gate.ts` on every recipe.
16
+ *
17
+ * 🔸 **What the entry is given.** The document's text, the directory its
18
+ * pages resolve against, and the profile — and nothing read off a Spine file:
19
+ * a model-side verdict that read `skeleton.json` would be a second reading of
20
+ * the encoding, which is the round trip's subject and not the rig's. Since cut
21
+ * 4c-1 it is also given the rig info (as `validate()` is) and, in `given`, the
22
+ * two values a `rigc-compiled/2` or `/1` document does not hold — the stage and
23
+ * each page's `pma` (`./given.ts`). A `/3` document states both (issue #1026)
24
+ * and is read for them; `given` beside one is refused by name.
25
+ *
26
+ * ⛔ **Not wired into any command.** `build` keeps the round trip and its
27
+ * forty-nine lines; this entry is what the selftest and the instrument call
28
+ * until every moved assertion is on it (the card's design, point 7).
29
+ *
30
+ * Links nothing from the runtime, directly or through what it imports — held
31
+ * by the selftest's `VF` controls.
32
+ */
33
+ import { ASSERTION_KIND, type AssertionProfile } from '../kinds.ts';
34
+ import { verdictHarness, type VerdictHarness, type Verdicts, type VerdictLists } from '../harness.ts';
35
+ import { SKIP_NO_ATLAS, SKIP_NO_MODEL } from '../reasons.ts';
36
+ import { a03RegionWidthHeightFinite } from '../bodies/a03.ts';
37
+ import { a11NoClippingAttachments } from '../bodies/a11.ts';
38
+ import { a17AtlasPageFilesExist } from '../bodies/a17.ts';
39
+ import { a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten } from '../bodies/a45.ts';
40
+ import { A00_MODEL_READ, A00_MODEL_REGIONS_ON_PAGES, MODEL_PARSE_KIND, modelRead, modelRegionsOnPages, SKIP_NO_MODEL_DOCUMENT, SKIP_NO_MODEL_PAGES, type ReadDocument } from './parse.ts';
41
+ import { modelSkinEntries } from './skin_entries.ts';
42
+ import { modelAtlasPages } from './atlas_pages.ts';
43
+ import { modelSlotColour } from './slot_colour.ts';
44
+ import type { SkinEntryFacts } from '../facts/skin_entries.ts';
45
+ import type { AtlasPageFacts } from '../facts/atlas_pages.ts';
46
+ import type { SlotColourFacts } from '../facts/slot_colour.ts';
47
+ import type { RigInfo } from '../../types.ts';
48
+ import type { ModelGiven } from './given.ts';
49
+ import { a08RegionNamesMatchAttachments } from '../bodies/a08.ts';
50
+ import { a13MeshBudget } from '../bodies/a13.ts';
51
+ import { a14NoFullFrameMesh } from '../bodies/a14.ts';
52
+ import { a15IdleNoMeshBoneKeys } from '../bodies/a15.ts';
53
+ import { a22MeshUvsInUnitRange } from '../bodies/a22.ts';
54
+ import { a38SkinMembersAreSkinRequired } from '../bodies/a38.ts';
55
+ import { a06AtlasPageSizeMatchesPng } from '../bodies/a06.ts';
56
+ import { a19OverlayPngsHaveAlpha } from '../bodies/a19.ts';
57
+ import { a27RegionNameMatchesPageFilename } from '../bodies/a27.ts';
58
+ import { modelSkinMeshes } from './skin_meshes.ts';
59
+ import { modelAnimatedBones } from './animated_bones.ts';
60
+ import { modelSkinMembers } from './skin_members.ts';
61
+ import { modelRegionJoins } from './region_joins.ts';
62
+ import { modelAtlasRegions } from './atlas_regions.ts';
63
+ import { modelStage } from './stage.ts';
64
+ import type { SkinMeshFacts } from '../facts/skin_meshes.ts';
65
+ import type { AnimatedBoneFacts } from '../facts/animated_bones.ts';
66
+ import type { SkinMemberFacts } from '../facts/skin_members.ts';
67
+ import type { RegionJoinFacts } from '../facts/region_joins.ts';
68
+ import type { AtlasRegionFacts } from '../facts/atlas_regions.ts';
69
+ import type { StageFacts } from '../facts/stage.ts';
70
+ import { a04MeshTrianglesAndEncoding } from '../bodies/a04.ts';
71
+ import { a20MeshWeightsCoherent } from '../bodies/a20.ts';
72
+ import { a21MeshRimPinned } from '../bodies/a21.ts';
73
+ import { a23PhysicsConstraintEffective } from '../bodies/a23.ts';
74
+ import { a28RibbonRowsShareWeights } from '../bodies/a28.ts';
75
+ import { a33VertexAttachmentGeometry } from '../bodies/a33.ts';
76
+ import { a36PathConstraintEffective } from '../bodies/a36.ts';
77
+ import { a37SliderConstraintEffective } from '../bodies/a37.ts';
78
+ import { a41PhysicsSurvivesEditorRoundTrip } from '../bodies/a41.ts';
79
+ import { a42DrivenConstraintsUpdateAfterTheirDriver } from '../bodies/a42.ts';
80
+ import { a44LinkedMeshStatesNoGeometryOfItsOwn } from '../bodies/a44.ts';
81
+ import { a47IkConstraintNotMutedThroughout } from '../bodies/a47.ts';
82
+ import { a48TransformConstraintNotMutedThroughout } from '../bodies/a48.ts';
83
+ import { modelMeshFacts } from './mesh_attachments.ts';
84
+ import { modelPolygonFacts } from './vertex_polygons.ts';
85
+ import { modelLinkFacts } from './linked_meshes.ts';
86
+ import { modelConstraintFacts } from './constraints.ts';
87
+ import type { MeshFacts } from '../facts/mesh_attachments.ts';
88
+ import type { PolygonFacts } from '../facts/vertex_polygons.ts';
89
+ import type { LinkFacts } from '../facts/linked_meshes.ts';
90
+ import type { ConstraintFacts } from '../facts/constraints.ts';
91
+
92
+ /** What the model side is given. */
93
+ export interface ModelValidateInput {
94
+ /** The document `build` writes beside the Spine pair (`skeleton.model.json`). */
95
+ modelText: string;
96
+ /** The directory the document's page names resolve against — the build's `--out`. */
97
+ atlasDir: string;
98
+ profile: AssertionProfile;
99
+ /** The rig info the build carries, as `validate()` is handed it — A13, A15, A19 (cut 4c-1) and A20, A21, A28, A41, A47, A48 (cut 4c-2) read it; optional because `ValidateInput.rig` is: absent for a bare directory, on both sides. */
100
+ rig?: RigInfo;
101
+ /**
102
+ * The stage and the pages' `pma`, for a `rigc-compiled/2` or `/1` document,
103
+ * which does not hold them (`./given.ts`). A06, A14 and A19 read them; a
104
+ * caller that gives none there has those three refuse by name rather than
105
+ * read a value nobody stated. A `/3` document states both (issue #1026), and
106
+ * `given` beside it is refused by name.
107
+ */
108
+ given?: ModelGiven;
109
+ }
110
+
111
+ /** The model side's report: `validate()`'s shape, over the moved assertions and the two parse rules. */
112
+ export interface ModelReport extends VerdictLists {
113
+ profile: AssertionProfile;
114
+ }
115
+
116
+ /**
117
+ * The fact families, composed: one supplier per family under `./`, each a
118
+ * function of the read document. A body names the family it reads, so a later
119
+ * cut that moves an assertion over an existing family adds a body file and a
120
+ * row below, and one over a new family adds a facts file, a supplier file and
121
+ * a field here.
122
+ */
123
+ export interface ModelSupply {
124
+ skinEntries: (read: ReadDocument) => SkinEntryFacts;
125
+ atlasPages: (read: ReadDocument) => AtlasPageFacts;
126
+ slotColour: (read: ReadDocument) => SlotColourFacts;
127
+ skinMeshes: (read: ReadDocument) => SkinMeshFacts;
128
+ animatedBones: (read: ReadDocument) => AnimatedBoneFacts;
129
+ skinMembers: (read: ReadDocument) => SkinMemberFacts;
130
+ regionJoins: (read: ReadDocument) => RegionJoinFacts;
131
+ atlasRegions: (read: ReadDocument, input: ModelValidateInput) => AtlasRegionFacts;
132
+ stage: (read: ReadDocument, input: ModelValidateInput) => StageFacts;
133
+ meshes: (read: ReadDocument) => MeshFacts;
134
+ polygons: (read: ReadDocument) => PolygonFacts;
135
+ links: (read: ReadDocument) => LinkFacts;
136
+ constraints: (read: ReadDocument) => ConstraintFacts;
137
+ }
138
+
139
+ /** The suppliers the model side runs on. */
140
+ export const MODEL_SUPPLY: ModelSupply = {
141
+ skinEntries: modelSkinEntries,
142
+ atlasPages: modelAtlasPages,
143
+ slotColour: modelSlotColour,
144
+ skinMeshes: modelSkinMeshes,
145
+ animatedBones: modelAnimatedBones,
146
+ skinMembers: modelSkinMembers,
147
+ regionJoins: modelRegionJoins,
148
+ atlasRegions: (read, input) => modelAtlasRegions(read, input.given),
149
+ stage: (read, input) => modelStage(read, input.given),
150
+ meshes: modelMeshFacts,
151
+ polygons: modelPolygonFacts,
152
+ links: modelLinkFacts,
153
+ constraints: modelConstraintFacts,
154
+ };
155
+
156
+ /**
157
+ * One moved assertion: its code, and how the model side runs its body once the
158
+ * document is read. The list is the registry of what has moved — each later
159
+ * cut appends its rows — and the selftest reads it for the codes it compares.
160
+ */
161
+ export interface MovedAssertion {
162
+ code: string;
163
+ /** The body, over the document. Not called when the model side read no document. */
164
+ run: (verdicts: Verdicts, read: ReadDocument, input: ModelValidateInput, supply: ModelSupply) => void;
165
+ /** What the body SKIPs with when there is no document — the model side's counterpart of the reason `validate()` gives when the round trip produced nothing. */
166
+ unread: string;
167
+ /**
168
+ * The body runs behind the reader alone, not behind the region rule — the
169
+ * model side's place for a rule `validate()` runs BEFORE its round trip
170
+ * (A08, issue #589), so that a region miss is named by the rule's own
171
+ * sentence on both sides while the parse beside it refuses the file.
172
+ */
173
+ beforeTheParse?: boolean;
174
+ }
175
+
176
+ /**
177
+ * The moved assertions, in `validate()`'s report order (the order its `check`
178
+ * calls stand in; the selftest and the instrument compare per code). `SKIP_NO_ATLAS` for
179
+ * A17 rather than `SKIP_NO_MODEL` because that is what A17 says over a missing
180
+ * atlas on either side: its body reads only the pages, and on this side no
181
+ * document means no pages.
182
+ */
183
+ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
184
+ { code: 'A08_REGION_NAMES_MATCH_ATTACHMENTS', run: (v, read, _input, supply) => (read.doc.pages === null ? v.skip('A08_REGION_NAMES_MATCH_ATTACHMENTS', SKIP_NO_MODEL_PAGES) : a08RegionNamesMatchAttachments(v, supply.regionJoins(read), new Set<string>())), unread: SKIP_NO_MODEL, beforeTheParse: true },
185
+ { code: 'A03_REGION_WIDTH_HEIGHT_FINITE', run: (v, read, _input, supply) => a03RegionWidthHeightFinite(v, supply.skinEntries(read)), unread: SKIP_NO_MODEL },
186
+ { code: 'A04_MESH_TRIANGLES_AND_ENCODING', run: (v, read, _input, supply) => a04MeshTrianglesAndEncoding(v, supply.meshes(read)), unread: SKIP_NO_MODEL },
187
+ { code: 'A33_VERTEX_ATTACHMENT_GEOMETRY', run: (v, read, _input, supply) => a33VertexAttachmentGeometry(v, supply.polygons(read)), unread: SKIP_NO_MODEL },
188
+ { code: 'A11_NO_CLIPPING_ATTACHMENTS', run: (v, read, _input, supply) => a11NoClippingAttachments(v, supply.skinEntries(read)), unread: SKIP_NO_MODEL },
189
+ { code: 'A13_MESH_BUDGET', run: (v, read, input, supply) => a13MeshBudget(v, supply.skinMeshes(read), input), unread: SKIP_NO_MODEL },
190
+ { code: 'A14_NO_FULL_FRAME_MESH', run: (v, read, input, supply) => a14NoFullFrameMesh(v, supply.skinMeshes(read), supply.stage(read, input)), unread: SKIP_NO_MODEL },
191
+ { code: 'A15_IDLE_NO_MESH_BONE_KEYS', run: (v, read, input, supply) => a15IdleNoMeshBoneKeys(v, supply.skinMeshes(read), supply.animatedBones(read), input), unread: SKIP_NO_MODEL },
192
+ { code: 'A20_MESH_WEIGHTS_COHERENT', run: (v, read, input, supply) => a20MeshWeightsCoherent(v, supply.meshes(read), input.profile === 'spine-html', input.rig), unread: SKIP_NO_MODEL },
193
+ { code: 'A21_MESH_RIM_PINNED', run: (v, read, input, supply) => a21MeshRimPinned(v, supply.meshes(read), input.rig), unread: SKIP_NO_MODEL },
194
+ { code: 'A22_MESH_UVS_IN_UNIT_RANGE', run: (v, read, _input, supply) => a22MeshUvsInUnitRange(v, supply.skinMeshes(read)), unread: SKIP_NO_MODEL },
195
+ { code: 'A23_PHYSICS_CONSTRAINT_EFFECTIVE', run: (v, read, _input, supply) => a23PhysicsConstraintEffective(v, supply.constraints(read), supply.meshes(read)), unread: SKIP_NO_MODEL },
196
+ { code: 'A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP', run: (v, read, input, supply) => a41PhysicsSurvivesEditorRoundTrip(v, supply.constraints(read), input.rig), unread: SKIP_NO_MODEL },
197
+ { code: 'A36_PATH_CONSTRAINT_EFFECTIVE', run: (v, read, _input, supply) => a36PathConstraintEffective(v, supply.constraints(read)), unread: SKIP_NO_MODEL },
198
+ { code: 'A37_SLIDER_CONSTRAINT_EFFECTIVE', run: (v, read, _input, supply) => a37SliderConstraintEffective(v, supply.constraints(read)), unread: SKIP_NO_MODEL },
199
+ { code: 'A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT', run: (v, read, input, supply) => a47IkConstraintNotMutedThroughout(v, supply.constraints(read), input.rig), unread: SKIP_NO_MODEL },
200
+ { code: 'A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT', run: (v, read, input, supply) => a48TransformConstraintNotMutedThroughout(v, supply.constraints(read), input.rig), unread: SKIP_NO_MODEL },
201
+ { code: 'A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER', run: (v, read, _input, supply) => a42DrivenConstraintsUpdateAfterTheirDriver(v, supply.constraints(read)), unread: SKIP_NO_MODEL },
202
+ { code: 'A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED', run: (v, read, _input, supply) => a38SkinMembersAreSkinRequired(v, supply.skinMembers(read)), unread: SKIP_NO_MODEL },
203
+ { code: 'A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN', run: (v, read, _input, supply) => a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten(v, supply.slotColour(read)), unread: SKIP_NO_MODEL },
204
+ { code: 'A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN', run: (v, read, _input, supply) => a44LinkedMeshStatesNoGeometryOfItsOwn(v, supply.links(read)), unread: SKIP_NO_MODEL },
205
+ { code: 'A17_ATLAS_PAGE_FILES_EXIST', run: (v, read, input, supply) => a17AtlasPageFilesExist(v, supply.atlasPages(read), input), unread: SKIP_NO_ATLAS },
206
+ { code: 'A06_ATLAS_PAGE_SIZE_MATCHES_PNG', run: (v, read, input, supply) => a06AtlasPageSizeMatchesPng(v, supply.atlasRegions(read, input), input, input.profile === 'spine-html'), unread: SKIP_NO_ATLAS },
207
+ { code: 'A19_OVERLAY_PNGS_HAVE_ALPHA', run: (v, read, input, supply) => a19OverlayPngsHaveAlpha(v, supply.atlasRegions(read, input), supply.stage(read, input), supply.skinEntries(read), input), unread: SKIP_NO_ATLAS },
208
+ { code: 'A27_REGION_NAME_MATCHES_PAGE_FILENAME', run: (v, read, input, supply) => a27RegionNameMatchesPageFilename(v, supply.atlasRegions(read, input)), unread: SKIP_NO_ATLAS },
209
+ { code: 'A28_RIBBON_ROWS_SHARE_WEIGHTS', run: (v, read, input, supply) => a28RibbonRowsShareWeights(v, supply.meshes(read), input.rig), unread: SKIP_NO_MODEL },
210
+ ];
211
+
212
+ /** The codes the model side prints: its two parse rules, then the moved assertions. */
213
+ export const MODEL_SIDE_CODES: readonly string[] = [A00_MODEL_READ, A00_MODEL_REGIONS_ON_PAGES, ...MOVED_ASSERTIONS.map((m) => m.code)];
214
+
215
+ /**
216
+ * The parse: the reader, then the region rule. Returns what the reader read
217
+ * (`null` when it refused) and whether the region rule held too — a
218
+ * `beforeTheParse` body needs only the first.
219
+ */
220
+ function parse(h: VerdictHarness, modelText: string): { read: ReadDocument | null; held: boolean } {
221
+ const read = h.check(A00_MODEL_READ, () => modelRead(h.verdicts, modelText)) ?? null;
222
+ if (read === null) {
223
+ h.check(A00_MODEL_REGIONS_ON_PAGES, () => h.skip(A00_MODEL_REGIONS_ON_PAGES, SKIP_NO_MODEL_DOCUMENT));
224
+ return { read: null, held: false };
225
+ }
226
+ return { read, held: h.check(A00_MODEL_REGIONS_ON_PAGES, () => modelRegionsOnPages(h.verdicts, read)) === true };
227
+ }
228
+
229
+ /**
230
+ * Run the model side: the parse, then every moved assertion — each over the
231
+ * document, or skipped naming the parse when there is none — and return the
232
+ * report in `validate()`'s shape, which `reportLines` in `src/validate.ts`
233
+ * prints. `plant` replaces a family's supplier; only the selftest's plants
234
+ * pass one.
235
+ */
236
+ export function validateModel(input: ModelValidateInput, plant: Partial<ModelSupply> = {}): ModelReport {
237
+ const supply: ModelSupply = { ...MODEL_SUPPLY, ...plant };
238
+ const h = verdictHarness(input.profile, { ...ASSERTION_KIND, ...MODEL_PARSE_KIND }, 'the model side');
239
+ const { read, held } = parse(h, input.modelText);
240
+ for (const moved of MOVED_ASSERTIONS) {
241
+ const doc = read !== null && (held || moved.beforeTheParse === true) ? read : null;
242
+ h.check(moved.code, () => (doc === null ? h.skip(moved.code, moved.unread) : moved.run(h.verdicts, doc, input, supply)));
243
+ }
244
+ const { failures, passed, skipped, profileSkipped, stats } = h;
245
+ return { failures, passed, skipped, profileSkipped, stats, profile: input.profile };
246
+ }
247
+
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The model side's supply of `LinkFacts` (issue #1025, step 4c of #380): the
3
+ * document's `linkedmesh` records, in the order the Spine file spells them —
4
+ * the skins by the emitter's `editorSkinOrder`, each skin's slot keys by its
5
+ * `editorSlotKeyOrder`, each slot's records in table order
6
+ * (`fileSpelledEntries`, `./vertex_polygons.ts`).
7
+ *
8
+ * `encoding` is empty on every link: a link record has no geometry field, so
9
+ * the geometry keys A44 refuses cannot be spelled here (`../facts/linked_meshes.ts`'s 🔒).
10
+ *
11
+ * Links nothing from the runtime.
12
+ */
13
+ import type { LinkEntry, LinkFacts } from '../facts/linked_meshes.ts';
14
+ import { fileSpelledEntries } from './vertex_polygons.ts';
15
+ import type { ReadDocument } from './parse.ts';
16
+
17
+ export function modelLinkFacts(read: ReadDocument): LinkFacts {
18
+ const links: LinkEntry[] = [];
19
+ for (const { skin, slot, placeholder, record } of fileSpelledEntries(read)) {
20
+ if (record.kind !== 'linkedmesh' || typeof record.source !== 'string') continue;
21
+ links.push({ skin, slot, placeholder, source: record.source, encoding: [] });
22
+ }
23
+ return { links };
24
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The model side's supply of `MeshFacts` (issue #1025, step 4c of #380): the
3
+ * document's mesh and linked-mesh records, in the file's walk order, with the
4
+ * geometry and the weights the runtime holds after its parse.
5
+ *
6
+ * Every value is the document's, through a function rigc already runs:
7
+ *
8
+ * - **The order** is `fileOrderedEntries` (`./skin_entries.ts`), the skins
9
+ * put in the file's order by the emitter's `editorSkinOrder`.
10
+ * - **A binding's bone** is the index the emitter writes for its name,
11
+ * `boneIndexOf` over the document's bones (`src/emit_spine.ts`), and its
12
+ * weight the float32 the runtime stores a vertex array in (`Math.fround`,
13
+ * `src/model.ts`'s 🔸).
14
+ * - **A link's geometry is its source's**: its vertices are the core's
15
+ * `sourceOfDoc`, its `uvs` and triangles the core's `drawWalkOf(...).meshOf`
16
+ * — the two lookups the core poses a linked mesh through — and its hull
17
+ * the source record's, read where those two read it.
18
+ * - **The counts the parser derives**: `worldVerticesLength` is the length
19
+ * of `uvs` (the length the parser hands its vertex reader for a mesh), and
20
+ * `hullLength` twice the record's `hull`, which the parser reads at the
21
+ * mesh row's default of the parser-default table when the key is absent
22
+ * (`PARSER_DEFAULTS` in `src/keyorder.ts`, which the selftest holds to the
23
+ * parser row by row).
24
+ *
25
+ * `encoding` is empty on every entry: the document states the weighted form
26
+ * outright and names bones, so the clauses about the flat run have no subject
27
+ * here (`../facts/mesh_attachments.ts`'s 🔒).
28
+ *
29
+ * Links nothing from the runtime.
30
+ */
31
+ import { boneIndexOf } from '../../emit_spine.ts';
32
+ import { drawWalkOf, shownRow, sourceOfDoc } from '../../core/index.ts';
33
+ import { PARSER_DEFAULTS } from '../../keyorder.ts';
34
+ import type { ModelVertices } from '../../model.ts';
35
+ import type { MeshBinding, MeshEntry, MeshFacts } from '../facts/mesh_attachments.ts';
36
+ import { fileOrderedEntries } from './skin_entries.ts';
37
+ import type { ReadDocument } from './parse.ts';
38
+
39
+ /** The parser's reading of a mesh with no `hull` key, off the parser-default table. */
40
+ function hullDefault(): number {
41
+ const stated = PARSER_DEFAULTS['mesh attachment']?.hull;
42
+ if (typeof stated !== 'number') throw new Error('internal: the parser-default table has no number for a mesh attachment\'s hull');
43
+ return stated;
44
+ }
45
+
46
+ export function modelMeshFacts(read: ReadDocument): MeshFacts {
47
+ const { doc } = read;
48
+ const indexOf = boneIndexOf(doc.bones);
49
+ const sourceVertices = sourceOfDoc(doc);
50
+ const walk = drawWalkOf(doc, []);
51
+ const slotBone = new Map(doc.slots.map((s) => [s.name, s.bone]));
52
+ const meshes: MeshEntry[] = [];
53
+ for (const entry of fileOrderedEntries(read)) {
54
+ const g = entry.record.geometry;
55
+ if (g === undefined || (g.kind !== 'mesh' && g.kind !== 'linkedmesh')) continue;
56
+ const where = `skin "${entry.skin}" slot "${entry.slot}" placeholder "${entry.placeholder}"`;
57
+ let vertices: ModelVertices;
58
+ let uvs: readonly number[];
59
+ let triangles: readonly number[];
60
+ let hull: number | undefined;
61
+ if (g.kind === 'mesh') {
62
+ ({ vertices, uvs, triangles, hull } = g);
63
+ } else {
64
+ // `readModel` resolves every link to a mesh or refuses the document, so a miss here is this module's defect.
65
+ const found = sourceVertices(g.skin, g.slot, g.source);
66
+ const drawn = walk.meshOf(g.skin, g.slot, g.source);
67
+ if (typeof found === 'string' || drawn === undefined) throw new Error(`internal: ${where} is a link the reader returned without a source mesh: ${typeof found === 'string' ? found : 'no uvs'}`);
68
+ vertices = found;
69
+ ({ uvs, triangles } = drawn);
70
+ const source = doc.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
71
+ hull = source?.kind === 'mesh' ? source.hull : undefined;
72
+ }
73
+ const weights: MeshBinding[][] | null = vertices.weighted ? vertices.bindings.map((influences) => influences.map((b) => ({ bone: indexOf(b.bone), weight: Math.fround(b.weight) }))) : null;
74
+ meshes.push({
75
+ name: shownRow(entry).name,
76
+ skin: entry.skin,
77
+ slot: entry.slot,
78
+ slotBone: slotBone.get(entry.slot) ?? '',
79
+ placeholder: entry.placeholder,
80
+ link: g.kind === 'linkedmesh' ? { source: g.source } : null,
81
+ triangles,
82
+ worldVerticesLength: uvs.length,
83
+ regionUVs: uvs,
84
+ hullLength: 2 * (hull ?? hullDefault()),
85
+ weights,
86
+ encoding: [],
87
+ });
88
+ }
89
+ return { bones: doc.bones.map((b) => b.name), meshes };
90
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The model side's parse (issue #1025, step 4c of #380): what stands where
3
+ * `A00_ROUNDTRIP_PARSE` stands when there is no spine-core object to load.
4
+ *
5
+ * Two rules, each a named line:
6
+ *
7
+ * - **`A00_MODEL_READ`** — `readModel` (`src/core/index.ts`) read the document.
8
+ * Its refusals are the parse: every one names the path of what it refused,
9
+ * and the line carries the reader's own sentence. A document it refuses is
10
+ * one whose wrongness has no readable model to live in — a size that is
11
+ * absent, a colour that is not hex, key times that do not increase (the
12
+ * census found four of the slice's mutants there) — and that refusal is the
13
+ * model side's verdict on it.
14
+ * - **`A00_MODEL_REGIONS_ON_PAGES`** — every region a record resolves through
15
+ * is a region of the document's `pages`, and no region record states an
16
+ * `atlas` of `null`. Measured by the census before this rule existed: 23
17
+ * cases fail `A00_ROUNDTRIP_PARSE` in a selftest run, and 14 of them are
18
+ * documents `readModel` accepts — a record's path naming no region (five
19
+ * forged), and nine builds whose model holds `atlas: null` because the build
20
+ * had no art or its pack lacked the region. spine-core refuses all fourteen
21
+ * at load (`Region not found in atlas`), and the core refused them only when
22
+ * it posed. This rule refuses them where the parse does, by the record's
23
+ * address and the region's name.
24
+ *
25
+ * The region a record resolves through is the core's rule, called rather than
26
+ * restated: its name and path are `shownRow`'s (path, else name, else the
27
+ * placeholder), a series' frame names are `frameRegionName` over the series
28
+ * `readUvSequences` reads, and a name is looked up as `documentPageLookup`
29
+ * looks it up — the first region of that exact spelling, pages in order.
30
+ *
31
+ * ⛔ Rejected: the codes `M00_…` (`M` plus two digits names a mutant row in the
32
+ * selftest — `M03`, `M12` — and a reader of a report could not tell a rule
33
+ * from a plant), and a new assertion number (`A50`): the gate's registry would
34
+ * then demand the row of every `validate()` report, and these two rules have no
35
+ * meaning there. `A00_` because they stand in A00's place, and `_MODEL_`
36
+ * because they are the model side's.
37
+ *
38
+ * Links nothing from the runtime.
39
+ */
40
+ import { CORE_REGION_KINDS, CoreInputError, readModel, shownRow, type CompiledDocument } from '../../core/index.ts';
41
+ import { documentPageLookup, frameRegionName, readUvSequences } from '../../core/uvs.ts';
42
+ import type { AssertionKind } from '../kinds.ts';
43
+ import type { Verdicts } from '../harness.ts';
44
+
45
+ /** The reader's refusal, as a line. */
46
+ export const A00_MODEL_READ = 'A00_MODEL_READ';
47
+ /** The region rule, as a line. */
48
+ export const A00_MODEL_REGIONS_ON_PAGES = 'A00_MODEL_REGIONS_ON_PAGES';
49
+
50
+ /** The two rules' kinds: both are validity — a document either is one a build could pose, or it is not. */
51
+ export const MODEL_PARSE_KIND: Readonly<Record<string, AssertionKind>> = {
52
+ [A00_MODEL_READ]: 'validity',
53
+ [A00_MODEL_REGIONS_ON_PAGES]: 'validity',
54
+ };
55
+
56
+ /** What the region rule SKIPs with when the reader refused the document. */
57
+ export const SKIP_NO_MODEL_DOCUMENT = `the reader refused the document, so there is no record to resolve (${A00_MODEL_READ} owns that failure)`;
58
+ /** What A08 SKIPs with on the model side over a `rigc-compiled/1` document, which states no pages to read region names off (issue #1025, cut 4c-1). */
59
+ export const SKIP_NO_MODEL_PAGES = 'the document is a rigc-compiled/1 document, which states no pages, so there are no region names to join against';
60
+ /** What the region rule SKIPs with when no record resolves through a region. */
61
+ export const SKIP_NO_REGION_RECORD = 'no record of the document resolves through an atlas region';
62
+
63
+ /** The document as both readings hold it: `readModel`'s, and the JSON the suppliers read spellings from. */
64
+ export interface ReadDocument {
65
+ doc: CompiledDocument;
66
+ json: Record<string, unknown>;
67
+ }
68
+
69
+ /** `A00_MODEL_READ`'s body: the document read, or the reader's refusal as a FAIL. Returns the read document or null. */
70
+ export function modelRead({ fail }: Verdicts, modelText: string): ReadDocument | null {
71
+ let doc: CompiledDocument;
72
+ try {
73
+ doc = readModel(modelText);
74
+ } catch (err) {
75
+ if (!(err instanceof CoreInputError)) throw err;
76
+ fail(A00_MODEL_READ, err.message);
77
+ return null;
78
+ }
79
+ return { doc, json: JSON.parse(modelText) as Record<string, unknown> };
80
+ }
81
+
82
+ /**
83
+ * Every region name each record resolves through, by the record's address —
84
+ * the census's rule R-region, read with the core's own lookups. A record of a
85
+ * kind that draws no region (clipping, path, bounding box) resolves through
86
+ * none.
87
+ */
88
+ export function regionNamesOf(read: ReadDocument): Array<{ where: string; names: string[]; atlasNull: boolean }> {
89
+ const sequences = readUvSequences(read.json);
90
+ const out: Array<{ where: string; names: string[]; atlasNull: boolean }> = [];
91
+ for (const skin of read.doc.skins) {
92
+ for (const [slot, table] of Object.entries(skin.attachments)) {
93
+ for (const [placeholder, record] of Object.entries(table)) {
94
+ if (!CORE_REGION_KINDS.has(record.kind)) continue;
95
+ const path = shownRow({ skin: skin.name, placeholder, record }).path as string;
96
+ const series = sequences.get(`${skin.name}/${slot}/${placeholder}`);
97
+ const names = series === undefined ? [path] : Array.from({ length: series.count }, (_, i) => frameRegionName(path, series, i));
98
+ out.push({ where: `skin "${skin.name}" slot "${slot}" placeholder "${placeholder}"`, names, atlasNull: record.kind === 'region' && record.atlas === null });
99
+ }
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+
105
+ /** `A00_MODEL_REGIONS_ON_PAGES`'s body. Returns whether the document passed it. */
106
+ export function modelRegionsOnPages({ fail, skip }: Verdicts, read: ReadDocument): boolean {
107
+ let records: ReturnType<typeof regionNamesOf>;
108
+ try {
109
+ records = regionNamesOf(read);
110
+ } catch (err) {
111
+ // A series `readModel` keeps only the count of: `readUvSequences` refuses its `start` or `digits` by path.
112
+ if (!(err instanceof CoreInputError)) throw err;
113
+ fail(A00_MODEL_REGIONS_ON_PAGES, err.message);
114
+ return false;
115
+ }
116
+ if (records.length === 0) {
117
+ skip(A00_MODEL_REGIONS_ON_PAGES, SKIP_NO_REGION_RECORD);
118
+ return true;
119
+ }
120
+ if (read.doc.pages === null) {
121
+ fail(A00_MODEL_REGIONS_ON_PAGES, `the document is ${read.doc.spec}, which states no pages, so none of the ${records.length} record(s) that resolve through a region can be found on one`);
122
+ return false;
123
+ }
124
+ const lookup = documentPageLookup(read.doc.pages);
125
+ let held = true;
126
+ for (const { where, names, atlasNull } of records) {
127
+ if (atlasNull) {
128
+ fail(A00_MODEL_REGIONS_ON_PAGES, `${where}: the region record's atlas is null — the build had no rectangle for "${names[0]}", and a runtime loading it finds no region`);
129
+ held = false;
130
+ }
131
+ for (const name of names) {
132
+ if (lookup(name) !== null) continue;
133
+ fail(A00_MODEL_REGIONS_ON_PAGES, `${where}: resolves through the region ${JSON.stringify(name)}, which no page of the document holds`);
134
+ held = false;
135
+ }
136
+ }
137
+ return held;
138
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The model side's supply of `RegionJoinFacts` (issue #1025, step 4c of
3
+ * #380): the document's region names and every lookup a skin record makes.
4
+ *
5
+ * - **The region names** are the document's `pages` regions, page by page in
6
+ * order — `pagesOfAtlas` writes them from the atlas text `build` wrote, by the
7
+ * reader `PKR01` holds to spine-core's, so they are the atlas's regions in its
8
+ * order, each name as the atlas spells it. A `rigc-compiled/1` document states
9
+ * no pages, and the registry skips A08 on it by name rather than reading that
10
+ * as an atlas that declares no region (`./index.ts`).
11
+ * - **The lookups** walk the document's skins as the file lays them: the skins
12
+ * by the emitter's `editorSkinOrder`, a skin's slot keys by its
13
+ * `editorSlotKeyOrder` (both `src/compile.ts`, the functions `emitSkins` is
14
+ * handed) — NOT the draw order `./skin_entries.ts` walks, because A08 walks the
15
+ * file's keys and the loaded skins do not — and a slot's records in its
16
+ * table's order. A record of a kind that resolves a region (`CORE_REGION_KINDS`:
17
+ * region, mesh, linked mesh) joins under its `name`, else its placeholder,
18
+ * through its `path`, else that name; a `sequence` makes the lookups
19
+ * `attachmentRegionLookups` makes of the Spine file's, called on the
20
+ * document's own sequence record.
21
+ *
22
+ * Links nothing from the runtime.
23
+ */
24
+ import { editorSkinOrder, editorSlotKeyOrder } from '../../compile.ts';
25
+ import { CORE_REGION_KINDS } from '../../core/index.ts';
26
+ import type { SkinTableEntry } from '../../model.ts';
27
+ import type { RegionJoinFacts } from '../facts/region_joins.ts';
28
+ import { attachmentRegionLookups, type AttachmentRegionJoin } from '../region_lookups.ts';
29
+ import { isObj, type Json } from '../values.ts';
30
+ import type { ReadDocument } from './parse.ts';
31
+
32
+ /** The joins, with the skins and their slot keys put in order by `skins` and `slotKeys` — the emitter's unless the selftest asks what the document's own order would print. */
33
+ export function modelRegionJoinsWith(
34
+ read: ReadDocument,
35
+ skins: <T extends { name: string }>(list: readonly T[]) => T[] = editorSkinOrder,
36
+ slotKeys: <T>(table: Record<string, T>) => Record<string, T> = editorSlotKeyOrder,
37
+ ): RegionJoinFacts {
38
+ const pages = read.doc.pages;
39
+ const regionNames = pages === null ? null : pages.flatMap((page) => page.regions.map((region) => region.name));
40
+ const stated = (Array.isArray(read.json.skins) ? read.json.skins.filter(isObj) : []).filter((skin): skin is Json & { name: string } => typeof skin.name === 'string');
41
+ const joins: AttachmentRegionJoin[] = [];
42
+ for (const skin of skins(stated)) {
43
+ if (!isObj(skin.attachments)) continue;
44
+ for (const [slot, table] of Object.entries(slotKeys(skin.attachments))) {
45
+ if (!isObj(table)) continue;
46
+ for (const [placeholder, record] of Object.entries(table)) {
47
+ if (!isObj(record) || !CORE_REGION_KINDS.has(record.kind as SkinTableEntry['kind'])) continue;
48
+ const name = typeof record.name === 'string' ? record.name : placeholder;
49
+ const path = typeof record.path === 'string' ? record.path : name;
50
+ joins.push({ skin: skin.name, slot, placeholder, name, lookups: attachmentRegionLookups(record.sequence, path) });
51
+ }
52
+ }
53
+ }
54
+ return { regionNames, joins };
55
+ }
56
+
57
+ export function modelRegionJoins(read: ReadDocument): RegionJoinFacts {
58
+ return modelRegionJoinsWith(read);
59
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The model side's supply of `SkinEntryFacts` (issue #1025, step 4c of #380):
3
+ * the skins' region attachments and clipping count, read off the document.
4
+ *
5
+ * ⭐ **In the file's order, by the emitter's own rule.** The document holds the
6
+ * skins in the spec's order and the Spine file holds them in the editor's
7
+ * (`default` first, the rest by the editor's comparator), so a walk of the
8
+ * document in its own order lists the same entries in another order on a rig
9
+ * whose skins the comparator moves — the census measured that on 3 of 52
10
+ * multi-skin compiles. The skins are therefore put in the file's order by
11
+ * calling `editorSkinOrder` from `src/compile.ts`, the function the emitter is
12
+ * handed (`emitSkins`), not by a copy of its rule. Within a skin the entries go
13
+ * slot by slot in the skeleton's slot order — the document's `slots`, which is
14
+ * the emitted array's order — and a slot's entries in its table's order, which
15
+ * the emitter writes unchanged; the selftest measures this walk equal to
16
+ * spine-core's on every multi-skin build it makes.
17
+ *
18
+ * A region's name is the record's `name`, else its placeholder — `shownRow`'s
19
+ * rule in the core — and its size the record's `width` and `height`, the
20
+ * numbers the file states (`readModel` refuses a size that is not a finite
21
+ * number, so a size the runtime would load as NaN has no document to arrive in).
22
+ *
23
+ * Links nothing from the runtime.
24
+ */
25
+ import { editorSkinOrder } from '../../compile.ts';
26
+ import { shownRow } from '../../core/index.ts';
27
+ import type { RegionEntry, SkinEntryFacts } from '../facts/skin_entries.ts';
28
+ import type { ReadDocument } from './parse.ts';
29
+
30
+ /** How the skins are put in the file's order: the emitter's rule. A parameter only so the selftest can show what the document's own order would print (`VF05`). */
31
+ export type SkinOrder = <T extends { name: string }>(skins: readonly T[]) => T[];
32
+
33
+ /** The skins' entries in the file's walk order, each with its skin, slot and placeholder. */
34
+ export function fileOrderedEntries(read: ReadDocument, order: SkinOrder = editorSkinOrder): Array<{ skin: string; slot: string; placeholder: string; record: ReadDocument['doc']['skins'][number]['attachments'][string][string] }> {
35
+ const out: ReturnType<typeof fileOrderedEntries> = [];
36
+ for (const skin of order(read.doc.skins)) {
37
+ for (const { name: slot } of read.doc.slots) {
38
+ const table = skin.attachments[slot];
39
+ if (table === undefined) continue;
40
+ for (const [placeholder, record] of Object.entries(table)) out.push({ skin: skin.name, slot, placeholder, record });
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+
46
+ export function modelSkinEntries(read: ReadDocument, order: SkinOrder = editorSkinOrder): SkinEntryFacts {
47
+ const regionAttachments: RegionEntry[] = [];
48
+ let clippingCount = 0;
49
+ for (const entry of fileOrderedEntries(read, order)) {
50
+ if (entry.record.kind === 'clipping') clippingCount++;
51
+ if (entry.record.kind !== 'region') continue;
52
+ const geometry = entry.record.geometry;
53
+ // `readModel` reads every region record's geometry or refuses the document, so a record without one is this module's defect.
54
+ if (geometry?.kind !== 'region') throw new Error(`internal: skin "${entry.skin}" slot "${entry.slot}" placeholder "${entry.placeholder}" is a region record the reader returned without its geometry`);
55
+ const shown = shownRow(entry);
56
+ regionAttachments.push({ name: shown.name, width: geometry.region.width, height: geometry.region.height, path: shown.path as string });
57
+ }
58
+ return { regionAttachments, clippingCount };
59
+ }