rig-c 0.0.0-stage → 2.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
@@ -0,0 +1,146 @@
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 { rigSectionProblems } from './declared.ts';
44
+ import type { Verdicts } from '../harness.ts';
45
+
46
+ /** The reader's refusal, as a line. */
47
+ export const A00_MODEL_READ = 'A00_MODEL_READ';
48
+ /** The region rule, as a line. */
49
+ export const A00_MODEL_REGIONS_ON_PAGES = 'A00_MODEL_REGIONS_ON_PAGES';
50
+
51
+ /** The two rules' kinds: both are validity — a document either is one a build could pose, or it is not. */
52
+ export const MODEL_PARSE_KIND: Readonly<Record<string, AssertionKind>> = {
53
+ [A00_MODEL_READ]: 'validity',
54
+ [A00_MODEL_REGIONS_ON_PAGES]: 'validity',
55
+ };
56
+
57
+ /** What the region rule SKIPs with when the reader refused the document. */
58
+ export const SKIP_NO_MODEL_DOCUMENT = `the reader refused the document, so there is no record to resolve (${A00_MODEL_READ} owns that failure)`;
59
+ /** 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). */
60
+ 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';
61
+ /** What the region rule SKIPs with when no record resolves through a region. */
62
+ export const SKIP_NO_REGION_RECORD = 'no record of the document resolves through an atlas region';
63
+
64
+ /** The document as both readings hold it: `readModel`'s, and the JSON the suppliers read spellings from. */
65
+ export interface ReadDocument {
66
+ doc: CompiledDocument;
67
+ json: Record<string, unknown>;
68
+ }
69
+
70
+ /** `A00_MODEL_READ`'s body: the document read, or the reader's refusal as a FAIL. Returns the read document or null. */
71
+ export function modelRead({ fail }: Verdicts, modelText: string): ReadDocument | null {
72
+ let doc: CompiledDocument;
73
+ try {
74
+ doc = readModel(modelText);
75
+ } catch (err) {
76
+ if (!(err instanceof CoreInputError)) throw err;
77
+ fail(A00_MODEL_READ, err.message);
78
+ return null;
79
+ }
80
+ const json = JSON.parse(modelText) as Record<string, unknown>;
81
+ // Issue #1054: the rig section is a source the model side reads (`./declared.ts`), so it is read here with the rest — refused by its paths, never cast.
82
+ const rigProblems = rigSectionProblems(json);
83
+ if (rigProblems.length > 0) {
84
+ fail(A00_MODEL_READ, `the model document: ${rigProblems.length} problem(s) in the rig section the model side reads its declarations from: ${rigProblems.join('; ')}`);
85
+ return null;
86
+ }
87
+ return { doc, json };
88
+ }
89
+
90
+ /**
91
+ * Every region name each record resolves through, by the record's address —
92
+ * the census's rule R-region, read with the core's own lookups. A record of a
93
+ * kind that draws no region (clipping, path, bounding box) resolves through
94
+ * none.
95
+ */
96
+ export function regionNamesOf(read: ReadDocument): Array<{ where: string; names: string[]; atlasNull: boolean }> {
97
+ const sequences = readUvSequences(read.json);
98
+ const out: Array<{ where: string; names: string[]; atlasNull: boolean }> = [];
99
+ for (const skin of read.doc.skins) {
100
+ for (const [slot, table] of Object.entries(skin.attachments)) {
101
+ for (const [placeholder, record] of Object.entries(table)) {
102
+ if (!CORE_REGION_KINDS.has(record.kind)) continue;
103
+ const path = shownRow({ skin: skin.name, placeholder, record }).path as string;
104
+ const series = sequences.get(`${skin.name}/${slot}/${placeholder}`);
105
+ const names = series === undefined ? [path] : Array.from({ length: series.count }, (_, i) => frameRegionName(path, series, i));
106
+ out.push({ where: `skin "${skin.name}" slot "${slot}" placeholder "${placeholder}"`, names, atlasNull: record.kind === 'region' && record.atlas === null });
107
+ }
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /** `A00_MODEL_REGIONS_ON_PAGES`'s body. Returns whether the document passed it. */
114
+ export function modelRegionsOnPages({ fail, skip }: Verdicts, read: ReadDocument): boolean {
115
+ let records: ReturnType<typeof regionNamesOf>;
116
+ try {
117
+ records = regionNamesOf(read);
118
+ } catch (err) {
119
+ // A series `readModel` keeps only the count of: `readUvSequences` refuses its `start` or `digits` by path.
120
+ if (!(err instanceof CoreInputError)) throw err;
121
+ fail(A00_MODEL_REGIONS_ON_PAGES, err.message);
122
+ return false;
123
+ }
124
+ if (records.length === 0) {
125
+ skip(A00_MODEL_REGIONS_ON_PAGES, SKIP_NO_REGION_RECORD);
126
+ return true;
127
+ }
128
+ if (read.doc.pages === null) {
129
+ 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`);
130
+ return false;
131
+ }
132
+ const lookup = documentPageLookup(read.doc.pages);
133
+ let held = true;
134
+ for (const { where, names, atlasNull } of records) {
135
+ if (atlasNull) {
136
+ 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`);
137
+ held = false;
138
+ }
139
+ for (const name of names) {
140
+ if (lookup(name) !== null) continue;
141
+ fail(A00_MODEL_REGIONS_ON_PAGES, `${where}: resolves through the region ${JSON.stringify(name)}, which no page of the document holds`);
142
+ held = false;
143
+ }
144
+ }
145
+ return held;
146
+ }
@@ -0,0 +1,67 @@
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
+ * and each skin's slot keys in the order `fileSkinOrder` gives
13
+ * (`src/compile.ts`, issue #1034) — a `rigc-compiled/3` document's
14
+ * `editorOrder`, else the emitter's `editorSkinOrder` and
15
+ * `editorSlotKeyOrder`, the functions `emitSkins` is handed — NOT the draw order `./skin_entries.ts` walks, because A08 walks the
16
+ * file's keys and the loaded skins do not — and a slot's records in its
17
+ * table's order. A record of a kind that resolves a region (`CORE_REGION_KINDS`:
18
+ * region, mesh, linked mesh) joins under its `name`, else its placeholder,
19
+ * through its `path`, else that name; a `sequence` makes the lookups
20
+ * `attachmentRegionLookups` makes of the Spine file's, called on the
21
+ * document's own sequence record.
22
+ *
23
+ * Links nothing from the runtime.
24
+ */
25
+ import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
26
+ import { CORE_REGION_KINDS } from '../../core/index.ts';
27
+ import type { SkinTableEntry } from '../../model.ts';
28
+ import type { RegionJoinFacts } from '../facts/region_joins.ts';
29
+ import { attachmentRegionLookups, type AttachmentRegionJoin } from '../region_lookups.ts';
30
+ import { isObj, type Json } from '../values.ts';
31
+ import type { ReadDocument } from './parse.ts';
32
+
33
+ /** The skins and each skin's slot keys in an order other than the file's — a parameter only so the selftest can show what that order would print. */
34
+ export type SlotKeyWalk = <T extends Json & { name: string }>(skins: readonly T[]) => Array<{ skin: T; slots: readonly string[] }>;
35
+
36
+ /**
37
+ * The joins, with the skins and their slot keys in the file's order — the order
38
+ * a `rigc-compiled/3` document states, else the emitter's rule
39
+ * (`fileSkinOrder`, issue #1034) — unless the selftest hands `order`, the
40
+ * skins and each skin's slot keys in another order, to show what it would
41
+ * print (`VF09`, `VF11`).
42
+ */
43
+ export function modelRegionJoinsWith(read: ReadDocument, order?: SlotKeyWalk): RegionJoinFacts {
44
+ const pages = read.doc.pages;
45
+ const regionNames = pages === null ? null : pages.flatMap((page) => page.regions.map((region) => region.name));
46
+ const stated = (Array.isArray(read.json.skins) ? read.json.skins.filter(isObj) : []).filter((skin): skin is Json & { name: string } => typeof skin.name === 'string');
47
+ const joins: AttachmentRegionJoin[] = [];
48
+ const walk = order === undefined ? skinsInFileOrder(stated, fileSkinOrder(read.doc)) : order(stated);
49
+ for (const { skin, slots } of walk) {
50
+ if (!isObj(skin.attachments)) continue;
51
+ for (const slot of slots) {
52
+ const table = skin.attachments[slot];
53
+ if (!isObj(table)) continue;
54
+ for (const [placeholder, record] of Object.entries(table)) {
55
+ if (!isObj(record) || !CORE_REGION_KINDS.has(record.kind as SkinTableEntry['kind'])) continue;
56
+ const name = typeof record.name === 'string' ? record.name : placeholder;
57
+ const path = typeof record.path === 'string' ? record.path : name;
58
+ joins.push({ skin: skin.name, slot, placeholder, name, lookups: attachmentRegionLookups(record.sequence, path) });
59
+ }
60
+ }
61
+ }
62
+ return { regionNames, joins };
63
+ }
64
+
65
+ export function modelRegionJoins(read: ReadDocument): RegionJoinFacts {
66
+ return modelRegionJoinsWith(read);
67
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The runtime's words for each timeline a document's animation can hold
3
+ * (issue #1054): the class it loads as, the property ids it registers and what
4
+ * those ids carry — the values A40's sentence prints, which the document has no
5
+ * field for and the core has no reason to know.
6
+ *
7
+ * ⭐ **Why they live here and not beside the core's table.** Until issue #1054
8
+ * this table stood in `src/core/additive.ts`, beside the mode each kind's
9
+ * application has: 34 runtime class names and their property names as string
10
+ * literals in a module that otherwise speaks the document's words (#1011's
11
+ * second ask). The mode is the core's — it decides what the probe computes —
12
+ * and stays there, keyed by the document's own words (`ADDITIVE_MODE`); what
13
+ * the runtime calls the timeline is a fact about the validator's other
14
+ * supplier, and sits here with the constraint classes' names
15
+ * (`RUNTIME_CONSTRAINT_CLASS`, `./constraints.ts`). Both tables are keyed by
16
+ * the same spelling (`additiveSpelling`), and the selftest's `CO27` holds
17
+ * every row of both against the timeline objects spine-core loads, over a
18
+ * seeded population reaching every spelling.
19
+ *
20
+ * Read off the loaded timeline objects, never written from the runtime's
21
+ * source: the class is the object's constructor name, the properties its
22
+ * registered ids' names.
23
+ *
24
+ * Links nothing from the runtime.
25
+ */
26
+
27
+ /** What a timeline's property ids are followed by: its target's index (and, for an attachment's, the attachment), or nothing. */
28
+ export type RuntimeTimelineAddress = 'bone' | 'slot' | 'attachment' | 'constraint' | 'none';
29
+
30
+ /** One timeline spelling, as the runtime loads it: its class, the properties it registers in order, and what its ids carry. */
31
+ export interface RuntimeTimelineRow {
32
+ runtimeClass: string;
33
+ properties: readonly string[];
34
+ address: RuntimeTimelineAddress;
35
+ }
36
+
37
+ const row = (runtimeClass: string, properties: readonly string[], address: RuntimeTimelineAddress): RuntimeTimelineRow => ({ runtimeClass, properties, address });
38
+
39
+ /** Every timeline spelling a document's animation can hold (`additiveSpelling`'s keys), as the runtime loads it. */
40
+ export const RUNTIME_TIMELINE: Readonly<Record<string, RuntimeTimelineRow>> = {
41
+ 'bone rotate': row('RotateTimeline', ['rotate'], 'bone'),
42
+ 'bone translate': row('TranslateTimeline', ['x', 'y'], 'bone'),
43
+ 'bone translatex': row('TranslateXTimeline', ['x'], 'bone'),
44
+ 'bone translatey': row('TranslateYTimeline', ['y'], 'bone'),
45
+ 'bone scale': row('ScaleTimeline', ['scaleX', 'scaleY'], 'bone'),
46
+ 'bone scalex': row('ScaleXTimeline', ['scaleX'], 'bone'),
47
+ 'bone scaley': row('ScaleYTimeline', ['scaleY'], 'bone'),
48
+ 'bone shear': row('ShearTimeline', ['shearX', 'shearY'], 'bone'),
49
+ 'bone shearx': row('ShearXTimeline', ['shearX'], 'bone'),
50
+ 'bone sheary': row('ShearYTimeline', ['shearY'], 'bone'),
51
+ 'bone inherit': row('InheritTimeline', ['inherit'], 'bone'),
52
+ 'slot attachment': row('AttachmentTimeline', ['attachment'], 'slot'),
53
+ 'slot rgba': row('RGBATimeline', ['rgb', 'alpha'], 'slot'),
54
+ 'slot rgb': row('RGBTimeline', ['rgb'], 'slot'),
55
+ 'slot alpha': row('AlphaTimeline', ['alpha'], 'slot'),
56
+ 'slot rgba2': row('RGBA2Timeline', ['rgb', 'alpha', 'rgb2'], 'slot'),
57
+ 'slot rgb2': row('RGB2Timeline', ['rgb', 'rgb2'], 'slot'),
58
+ 'attachment deform': row('DeformTimeline', ['deform'], 'attachment'),
59
+ 'attachment sequence': row('SequenceTimeline', ['sequence'], 'attachment'),
60
+ ik: row('IkConstraintTimeline', ['ikConstraint'], 'constraint'),
61
+ transform: row('TransformConstraintTimeline', ['transformConstraint'], 'constraint'),
62
+ 'path position': row('PathConstraintPositionTimeline', ['pathConstraintPosition'], 'constraint'),
63
+ 'path spacing': row('PathConstraintSpacingTimeline', ['pathConstraintSpacing'], 'constraint'),
64
+ 'path mix': row('PathConstraintMixTimeline', ['pathConstraintMix'], 'constraint'),
65
+ 'physics inertia': row('PhysicsConstraintInertiaTimeline', ['physicsConstraintInertia'], 'constraint'),
66
+ 'physics strength': row('PhysicsConstraintStrengthTimeline', ['physicsConstraintStrength'], 'constraint'),
67
+ 'physics damping': row('PhysicsConstraintDampingTimeline', ['physicsConstraintDamping'], 'constraint'),
68
+ 'physics mass': row('PhysicsConstraintMassTimeline', ['physicsConstraintMass'], 'constraint'),
69
+ 'physics wind': row('PhysicsConstraintWindTimeline', ['physicsConstraintWind'], 'constraint'),
70
+ 'physics gravity': row('PhysicsConstraintGravityTimeline', ['physicsConstraintGravity'], 'constraint'),
71
+ 'physics mix': row('PhysicsConstraintMixTimeline', ['physicsConstraintMix'], 'constraint'),
72
+ // A reset registers its property with no constraint index, named or not (measured: both read `physicsConstraintReset`).
73
+ 'physics reset': row('PhysicsConstraintResetTimeline', ['physicsConstraintReset'], 'none'),
74
+ 'slider time': row('SliderTimeline', ['sliderTime'], 'constraint'),
75
+ 'slider mix': row('SliderMixTimeline', ['sliderMix'], 'constraint'),
76
+ drawOrder: row('DrawOrderTimeline', ['drawOrder'], 'none'),
77
+ events: row('EventTimeline', ['event'], 'none'),
78
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The model side's supply of `SequenceFacts` (issue #1025, cut 4c-3 of step
3
+ * 4c of #380).
4
+ *
5
+ * - **The skin entries**, in the file's order: the skins and each skin's slot
6
+ * keys as `fileSkinOrder` gives them (`src/compile.ts`, issue #1034 — a
7
+ * `rigc-compiled/3` document's stated `editorOrder`, else the emitter's
8
+ * rule; the walk `./region_joins.ts` makes for A08, which walks the same
9
+ * keys), and a slot's records in its table's order. A record of a kind that draws a
10
+ * region (`CORE_REGION_KINDS`) is an entry; its `name` and `path` are the
11
+ * record's own, which the emitter writes unchanged, and its `sequence` is
12
+ * spelled as the file spells it — written by the emitter's own
13
+ * `emitSequenceBlock` (the document's block also carries each frame's atlas
14
+ * rectangle, which the file does not) and passed through
15
+ * `withoutParserDefaults`, which leaves out `start: 1` and `setup: 0`. A
16
+ * document the reader and the region rule accepted loads every record, so
17
+ * every entry is `loaded`.
18
+ * - **The sequence timelines**: the animations in `fileAnimationOrder`'s order,
19
+ * and each animation's `attachments` group at its three levels (skin, slot,
20
+ * attachment) in the order the file keys them — `keyedOrder` over the
21
+ * document's order, which is the model's, as `src/deformstructure.ts` walks
22
+ * the same group (issue #1034) — passed through
23
+ * `withoutParserDefaults` and `inEditorKeyOrder` (`src/keyorder.ts`), so a
24
+ * key's `delay` the parser would read off the key before it is left out
25
+ * here exactly where the file leaves it out, and A46 prints what it prints
26
+ * over the file.
27
+ * - **A slot at a pose**: the core's raw entry (`poseRawAnimation`,
28
+ * `src/core/raw.ts`) taking one step of the time on a fresh track, under the
29
+ * view the core poses with no skin set (`noSkinView`) — A45's posed colour
30
+ * and A43's posed tint are taken the same way. The entry the slot shows is
31
+ * the pose's shown record; the entry whose timelines it plays is the core's
32
+ * own rule for a linked mesh that plays its source's (`timelineIdentity`,
33
+ * whose condition is the record's `timelines`); and the region its series resolves to is `drawnRegions`
34
+ * (`src/core/uvs.ts`), which refuses by name the one case whose reading was
35
+ * not measured — a linked mesh with a series of its own, stepped over its
36
+ * source's frame count. No gate samples the times A46 asks for (mid-frame,
37
+ * and each `hold` key's own time), so the selftest compares these values
38
+ * with spine-core's at every time the body asks, on every call with a model
39
+ * in hand (`VF12`).
40
+ *
41
+ * Links nothing from the runtime.
42
+ */
43
+ import { fileAnimationOrder, fileSkinOrder, keyedOrder, skinsInFileOrder } from '../../compile.ts';
44
+ import { CORE_REGION_KINDS, CoreInputError, type CompiledDocument } from '../../core/index.ts';
45
+ import { poseRawAnimation } from '../../core/raw.ts';
46
+ import { documentPageLookup, drawnRegions, readUvSequences } from '../../core/uvs.ts';
47
+ import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
48
+ import { emitSequenceBlock } from '../../emit_spine.ts';
49
+ import type { ModelSequence, SkinTableEntry } from '../../model.ts';
50
+ import { noSkinView } from '../../render_core.ts';
51
+ import { entryAddress, type SequenceFacts, type SequenceSkinEntry, type SequenceTimeline } from '../facts/sequences.ts';
52
+ import { isObj, type Json } from '../values.ts';
53
+ import type { ReadDocument } from './parse.ts';
54
+
55
+ const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
56
+
57
+ /** The skin entries that draw a region, in the file's order, each `sequence` spelled as the file spells it. */
58
+ function fileEntries(read: ReadDocument): SequenceSkinEntry[] {
59
+ const stated = list(read.json.skins).filter((skin): skin is Json & { name: string } => typeof skin.name === 'string');
60
+ const picked: Array<{ skin: string; slot: string; placeholder: string; record: Json }> = [];
61
+ const shell: Json[] = [];
62
+ for (const { skin, slots } of skinsInFileOrder(stated, fileSkinOrder(read.doc))) {
63
+ if (!isObj(skin.attachments)) continue;
64
+ const attachments: Json = {};
65
+ for (const slot of slots) {
66
+ const table = skin.attachments[slot];
67
+ if (!isObj(table)) continue;
68
+ const placeholders: Json = {};
69
+ for (const [placeholder, record] of Object.entries(table)) {
70
+ if (!isObj(record) || !CORE_REGION_KINDS.has(record.kind as SkinTableEntry['kind'])) continue;
71
+ picked.push({ skin: skin.name, slot, placeholder, record });
72
+ // Only what the omission pass reads to spell the series: the attachment's type, then the block as the emitter writes it
73
+ // (`emitSequenceBlock`: the four fields the file carries, never the document's per-frame `atlas`, in a fresh object — the pass edits in place).
74
+ placeholders[placeholder] = { ...(record.kind === 'region' ? {} : { type: record.kind }), ...(isObj(record.sequence) ? { sequence: emitSequenceBlock(record.sequence as unknown as ModelSequence) } : {}) };
75
+ }
76
+ attachments[slot] = placeholders;
77
+ }
78
+ shell.push({ name: skin.name, attachments });
79
+ }
80
+ withoutParserDefaults({ skins: shell });
81
+ return picked.map(({ skin, slot, placeholder, record }) => {
82
+ const spelled = (((shell.find((s) => s.name === skin) as Json).attachments as Json)[slot] as Json)[placeholder] as Json;
83
+ return { skin, slot, placeholder, name: record.name, path: record.path, sequence: spelled.sequence, loaded: true };
84
+ });
85
+ }
86
+
87
+ /** Names in the order each first appears, put in the order a JSON object keyed by them lists them (`keyedOrder`). */
88
+ const keyedFirsts = (names: readonly string[]): string[] => keyedOrder(names.filter((name, i) => names.indexOf(name) === i));
89
+
90
+ /** Every animation's sequence timelines in the file's order and spelling: `animations.<name>.attachments.<skin>.<slot>.<placeholder>.sequence = keys`. */
91
+ function fileSequenceTimelines(read: ReadDocument): SequenceTimeline[] {
92
+ const byName = new Map<string, Json>();
93
+ for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
94
+ type Level = { name: string; json: Json };
95
+ const named = (v: unknown): Level[] => list(v).flatMap((json) => (typeof json.name === 'string' ? [{ name: json.name, json }] : []));
96
+ /** One level of the group in the order the file keys it: `keyedOrder` over the document's order, each name's entries together. */
97
+ const inFileOrder = (levels: Level[]): Array<[string, Json[]]> => keyedFirsts(levels.map((l) => l.name)).map((name) => [name, levels.filter((l) => l.name === name).map((l) => l.json)]);
98
+ const order = fileAnimationOrder(read.doc);
99
+ const walk: Array<{ animation: string; skin: string; slot: string; placeholder: string }> = [];
100
+ const animations: Json = {};
101
+ for (const name of order) {
102
+ const anim = byName.get(name);
103
+ if (anim === undefined) continue;
104
+ const bySkin: Json = {};
105
+ for (const [skin, skinEntries] of inFileOrder(named(anim.attachments))) {
106
+ const bySlot: Json = {};
107
+ for (const [slot, slotEntries] of inFileOrder(skinEntries.flatMap((e) => named(e.slots)))) {
108
+ const byAttachment: Json = {};
109
+ for (const [placeholder, attachments] of inFileOrder(slotEntries.flatMap((e) => named(e.attachments)))) {
110
+ const timelines: Json = {};
111
+ // `deform` then `sequence`, each only where the document holds it — `emitAttachmentTimelines`' order.
112
+ for (const attachment of attachments) {
113
+ for (const word of ['deform', 'sequence'] as const) if (Array.isArray(attachment[word])) timelines[word] = list(attachment[word]).map((key) => ({ ...key }));
114
+ }
115
+ byAttachment[placeholder] = timelines;
116
+ if (Array.isArray(timelines.sequence)) walk.push({ animation: name, skin, slot, placeholder });
117
+ }
118
+ bySlot[slot] = byAttachment;
119
+ }
120
+ bySkin[skin] = bySlot;
121
+ }
122
+ animations[name] = Object.keys(bySkin).length > 0 ? { attachments: bySkin } : {};
123
+ }
124
+ // The emitter's own passes over the keys (`withoutParserDefaults`, `inEditorKeyOrder`), then the walk read back in the order built above.
125
+ const file = inEditorKeyOrder(withoutParserDefaults({ animations }));
126
+ return walk.map(({ animation, skin, slot, placeholder }) => {
127
+ const timelines = ((((file.animations[animation] as Json).attachments as Json)[skin] as Json)[slot] as Json)[placeholder] as Json;
128
+ return { animation, skin, slot, placeholder, keys: timelines.sequence as unknown[] };
129
+ });
130
+ }
131
+
132
+ export function modelSequences(read: ReadDocument): SequenceFacts {
133
+ let view: CompiledDocument | null = null;
134
+ return {
135
+ entries: fileEntries(read),
136
+ timelines: fileSequenceTimelines(read),
137
+ hasSlot: (slot) => read.doc.slots.some((s) => s.name === slot),
138
+ duration: (animation) => read.doc.animations.find((a) => a.name === animation)?.timelines.duration ?? null,
139
+ posedFrame: (animation, slot, time) => {
140
+ view ??= noSkinView(read.doc);
141
+ const pose = poseRawAnimation(view, animation, [time], {}, 'animation')[1];
142
+ const shown = pose.shown.find((s) => s.slot === slot);
143
+ if (shown === undefined) return null;
144
+ const at = entryAddress(shown.skin, slot, shown.placeholder);
145
+ const g = shown.geometry;
146
+ const record = view.skins.find((k) => k.name === shown.skin)?.attachments[slot]?.[shown.placeholder];
147
+ // A linked mesh that plays its source's timelines plays them as its source (`timelineIdentity` in `src/core/deform.ts`, the
148
+ // runtime's `timelineAttachment`); every other entry plays its own.
149
+ const playsAs = g.kind === 'linkedmesh' && record?.timelines === true ? entryAddress(g.skin, g.slot, g.source) : at;
150
+ if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') return { shown: at, playsAs, region: null };
151
+ if (read.doc.pages === null) throw new CoreInputError('the document states no pages, so no region a series resolves to can be named');
152
+ const resolved = drawnRegions(view, pose.shown, [slot], { sequences: readUvSequences(read.json), lookup: documentPageLookup(read.doc.pages) });
153
+ if (resolved.drawn === null) throw new CoreInputError(`the core does not name the region slot "${slot}" shows at t=${time}: ${resolved.why}`);
154
+ return { shown: at, playsAs, region: resolved.drawn[0]?.region ?? null };
155
+ },
156
+ };
157
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The model side's supply of `SkeletonRosterFacts` (issue #1025, cut 4c-4 of
3
+ * #380): the document's bones and slots, in the document's order.
4
+ *
5
+ * The emitter writes the file's `bones` and `slots` arrays in the model's
6
+ * order and never re-sorts them (`emitBones`, `emitSlots` in
7
+ * `src/emit_spine.ts`; a slot's order is the draw order a draw-order key's
8
+ * offsets count in), and the document writes each list in that same order, so
9
+ * the document's lists are the file's. A bone's parent is the name the
10
+ * document states, `null` for a root; a slot declares a dark colour where the
11
+ * document states one, which is where `emitSlots` writes the key.
12
+ *
13
+ * Links nothing from the runtime.
14
+ */
15
+ import type { SkeletonRosterFacts } from '../facts/skeleton_roster.ts';
16
+ import type { ReadDocument } from './parse.ts';
17
+
18
+ export function modelSkeletonRoster(read: ReadDocument): SkeletonRosterFacts {
19
+ return {
20
+ bones: read.doc.bones.map((bone) => ({ name: bone.name, parent: bone.parent ?? null })),
21
+ slots: read.doc.slots.map((slot) => ({ name: slot.name, dark: slot.dark !== undefined })),
22
+ };
23
+ }
@@ -0,0 +1,69 @@
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
+ * `fileSkinOrder` from `src/compile.ts` (issue #1034): the order a
12
+ * `rigc-compiled/3` document states, else `editorSkinOrder`, the function the
13
+ * emitter is handed (`emitSkins`) — never a copy of its rule. Within a skin the entries go
14
+ * slot by slot in the skeleton's slot order — the document's `slots`, which is
15
+ * the emitted array's order — and a slot's entries in its table's order, which
16
+ * the emitter writes unchanged; the selftest measures this walk equal to
17
+ * spine-core's on every multi-skin build it makes.
18
+ *
19
+ * A region's name is the record's `name`, else its placeholder — `shownRow`'s
20
+ * rule in the core — and its size the record's `width` and `height`, the
21
+ * numbers the file states (`readModel` refuses a size that is not a finite
22
+ * number, so a size the runtime would load as NaN has no document to arrive in).
23
+ *
24
+ * Links nothing from the runtime.
25
+ */
26
+ import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
27
+ import { shownRow } from '../../core/index.ts';
28
+ import type { RegionEntry, SkinEntryFacts } from '../facts/skin_entries.ts';
29
+ import type { ReadDocument } from './parse.ts';
30
+
31
+ /** How the skins are put in an order other than the file's. A parameter only so the selftest can show what the document's own order would print (`VF05`). */
32
+ export type SkinOrder = <T extends { name: string }>(skins: readonly T[]) => T[];
33
+
34
+ /**
35
+ * The document's skins in the file's order (issue #1034): `fileSkinOrder` —
36
+ * a `rigc-compiled/3` document's `editorOrder`, the one source on `/3`, else
37
+ * the emitter's own rule (`editorSkinOrder`) over the document's skins.
38
+ */
39
+ export function skinsInTheFilesOrder<T extends { name: string }>(read: ReadDocument, skins: readonly T[]): T[] {
40
+ return skinsInFileOrder(skins, fileSkinOrder(read.doc)).map((walk) => walk.skin);
41
+ }
42
+
43
+ /** The skins' entries in the file's walk order, each with its skin, slot and placeholder; `order`, when given, replaces the file's skin order (`VF05`). */
44
+ export function fileOrderedEntries(read: ReadDocument, order?: SkinOrder): Array<{ skin: string; slot: string; placeholder: string; record: ReadDocument['doc']['skins'][number]['attachments'][string][string] }> {
45
+ const out: ReturnType<typeof fileOrderedEntries> = [];
46
+ for (const skin of order === undefined ? skinsInTheFilesOrder(read, read.doc.skins) : order(read.doc.skins)) {
47
+ for (const { name: slot } of read.doc.slots) {
48
+ const table = skin.attachments[slot];
49
+ if (table === undefined) continue;
50
+ for (const [placeholder, record] of Object.entries(table)) out.push({ skin: skin.name, slot, placeholder, record });
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+
56
+ export function modelSkinEntries(read: ReadDocument, order?: SkinOrder): SkinEntryFacts {
57
+ const regionAttachments: RegionEntry[] = [];
58
+ let clippingCount = 0;
59
+ for (const entry of fileOrderedEntries(read, order)) {
60
+ if (entry.record.kind === 'clipping') clippingCount++;
61
+ if (entry.record.kind !== 'region') continue;
62
+ const geometry = entry.record.geometry;
63
+ // `readModel` reads every region record's geometry or refuses the document, so a record without one is this module's defect.
64
+ 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`);
65
+ const shown = shownRow(entry);
66
+ regionAttachments.push({ name: shown.name, width: geometry.region.width, height: geometry.region.height, path: shown.path as string });
67
+ }
68
+ return { regionAttachments, clippingCount };
69
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The model side's supply of `SkinMemberFacts` (issue #1025, step 4c of
3
+ * #380): the bones, the constraints and each skin's lists, read off the
4
+ * document and joined the way the runtime's loader joins them.
5
+ *
6
+ * - **Bones**: the document's `bones` in order, each with its `skinRequired`
7
+ * as stated (absent is not required) and its parent entry.
8
+ * - **Constraints**: the document's `constraints` in order — update order, the
9
+ * order the emitter writes them in — each skin-required exactly when it
10
+ * states `skin: true`, the field the emitter writes through unchanged.
11
+ * - **Skins**: in the file's order (`fileSkinOrder`, `src/compile.ts`: a
12
+ * `rigc-compiled/3` document's `editorOrder`, else the emitter's
13
+ * `editorSkinOrder`, issue #1034); a
14
+ * skin's bones as it lists them, each the FIRST bone of that name; its
15
+ * constraints kind by kind in the emitter's key order
16
+ * (`RIG_SKIN_CONSTRAINT_KEYS`, which `emitSkins` walks), each the first
17
+ * constraint of that kind and name — the object the loader finds, and the
18
+ * same object `constraints` holds, so a body asking whether one is listed
19
+ * asks by identity. `readModel` refuses a skin listing a constraint of a
20
+ * kind and name the document lacks; a bone name it lacks is this module's to
21
+ * refuse, by name.
22
+ *
23
+ * Links nothing from the runtime.
24
+ */
25
+ import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
26
+ import { RIG_SKIN_CONSTRAINT_KEYS } from '../../rig.ts';
27
+ import type { MemberBone, MemberConstraint, MemberSkin, SkinMemberFacts } from '../facts/skin_members.ts';
28
+ import { isObj } from '../values.ts';
29
+ import type { ReadDocument } from './parse.ts';
30
+
31
+ export function modelSkinMembers(read: ReadDocument): SkinMemberFacts {
32
+ const doc = read.doc;
33
+ const bones: MemberBone[] = [];
34
+ const boneByName = new Map<string, MemberBone>();
35
+ for (const bone of doc.bones) {
36
+ const parent = bone.parent === undefined ? null : boneByName.get(bone.parent);
37
+ if (parent === undefined) throw new Error(`internal: bone "${bone.name}" names parent "${bone.parent}", which the reader accepted and no earlier bone is`);
38
+ const entry: MemberBone = { name: bone.name, skinRequired: bone.skinRequired === true, parent };
39
+ bones.push(entry);
40
+ if (!boneByName.has(bone.name)) boneByName.set(bone.name, entry);
41
+ }
42
+ const constraints: Array<MemberConstraint & { kind: string }> = [];
43
+ const stated = Array.isArray(read.json.constraints) ? read.json.constraints.filter(isObj) : [];
44
+ for (const constraint of stated) {
45
+ constraints.push({ name: String(constraint.name), kind: String(constraint.kind), skinRequired: constraint.skin === true });
46
+ }
47
+ const skins: MemberSkin[] = skinsInFileOrder(doc.skins, fileSkinOrder(doc)).map(({ skin }) => {
48
+ const listedBones = skin.bones.map((name) => {
49
+ const found = boneByName.get(name);
50
+ if (found === undefined) throw new Error(`skin "${skin.name}" lists bone "${name}", which is not a bone of the document`);
51
+ return found;
52
+ });
53
+ const listedConstraints: MemberConstraint[] = [];
54
+ for (const kind of RIG_SKIN_CONSTRAINT_KEYS) {
55
+ for (const name of skin.constraints[kind]) {
56
+ const found = constraints.find((c) => c.kind === kind && c.name === name);
57
+ if (found === undefined) throw new Error(`internal: skin "${skin.name}" lists ${kind} constraint "${name}", which the reader accepted and the document does not hold`);
58
+ listedConstraints.push(found);
59
+ }
60
+ }
61
+ return { name: skin.name, bones: listedBones, constraints: listedConstraints };
62
+ });
63
+ return { skins, bones, constraints };
64
+ }