rig-c 0.0.0-stage → 2.21.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 (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 +1191 -0
  185. package/src/meshquality.ts +2051 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1444 -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,193 @@
1
+ /**
2
+ * The model side's supply of `SliderCompositionFacts` (issue #1025, cut 4c-5
3
+ * of step 4c of #380): the document's sliders in its `constraints` order, the
4
+ * timelines of each animation they apply in the order the runtime builds
5
+ * them, and what each does when applied additively, as the core computes it.
6
+ *
7
+ * - **A slider's fields** are the core's record (`readSliderRecord`): `mix`,
8
+ * `additive` and `skin` read at the parser's value where the document
9
+ * leaves them out. The skins naming it are the document's skins in the
10
+ * file's order (`fileSkinOrder`), each whose `slider` list names it.
11
+ * - **The timelines, in the runtime's order** (`../facts/slider_composition.ts`'
12
+ * ⭐): the slot and bone timelines as the file states them (`fileSlotTimelines`,
13
+ * `fileBoneTimelines` — the emitter's own passes over the document's
14
+ * order), the constraint timelines in the order the runtime builds them
15
+ * (`modelConstraintTimelineSources`), the attachments' deform and sequence
16
+ * timelines at their three levels in the order the file keys them
17
+ * (`keyedOrder` over the document's order, deform before sequence —
18
+ * `emitAttachmentTimelines`' order), then the draw order and the events.
19
+ * - **The words.** Each timeline's class name and properties are the
20
+ * validator's table (`RUNTIME_TIMELINE`, `./runtime_timelines.ts`; the
21
+ * core's until issue #1054), the one value here that is not the document's,
22
+ * measured off the loaded timelines; an id's index is
23
+ * the target's position in the document's bones, slots or constraints —
24
+ * which the emitter writes in that order — `-1` for a physics timeline
25
+ * naming none; a deform's attachment is named as the runtime names it, the
26
+ * record's `name`, else its placeholder.
27
+ * - **The behaviour** is the core's probe over the document under no skin set
28
+ * (`noSkinView`, the view measured in issue #1051) and under each skin
29
+ * (`underSkin`): `additiveCells`, and the
30
+ * class `additiveBehaviour` reads off them. The core suite's `CO27` holds
31
+ * both to the runtime's probe; the selftest asks this supplier every
32
+ * question the body asks the runtime's (`VF14`).
33
+ *
34
+ * Links nothing from the runtime.
35
+ */
36
+ import { fileSkinOrder, keyedOrder } from '../../compile.ts';
37
+ import { additiveBehaviour, additiveCells, additiveSpelling, type AdditiveTimeline, type AdditiveView } from '../../core/additive.ts';
38
+ import { RUNTIME_TIMELINE } from './runtime_timelines.ts';
39
+ import { EVERY_GLOBAL_PHYSICS, type PhysicsTimelineKind } from '../../core/constraints_physics.ts';
40
+ import { underSkin, type CompiledDocument } from '../../core/index.ts';
41
+ import type { BoneTimelineKind, SlotTimelineKind } from '../../core/animation.ts';
42
+ import { noSkinView } from '../../render_core.ts';
43
+ import { entryAddress } from '../facts/sequences.ts';
44
+ import type { SliderCompositionFacts, SliderFact, SliderTimelineFact } from '../facts/slider_composition.ts';
45
+ import { modelConstraintTimelineSources } from './constraints.ts';
46
+ import { fileBoneTimelines } from './bone_timelines.ts';
47
+ import { fileSlotTimelines } from './slot_colour.ts';
48
+ import type { ReadDocument } from './parse.ts';
49
+ import { isObj } from '../values.ts';
50
+
51
+ /** One timeline of an animation: the facts' reading of it and the core's. */
52
+ export interface ModelSliderTimeline {
53
+ fact: SliderTimelineFact;
54
+ core: AdditiveTimeline;
55
+ }
56
+
57
+ /** Names in the order each first appears, put in the order a JSON object keyed by them lists them (`keyedOrder`). */
58
+ const keyedFirsts = (names: readonly string[]): string[] => keyedOrder(names.filter((name, i) => names.indexOf(name) === i));
59
+
60
+ /** The words of one timeline: the table's class and properties, each id addressed by `index` (and `attachment`), and the sentence's naming. */
61
+ function factOf(doc: CompiledDocument, core: AdditiveTimeline): SliderTimelineFact {
62
+ const row = RUNTIME_TIMELINE[additiveSpelling(core)];
63
+ if (row === undefined) throw new Error(`internal: "${additiveSpelling(core)}" is no row of the runtime's timeline table`);
64
+ const property = (p: string, index: number | null, attachment: string | null, names: string) => ({
65
+ id: [p, ...(index === null ? [] : [String(index)]), ...(attachment === null ? [] : [attachment])].join('|'),
66
+ property: p,
67
+ names,
68
+ });
69
+ const slotIndex = (name: string): number => doc.slots.findIndex((s) => s.name === name);
70
+ let properties: SliderTimelineFact['properties'];
71
+ switch (core.group) {
72
+ case 'bone': {
73
+ const index = doc.bones.findIndex((b) => b.name === core.bone);
74
+ properties = row.properties.map((p) => property(p, index, null, `bone "${core.bone}" ${p}`));
75
+ break;
76
+ }
77
+ case 'slot':
78
+ properties = row.properties.map((p) => property(p, slotIndex(core.slot), null, `slot "${core.slot}" ${p}`));
79
+ break;
80
+ case 'attachment': {
81
+ const record = doc.skins.find((k) => k.name === core.skin)?.attachments[core.slot]?.[core.attachment];
82
+ const shown = record?.name ?? core.attachment;
83
+ const address = entryAddress(core.skin, core.slot, core.attachment);
84
+ properties = row.properties.map((p) => property(p, slotIndex(core.slot), address, core.word === 'deform' ? `slot "${core.slot}" deform of "${shown}"` : `slot "${core.slot}" ${p}`));
85
+ break;
86
+ }
87
+ case 'drawOrder':
88
+ case 'events':
89
+ properties = row.properties.map((p) => property(p, null, null, `the skeleton's ${p}`));
90
+ break;
91
+ default: {
92
+ const index = core.group === 'physics' && core.constraint === EVERY_GLOBAL_PHYSICS ? -1 : doc.constraints.findIndex((c) => c.kind === core.group && c.name === core.constraint);
93
+ properties = row.properties.map((p) => property(p, row.address === 'none' ? null : index, null, index >= 0 ? `constraint "${core.constraint}" ${p}` : `the skeleton's ${p}`));
94
+ }
95
+ }
96
+ return { runtimeClass: row.runtimeClass, properties };
97
+ }
98
+
99
+ /** Every timeline of animation `name`, in the order the runtime builds them (the header). */
100
+ export function modelAnimationTimelines(read: ReadDocument, name: string): ModelSliderTimeline[] {
101
+ const { doc } = read;
102
+ const anim = doc.animations.find((a) => a.name === name);
103
+ if (anim === undefined) return [];
104
+ const t = anim.timelines;
105
+ const out: AdditiveTimeline[] = [];
106
+ for (const { slot, timelines } of fileSlotTimelines(read).filter((s) => s.animation === name)) {
107
+ const target = t.slots.find((s) => s.name === slot);
108
+ for (const word of Object.keys(timelines)) {
109
+ const keys = target?.timelines.find((x) => x.kind === word)?.keys;
110
+ if (keys !== undefined) out.push({ group: 'slot', word: word as SlotTimelineKind, slot, keys });
111
+ }
112
+ }
113
+ for (const { bone, timelines } of fileBoneTimelines(read).filter((b) => b.animation === name)) {
114
+ const target = t.bones.find((b) => b.name === bone);
115
+ for (const word of isObj(timelines) ? Object.keys(timelines) : []) {
116
+ const keys = target?.timelines.find((x) => x.kind === word)?.keys;
117
+ if (keys !== undefined) out.push({ group: 'bone', word: word as BoneTimelineKind, bone, keys });
118
+ }
119
+ }
120
+ for (const source of modelConstraintTimelineSources(read).filter((s) => s.animation === name)) {
121
+ switch (source.kind) {
122
+ case 'ik': {
123
+ const keys = anim.constraints.ik.find((x) => x.name === source.name)?.keys;
124
+ if (keys !== undefined) out.push({ group: 'ik', word: 'ik', constraint: source.name, keys });
125
+ break;
126
+ }
127
+ case 'transform':
128
+ out.push({ group: 'transform', word: 'transform', constraint: source.name, keys: source.keys });
129
+ break;
130
+ case 'path':
131
+ out.push({ group: 'path', word: source.word as 'position' | 'spacing' | 'mix', constraint: source.name, keys: source.keys });
132
+ break;
133
+ case 'physics':
134
+ out.push({ group: 'physics', word: source.word as PhysicsTimelineKind, constraint: source.name, keys: source.keys });
135
+ break;
136
+ case 'slider':
137
+ out.push({ group: 'slider', word: source.word as 'time' | 'mix', constraint: source.name, keys: source.keys });
138
+ break;
139
+ }
140
+ }
141
+ const attachments = t.attachments;
142
+ for (const skin of keyedFirsts(attachments.map((a) => a.skin))) {
143
+ const inSkin = attachments.filter((a) => a.skin === skin);
144
+ for (const slot of keyedFirsts(inSkin.map((a) => a.slot))) {
145
+ const inSlot = inSkin.filter((a) => a.slot === slot);
146
+ for (const attachment of keyedFirsts(inSlot.map((a) => a.attachment))) {
147
+ for (const a of inSlot.filter((x) => x.attachment === attachment)) {
148
+ if (a.deform !== null) out.push({ group: 'attachment', word: 'deform', skin, slot, attachment, keys: a.deform });
149
+ if (a.sequence !== null) out.push({ group: 'attachment', word: 'sequence', skin, slot, attachment, keys: a.sequence });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ if (t.drawOrder.length > 0) out.push({ group: 'drawOrder', word: 'drawOrder', keys: t.drawOrder });
155
+ if (t.events.length > 0) out.push({ group: 'events', word: 'events', times: t.events.map((k) => k.time) });
156
+ return out.map((core) => ({ fact: factOf(doc, core), core }));
157
+ }
158
+
159
+ /** The views the probe poses under: no skin set, then every skin in the file's order. */
160
+ export function additiveViews(doc: CompiledDocument): AdditiveView[] {
161
+ return [{ name: '(none)', doc: noSkinView(doc) }, ...fileSkinOrder(doc).map((skin) => ({ name: skin.name, doc: underSkin(doc, skin.name) }))];
162
+ }
163
+
164
+ export function modelSliderComposition(read: ReadDocument): SliderCompositionFacts {
165
+ const { doc } = read;
166
+ const skinOrder = fileSkinOrder(doc).map((skin) => skin.name);
167
+ const walked = new Map<string, ModelSliderTimeline[]>();
168
+ const timelinesOf = (name: string): ModelSliderTimeline[] => {
169
+ const held = walked.get(name);
170
+ if (held !== undefined) return held;
171
+ const fresh = modelAnimationTimelines(read, name);
172
+ walked.set(name, fresh);
173
+ return fresh;
174
+ };
175
+ const sliders: SliderFact[] = [];
176
+ doc.constraints.forEach((c, index) => {
177
+ const r = c.record;
178
+ if (r?.kind !== 'slider') return;
179
+ const animation = doc.animations.some((a) => a.name === r.animation) ? { name: r.animation, timelines: timelinesOf(r.animation).map((x) => x.fact) } : null;
180
+ const skins = skinOrder.filter((name) => doc.skins.some((k) => k.name === name && k.constraints.slider.includes(r.name)));
181
+ sliders.push({ name: r.name, index, mix: r.mix, additive: r.additive, skinRequired: r.skin, skins, animation });
182
+ });
183
+ let views: AdditiveView[] | null = null;
184
+ return {
185
+ sliders,
186
+ behaviour: (animation, at) => {
187
+ const timeline = timelinesOf(animation)[at];
188
+ if (timeline === undefined) throw new Error(`internal: animation "${animation}" has no timeline ${at}`);
189
+ views ??= additiveViews(doc);
190
+ return additiveBehaviour(additiveCells(views, timeline.core));
191
+ },
192
+ };
193
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The model side's supply of `SlotColourFacts` (issue #1025, step 4c of #380).
3
+ *
4
+ * - **The slot timelines, in the file's order and spelling.** The document
5
+ * holds each animation's slot timelines in the order they are applied, with
6
+ * every key as the compiler built it; the Spine file keys the animations in
7
+ * the editor's order and passes every key through two passes. So the
8
+ * animations are put in the file's order by `fileAnimationOrder`
9
+ * (`src/compile.ts`: a `rigc-compiled/3` document's `editorOrder`, else the
10
+ * emitter's own rule over its names, issue #1034), and the timelines are laid out as the
11
+ * file lays them and handed to the emitter's own `withoutParserDefaults` and
12
+ * `inEditorKeyOrder` (`src/keyorder.ts`) — so a key field the emitter would
13
+ * leave out at the parser's default is left out here too, and a body
14
+ * printing `JSON.stringify(key.value)` prints what it prints over the file.
15
+ * Nothing here restates either rule.
16
+ * - **Whether an animation exists**: the document's own roster.
17
+ * - **A slot's posed colour**: the core's raw entry (`poseRawAnimation`,
18
+ * `src/core/raw.ts`) taking one step of the time on a fresh non-looping
19
+ * track, under the view the core poses with no skin set (`noSkinView`,
20
+ * `src/render_core.ts`) — the reading of what `validate()` does through
21
+ * spine-core (a fresh skeleton, no skin, setup pose, the track stepped to the
22
+ * time and applied). Its slot rows are the ones `CR03` holds bit for bit to
23
+ * spine-core on every frame of the nineteen recipes; that the two readings
24
+ * print the same A45 line is the selftest's supplier check, on every call
25
+ * that poses one (`VF02`). A channel the pose read as no number is NaN, as a
26
+ * non-finite channel reads on the runtime's side.
27
+ *
28
+ * Links nothing from the runtime.
29
+ */
30
+ import { fileAnimationOrder } from '../../compile.ts';
31
+ import type { CompiledDocument } from '../../core/index.ts';
32
+ import { poseRawAnimation } from '../../core/raw.ts';
33
+ import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
34
+ import { noSkinView } from '../../render_core.ts';
35
+ import type { SlotColourFacts, SlotTimelines } from '../facts/slot_colour.ts';
36
+ import { isObj, type Json } from '../values.ts';
37
+ import type { ReadDocument } from './parse.ts';
38
+
39
+ const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
40
+
41
+ /** Every animation's slot timelines as the Spine file states them: `animations.<name>.slots.<slot>.<timeline> = keys`. */
42
+ export function fileSlotTimelines(read: ReadDocument): SlotTimelines[] {
43
+ const byName = new Map<string, Json>();
44
+ for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
45
+ const animations: Json = {};
46
+ for (const name of fileAnimationOrder(read.doc)) {
47
+ const slots: Json = {};
48
+ for (const slot of list((byName.get(name) as Json).slots)) {
49
+ if (typeof slot.name !== 'string') continue;
50
+ const timelines: Json = {};
51
+ for (const timeline of list(slot.timelines)) {
52
+ if (typeof timeline.name === 'string') timelines[timeline.name] = list(timeline.keys).map((key) => ({ ...key }));
53
+ }
54
+ slots[slot.name] = timelines;
55
+ }
56
+ // An animation keying no slot writes no `slots` group — `emitAnimation`'s `if (animation.slots.size)`.
57
+ animations[name] = Object.keys(slots).length > 0 ? { slots } : {};
58
+ }
59
+ const file = inEditorKeyOrder(withoutParserDefaults({ animations }));
60
+ const out: SlotTimelines[] = [];
61
+ for (const [animation, anim] of Object.entries(file.animations)) {
62
+ if (!isObj(anim) || !isObj(anim.slots)) continue;
63
+ for (const [slot, timelines] of Object.entries(anim.slots)) if (isObj(timelines)) out.push({ animation, slot, timelines });
64
+ }
65
+ return out;
66
+ }
67
+
68
+ export function modelSlotColour(read: ReadDocument): SlotColourFacts {
69
+ let view: CompiledDocument | null = null;
70
+ return {
71
+ slotTimelines: fileSlotTimelines(read),
72
+ hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
73
+ posedSlot: (animation, slot, time) => {
74
+ view ??= noSkinView(read.doc);
75
+ const row = poseRawAnimation(view, animation, [time], {}, 'animation')[1].slots.find((r) => r[0] === slot);
76
+ if (row === undefined) return undefined;
77
+ const channel = (v: number | null): number => (v === null ? Number.NaN : v);
78
+ return { color: { r: channel(row[2]), g: channel(row[3]), b: channel(row[4]), a: channel(row[5]) } };
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The model side's supply of `StageFacts` (issue #1025, step 4c of #380): the
3
+ * stage a `rigc-compiled/3` document states (`stated.stage`, issue #1026), or,
4
+ * for a `/2` or `/1` document, which does not, the stage its caller gives.
5
+ * A `/3` document given one anyway is refused (`refuseGivenBeside`).
6
+ *
7
+ * ⚠️ A caller that gives no stage at all is refused by name rather than read as
8
+ * "the skeleton declares none": those are two different statements, and only
9
+ * the second is one the caller made. A stage stated as `null` IS that second
10
+ * statement, and reads as both extents absent, the way the runtime loads a
11
+ * header with neither key.
12
+ *
13
+ * Links nothing from the runtime.
14
+ */
15
+ import type { StageFacts } from '../facts/stage.ts';
16
+ import { refuseGivenBeside, type ModelGiven } from './given.ts';
17
+ import type { ReadDocument } from './parse.ts';
18
+
19
+ export function modelStage(read: ReadDocument, given: ModelGiven | undefined): StageFacts {
20
+ refuseGivenBeside(read, given);
21
+ // The document's own statement (issue #1026): `null` is a skeleton that declares none, read as both extents absent.
22
+ if (read.doc.stated !== null) {
23
+ const stage = read.doc.stated.stage;
24
+ return { width: stage?.width, height: stage?.height };
25
+ }
26
+ if (given === undefined) throw new Error(`the caller gave no stage; a ${read.doc.spec} document does not state one, so the model side cannot read it`);
27
+ return { width: given.stage?.width, height: given.stage?.height };
28
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The model side's supply of `StageBoxFacts` (issue #1168): the box the
3
+ * document's `stage.box` asks for, and what the document's `default` skin
4
+ * holds there — its record as the runtime would load it, and its world
5
+ * vertices at the setup pose as rigc's core poses them (`poseSetup` under no
6
+ * skin, constraints applied, then `worldVertices`, the function every vertex
7
+ * attachment of the core is posed by — no second arithmetic for the box).
8
+ *
9
+ * A `/2` or `/1` document states no stage and asks nothing; a `/3` document
10
+ * whose stage states no box asks nothing, and neither does one that declares no
11
+ * stage. The core's poses are held to spine-core's at tolerance 0 by
12
+ * `tools/core_gate.ts`, which is what lets this side and the round trip print
13
+ * one line.
14
+ *
15
+ * Links nothing from the runtime.
16
+ */
17
+ import { activeBones, poseSetup, rawNumber, underNoSkin } from '../../core/index.ts';
18
+ import { worldVertices } from '../../core/vertices.ts';
19
+ import { SKIP_NO_STAGE_BOX, type StageBoxFacts, type StageBoxHeld } from '../facts/stage_box.ts';
20
+ import type { ReadDocument } from './parse.ts';
21
+
22
+ export function modelStageBox(read: ReadDocument): StageBoxFacts {
23
+ const doc = read.doc;
24
+ const stage = doc.stated?.stage;
25
+ if (doc.stated === null || stage === undefined || stage === null) return { asked: null, why: SKIP_NO_STAGE_BOX, slot: false, held: null };
26
+ if (stage.box === undefined) return { asked: null, why: SKIP_NO_STAGE_BOX, slot: false, held: null };
27
+ const asked = { slot: stage.box.slot, attachment: stage.box.attachment, stage: { x: stage.x, y: stage.y, width: stage.width, height: stage.height } };
28
+ const slot = doc.slots.find((s) => s.name === asked.slot);
29
+ if (slot === undefined) return { asked, why: '', slot: false, held: null };
30
+ const record = doc.skins.find((k) => k.name === 'default')?.attachments[asked.slot]?.[asked.attachment];
31
+ if (record === undefined) return { asked, why: '', slot: true, held: null };
32
+ const g = record.geometry;
33
+ const type = record.kind === 'linkedmesh' ? 'mesh' : record.kind;
34
+ if (g === undefined || g.kind !== 'boundingbox') return { asked, why: '', slot: true, held: { type, weighted: false, stored: [], world: '' } };
35
+ const vertices = g.vertices;
36
+ const stored = vertices.weighted ? [] : vertices.xy.map(Math.fround);
37
+ const held: StageBoxHeld = { type, weighted: vertices.weighted, stored, world: poseWorld(read, slot.bone, vertices) };
38
+ return { asked, why: '', slot: true, held };
39
+ }
40
+
41
+ /** The box's world vertices at the setup pose under no skin, or why there are none. */
42
+ function poseWorld(read: ReadDocument, bone: string, vertices: Parameters<typeof worldVertices>[0]): number[] | string {
43
+ const view = underNoSkin(read.doc);
44
+ if (!activeBones(view).has(bone)) return `its bone "${bone}" is inactive with no skin set, so nothing poses it`;
45
+ const posed = poseSetup(view, { round: rawNumber });
46
+ const world = posed.world;
47
+ if (world === null) return posed.absent.find(([block]) => block === 'setup.bones')?.[1] ?? "rigc's core does not pose this setup pose";
48
+ const frame = world.get(bone);
49
+ if (frame === undefined) return `its bone "${bone}" has no setup world transform`;
50
+ return worldVertices(vertices, frame, world);
51
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The model side's supply of `SteppedPoseFacts` (issue #1025, cut 4c-5a of
3
+ * step 4c of #380): every pose A10 reads, posed by rigc's core.
4
+ *
5
+ * - **The roster**: the document's bones, and its animations in the file's
6
+ * order (`fileAnimationOrder`, `src/compile.ts`), each with
7
+ * `CoreAnimationTimelines.duration` — the last key of every timeline as
8
+ * float32, the value `tools/pose_oracle.ts compare` holds equal to
9
+ * spine-core's on its `animations` roster.
10
+ * - **The setup pose and each animation's walk**: the core's looping walk
11
+ * (`src/core/walk.ts` — `poseWalkSetup`, `poseLoopingWalk`), which reads
12
+ * A10's recipe as measured and returns a value that is not finite in place
13
+ * rather than refusing it. It is held equal to spine-core's walk at
14
+ * tolerance 0 by `tools/core_gate.ts`'s walk block and the selftest's
15
+ * `CO25`, and the poses A10 asks for here are compared with spine-core's on
16
+ * every call with a model in hand (`VF13`). Since issue #1179's second part
17
+ * the poses come through that walk's scan assembler (`scanWalkSetup`,
18
+ * `scanLoopingWalk`): the same walk, each pose carrying what A10 reads — the
19
+ * bones' matrix and origin, the slot rows, the drawn attachments' slot, name
20
+ * and vertices — and none of the bones' getter readings and oracle rows or
21
+ * the drawn rows' UVs, triangles and colour, which nothing it reads is
22
+ * computed from. The core suite's `CO43` holds the scan to the public walk's
23
+ * numbers bit for bit, `CO44` what it leaves out to move none of them, and
24
+ * `CO45` that the vertex arrays it hands on uncopied are each pose's own.
25
+ * - **A bone's mode**: the mode the core's bone timelines pose at the time
26
+ * (`posedBones`, a fresh non-looping track: the time held at the duration),
27
+ * folded as the runtime's lookup folds it (`foldInheritMode`). `readModel`
28
+ * refuses a setup `inheritMode` and an `inherit` key that fold to no mode by
29
+ * name, so on this side every bone of every pose holds one and the answer is
30
+ * `null` — derived from the pose rather than assumed, so a reader that
31
+ * started admitting such a spelling would show up here as a bone with none.
32
+ * - **Under no skin set**, as `validate()` poses (`noSkinView`, the view
33
+ * measured in issue #1051: no skin's bones or constraints applied, a slot
34
+ * looked up in the default skin alone).
35
+ *
36
+ * Links nothing from the runtime.
37
+ */
38
+ import { fileAnimationOrder } from '../../compile.ts';
39
+ import { foldInheritMode, type CompiledDocument } from '../../core/index.ts';
40
+ import { posedBones } from '../../core/animation.ts';
41
+ import { scanLoopingWalk, scanWalkSetup } from '../../core/walk.ts';
42
+ import type { ScanPose } from '../../core/raw.ts';
43
+ import { noSkinView } from '../../render_core.ts';
44
+ import type { ModelBone } from '../../model.ts';
45
+ import type { SteppedFrame, SteppedPoseFacts } from '../facts/stepped_poses.ts';
46
+ import type { ReadDocument } from './parse.ts';
47
+
48
+ /** A bone's posed mode as the facts spell it: `null` for a mode, else the value held. */
49
+ const modeOf = (bone: ModelBone): string | null => (foldInheritMode(bone.inheritMode ?? 'normal') === null ? String(bone.inheritMode) : null);
50
+
51
+ /** A channel the walk computed; a slot row holds `null` only where a rounding wrote one, which the walk does not. */
52
+ const channel = (v: number | null): number => (v === null ? Number.NaN : v);
53
+
54
+ /** Each bone's posed mode as the facts spell it (`modeOf`), by name. */
55
+ const modesOf = (posed: readonly ModelBone[]): ReadonlyMap<string, string | null> => new Map(posed.map((b) => [b.name, modeOf(b)]));
56
+
57
+ /** One scan pose as A10 reads it, each bone's mode taken from `modes` (`modesOf` the bones the timelines posed at the pose's time); the drawn attachments are the scan's own rows, already in the shape A10 reads. */
58
+ function frameOf(pose: ScanPose, modes: ReadonlyMap<string, string | null>): SteppedFrame {
59
+ return {
60
+ bones: pose.bones.map((b) => ({ name: b.name, a: b.a, b: b.b, c: b.c, d: b.d, worldX: b.worldX, worldY: b.worldY, inherit: modes.get(b.name) ?? null })),
61
+ drawn: pose.drawn,
62
+ slots: pose.slots.map((row) => ({
63
+ name: row[0],
64
+ colour: [channel(row[2]), channel(row[3]), channel(row[4]), channel(row[5])] as const,
65
+ dark: row[6] === null ? null : ([channel(row[6][0]), channel(row[6][1]), channel(row[6][2])] as const),
66
+ })),
67
+ };
68
+ }
69
+
70
+ export function modelSteppedPoses(read: ReadDocument): SteppedPoseFacts {
71
+ let view: CompiledDocument | null = null;
72
+ const viewOf = (): CompiledDocument => (view ??= noSkinView(read.doc));
73
+ const animationOf = (name: string) => {
74
+ const anim = viewOf().animations.find((a) => a.name === name);
75
+ if (anim === undefined) throw new Error(`internal: the model side was asked to pose animation "${name}", which the document does not hold`);
76
+ return anim;
77
+ };
78
+ return {
79
+ boneCount: read.doc.bones.length,
80
+ steppedAnimations: fileAnimationOrder(read.doc).flatMap((name) => {
81
+ const anim = read.doc.animations.find((a) => a.name === name);
82
+ return anim === undefined ? [] : [{ name, duration: anim.timelines.duration }];
83
+ }),
84
+ hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
85
+ hasBone: (name) => read.doc.bones.some((b) => b.name === name),
86
+ posedInherit: (animation, bone, time) => {
87
+ const anim = animationOf(animation);
88
+ const t = time < anim.timelines.duration ? time : anim.timelines.duration;
89
+ const posed = posedBones(viewOf(), anim.timelines, t).find((b) => b.name === bone);
90
+ return posed === undefined ? undefined : modeOf(posed);
91
+ },
92
+ // The document's stated mode — asked only of a bone posing none, which the reader leaves this side no document to hold.
93
+ statedInherit: (name) => `${JSON.stringify(read.doc.bones.find((b) => b.name === name)?.inheritMode)}`,
94
+ setup: () => frameOf(scanWalkSetup(viewOf()), modesOf(viewOf().bones)),
95
+ walk: (animation, step, frames) => {
96
+ const anim = animationOf(animation);
97
+ const poses = scanLoopingWalk(viewOf(), animation, new Array<number>(frames).fill(step));
98
+ // A bone's posed mode moves only by an `inherit` timeline: every other one `posedBones` reads leaves the setup's mode on its copy. So an animation
99
+ // keying none poses the setup's modes at every time, and they are read once rather than posed again at each of its frames (issue #1179).
100
+ const keysInherit = anim.timelines.bones.some((target) => target.timelines.some((tl) => tl.kind === 'inherit'));
101
+ const setupModes = keysInherit ? null : modesOf(viewOf().bones);
102
+ return poses.slice(1).map((pose) => frameOf(pose, setupModes ?? modesOf(posedBones(viewOf(), anim.timelines, pose.animationTime))));
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The model side's supply of `TwoColourFacts` (issue #1025, cut 4c-3 of step
3
+ * 4c of #380).
4
+ *
5
+ * - **The stated dark colours**: the document's slots in their order — the
6
+ * emitted slot array's — each `dark` as the document holds it, which the
7
+ * emitter writes into the slot unchanged (`emitSlot`). `readModel` refuses
8
+ * a `dark` that is not six or eight hex digits, so a spelling the runtime
9
+ * would load as NaN or drop has no document to arrive in.
10
+ * - **The loaded dark colour**: the core's reading of that hex (`readColour`,
11
+ * its first three channels) — the reading the setup pose's slot rows carry,
12
+ * which `tools/pose_oracle.ts compare` holds equal to spine-core's on its
13
+ * `setup.slots` block, on the grid and under `--raw`.
14
+ * - **The slot timelines**: `fileSlotTimelines` (`./slot_colour.ts`), the walk
15
+ * A45 reads — the file's order and spelling, through the emitter's own
16
+ * passes.
17
+ * - **A slot's posed colours**: the core's raw entry (`poseRawAnimation`,
18
+ * `src/core/raw.ts`) taking one step of the time on a fresh track, under the
19
+ * view the core poses with no skin set (`noSkinView`), as A45's posed colour
20
+ * is taken — the slot row's light channels and its dark colour, NaN where the
21
+ * row reads no number. No gate samples the times A43 asks for (a key's own
22
+ * time), so the selftest compares these values with spine-core's at every
23
+ * time the body asks, on every call with a model in hand (`VF12`).
24
+ *
25
+ * Links nothing from the runtime.
26
+ */
27
+ import { readColour, type CompiledDocument } from '../../core/index.ts';
28
+ import { poseRawAnimation } from '../../core/raw.ts';
29
+ import { noSkinView } from '../../render_core.ts';
30
+ import type { DarkColour, TwoColourFacts } from '../facts/two_colour.ts';
31
+ import type { ReadDocument } from './parse.ts';
32
+ import { fileSlotTimelines } from './slot_colour.ts';
33
+
34
+ /** A channel the pose read as no number, as a non-finite channel reads on the runtime's side. */
35
+ const channel = (v: number | null): number => (v === null ? Number.NaN : v);
36
+
37
+ export function modelTwoColour(read: ReadDocument): TwoColourFacts {
38
+ let view: CompiledDocument | null = null;
39
+ const slotDarks = read.doc.slots.flatMap((slot) => (typeof slot.dark === 'string' ? [{ slot: slot.name, dark: slot.dark }] : []));
40
+ return {
41
+ slotDarks,
42
+ loadedDark: (name): DarkColour | null => {
43
+ const dark = read.doc.slots.find((s) => s.name === name)?.dark;
44
+ if (dark === undefined) return null;
45
+ const [r, g, b] = readColour(dark);
46
+ return { r, g, b };
47
+ },
48
+ slotTimelines: fileSlotTimelines(read),
49
+ hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
50
+ posedTint: (animation, slot, time) => {
51
+ view ??= noSkinView(read.doc);
52
+ const row = poseRawAnimation(view, animation, [time], {}, 'animation')[1].slots.find((r) => r[0] === slot);
53
+ if (row === undefined) return undefined;
54
+ const dark = row[6];
55
+ return {
56
+ light: { r: channel(row[2]), g: channel(row[3]), b: channel(row[4]), a: channel(row[5]) },
57
+ dark: dark === null ? null : { r: channel(dark[0]), g: channel(dark[1]), b: channel(dark[2]) },
58
+ };
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The model side's supply of `PolygonFacts` (issue #1025, step 4c of #380):
3
+ * the document's bounding boxes, clipping attachments and paths, and the
4
+ * clip ends it states.
5
+ *
6
+ * - **The polygons** in the loaded walk's order, `fileOrderedEntries`
7
+ * (`./skin_entries.ts`). A polygon's `worldVerticesLength` is twice the
8
+ * `vertexCount` the record states — the number the emitter copies into the
9
+ * file, which the parser doubles into the length it reads the vertices to
10
+ * (`readVertices`); the selftest compares it with spine-core's on every
11
+ * call. A path's `closed` and `lengths` are the core's reading of the record
12
+ * (`readGeometry`: `closed` absent reads the parser's `false`).
13
+ * - **The clip ends** in the order the file spells them: the skins and each
14
+ * skin's slot keys in the order `fileSkinOrder` gives (`src/compile.ts`,
15
+ * issue #1034: a `rigc-compiled/3` document's `editorOrder`, else the
16
+ * emitter's `editorSkinOrder` and `editorSlotKeyOrder`, the functions
17
+ * `emitSkins` is handed), each slot's records in table order — every clipping record that
18
+ * states an `end`, with the `end` as it states it.
19
+ *
20
+ * `encoding` is empty on every polygon: the document states the weighted
21
+ * form outright, so a run's decode has no subject here.
22
+ *
23
+ * Links nothing from the runtime.
24
+ */
25
+ import { fileSkinOrder, skinsInFileOrder } from '../../compile.ts';
26
+ import { shownRow } from '../../core/index.ts';
27
+ import type { ClipEnd, PolygonEntry, PolygonFacts } from '../facts/vertex_polygons.ts';
28
+ import { isObj, type Json } from '../values.ts';
29
+ import { fileOrderedEntries } from './skin_entries.ts';
30
+ import type { ReadDocument } from './parse.ts';
31
+
32
+ /** The document's own record of one skin entry, as the JSON spells it. */
33
+ export function documentRecord(read: ReadDocument, skin: string, slot: string, placeholder: string): Json {
34
+ const skins = Array.isArray(read.json.skins) ? read.json.skins.filter(isObj) : [];
35
+ const table = skins.find((s) => s.name === skin)?.attachments;
36
+ const record = isObj(table) && isObj(table[slot]) ? (table[slot] as Json)[placeholder] : undefined;
37
+ if (!isObj(record)) throw new Error(`internal: skin "${skin}" slot "${slot}" placeholder "${placeholder}" is a record the reader returned and the document does not spell`);
38
+ return record;
39
+ }
40
+
41
+ /** Every record of the document in the order the Spine file spells it: skins, then each skin's slot keys, then the slot's table. */
42
+ export function fileSpelledEntries(read: ReadDocument): Array<{ skin: string; slot: string; placeholder: string; record: Json }> {
43
+ const out: Array<{ skin: string; slot: string; placeholder: string; record: Json }> = [];
44
+ for (const { skin, slots } of skinsInFileOrder(read.doc.skins, fileSkinOrder(read.doc))) {
45
+ for (const slot of slots) {
46
+ for (const placeholder of Object.keys(skin.attachments[slot] ?? {})) out.push({ skin: skin.name, slot, placeholder, record: documentRecord(read, skin.name, slot, placeholder) });
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+
52
+ const WHAT: Readonly<Record<string, string>> = { boundingbox: 'bounding box', clipping: 'clipping attachment', path: 'path' };
53
+
54
+ export function modelPolygonFacts(read: ReadDocument): PolygonFacts {
55
+ const polygons: PolygonEntry[] = [];
56
+ for (const entry of fileOrderedEntries(read)) {
57
+ const g = entry.record.geometry;
58
+ if (g === undefined || (g.kind !== 'boundingbox' && g.kind !== 'clipping' && g.kind !== 'path')) continue;
59
+ const stated = documentRecord(read, entry.skin, entry.slot, entry.placeholder).vertexCount;
60
+ polygons.push({
61
+ what: `${WHAT[g.kind]} "${shownRow(entry).name}"`,
62
+ worldVerticesLength: typeof stated === 'number' ? stated * 2 : Number.NaN,
63
+ path: g.kind === 'path' ? { closed: g.closed, lengths: g.lengths } : null,
64
+ encoding: [],
65
+ });
66
+ }
67
+ const clipEnds: ClipEnd[] = [];
68
+ for (const { slot, placeholder, record } of fileSpelledEntries(read)) {
69
+ if (record.kind === 'clipping' && record.end !== undefined) clipEnds.push({ placeholder, slot, end: record.end });
70
+ }
71
+ return { polygons, clipEnds, slots: read.doc.slots.map((s) => s.name) };
72
+ }