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
package/src/preview.ts ADDED
@@ -0,0 +1,434 @@
1
+ /**
2
+ * The single-file preview — a compiled artifact playing in Esoteric Software's
3
+ * own web player, with nothing beside it.
4
+ *
5
+ * ⭐ Why this exists at all. `validate` answers "is this valid Spine", `check`
6
+ * answers "does it match these frames", and neither of them can answer the
7
+ * question a first user actually has: *does it look right?* A rig whose head sits
8
+ * visibly off its torso compiles green, loads in `spine-core` and steps
9
+ * numerically clean — the offsets are the ones the spec asked for. The only
10
+ * remedy for that class of error is looking, and until this command the package
11
+ * offered no way to look (issue #216).
12
+ *
13
+ * ## Why the official player, and not our own renderer
14
+ *
15
+ * There is a rasteriser in [`render.ts`](render.ts) already, and `rigc render`
16
+ * uses it. This is deliberately the other thing: `SpinePlayer` is the runtime
17
+ * Esoteric ships, so a rig that plays here has been played by the reference
18
+ * implementation rather than by ours. Every picture our own code draws is, at
19
+ * some level, rigc checking its own work — this one is not, which is why issue
20
+ * #151 settled on it as the surface a human votes on.
21
+ *
22
+ * ## Why one file, and what `rawDataURIs` is doing
23
+ *
24
+ * `SpinePlayer` normally fetches its skeleton, atlas and pages over HTTP, which
25
+ * would make a preview a directory plus a web server — a thing to set up rather
26
+ * than a thing to open. Its `rawDataURIs` option maps each of those paths to a
27
+ * `data:` URI instead, and the player's downloader takes the mapping in
28
+ * preference to the network. So the whole artifact — skeleton JSON, atlas text
29
+ * and every page's PNG bytes — is embedded, and the result is one `.html` file
30
+ * that plays when double-clicked and can be attached to a message.
31
+ *
32
+ * ⚠️ The keys are the paths the player asks for, and they must match exactly.
33
+ * `config.atlas` has no directory part, so the player's own texture resolution
34
+ * (`parentPath + pageName`) asks for each page under **the name the atlas spells**
35
+ * — `../parts/torso.png` and all. That is why the atlas text is embedded verbatim
36
+ * rather than rewritten to flat names: the file that plays is the file that was
37
+ * built, which is the entire value of the interop proof.
38
+ *
39
+ * ## The player is referenced, never vendored
40
+ *
41
+ * The `<script>` and `<link>` point at unpkg. Nothing Esoteric owns is copied
42
+ * into this repository or into the published package — see [NOTICE.md](../NOTICE.md)
43
+ * — and the generated page belongs to whoever ran the command. The cost is that
44
+ * the file needs the network the first time it is opened, which the page says out
45
+ * loud when the script does not arrive.
46
+ */
47
+ import { SPINE_VERSION } from './compile.ts';
48
+ import { BACKGROUND } from './render_shared.ts';
49
+
50
+ /**
51
+ * The Spine Web Player line the generated page loads.
52
+ *
53
+ * Derived from the runtime line the compiler emits rather than written down
54
+ * twice: a page playing 4.2 data in a 4.3 player (or the reverse) is a failure
55
+ * mode nobody would look for, and this makes the two impossible to edit apart.
56
+ *
57
+ * The **patch** is deliberately a wildcard where `SPINE_VERSION` is exact. Data
58
+ * compatibility is a property of the minor line, and pinning a patch would break
59
+ * every generated page on the day spine-core ships a version the player did not.
60
+ */
61
+ export const PLAYER_LINE = `${SPINE_VERSION.split('.').slice(0, 2).join('.')}.*`;
62
+ export const PLAYER_SCRIPT_URL = `https://unpkg.com/@esotericsoftware/spine-player@${PLAYER_LINE}/dist/iife/spine-player.js`;
63
+ export const PLAYER_STYLE_URL = `https://unpkg.com/@esotericsoftware/spine-player@${PLAYER_LINE}/dist/spine-player.css`;
64
+
65
+ /** The name the embedded skeleton and atlas are asked for under, inside the page. */
66
+ export const SKELETON_KEY = 'skeleton.json';
67
+ export const ATLAS_KEY = 'skeleton.atlas';
68
+
69
+ /** One atlas page, named exactly as the atlas spells it, with its bytes. */
70
+ export interface PreviewPage {
71
+ /** The page name from the atlas text — a path, quite possibly a relative one. */
72
+ name: string;
73
+ bytes: Uint8Array;
74
+ }
75
+
76
+ export interface PreviewInput {
77
+ skeletonText: string;
78
+ atlasText: string;
79
+ pages: PreviewPage[];
80
+ /** The animation to autoplay and loop, or `null` for a skeleton with none. */
81
+ animation: string | null;
82
+ /** Every animation the page offers in its picker. */
83
+ animations: string[];
84
+ /** What the page calls itself — the skeleton's path, for the tab and the header. */
85
+ label: string;
86
+ /** rigc's own version, for the generated-by line and the header's gate line. */
87
+ version: string;
88
+ /** What the gate said about this candidate on THIS run — see `PreviewGate`. */
89
+ gate: PreviewGate;
90
+ }
91
+
92
+ /**
93
+ * The gate's reading of a candidate, taken when the page is written (issue #837).
94
+ *
95
+ * ⭐ Both strings are the validator's own lines, never a paraphrase: `summary`
96
+ * is the `N assertions: …` line `rigc validate <dir>` prints last and `refusal`
97
+ * is its first `FAIL` line, each without the report's gutter. The caller
98
+ * measures them on the run that writes the page, because a figure carried over
99
+ * from the build would be a claim about bytes nothing here read — and a bare
100
+ * directory re-gated is NOT what `build` gated: with no rig spec and no second
101
+ * compile, `A09` and `A18` report SKIP there, so the two runs print different
102
+ * counts over the same files and only this one's belongs on this page.
103
+ *
104
+ * `refusal` is `null` on green. A refused candidate is still previewed —
105
+ * looking at a red build is what this command is for — and its header says so
106
+ * in the gate's words.
107
+ */
108
+ export interface PreviewGate {
109
+ summary: string;
110
+ refusal: string | null;
111
+ }
112
+
113
+ /** The header's gate line, as one element: which rigc, what ran, and what it said. */
114
+ export function gateLine(gate: PreviewGate, version: string): string {
115
+ const said =
116
+ `rigc ${escapeHtml(version)}, re-gated as <code>rigc validate &lt;dir&gt;</code> gates it: ${escapeHtml(gate.summary)}`;
117
+ return gate.refusal === null
118
+ ? `<span class="rigc-gate" data-gate="green">${said}</span>`
119
+ : `<span class="rigc-gate" data-gate="refused" style="opacity: 1; color: #7d1d1d; font-weight: 600">${said} — ` +
120
+ `refused: ${escapeHtml(gate.refusal)}</span>`;
121
+ }
122
+
123
+ /** What a header says its player is playing. */
124
+ function playedText(animation: string | null, animations: string[]): string {
125
+ return animation === null
126
+ ? 'no animation — the setup pose'
127
+ : `${escapeHtml(animation)}${animations.length > 1 ? ` (of ${animations.length}; pick another in the controls)` : ''}`;
128
+ }
129
+
130
+ /** The header over one player: the path, what it plays, and the gate's line. */
131
+ function headerHtml(input: PreviewInput): string {
132
+ return (
133
+ `<header><b>${escapeHtml(input.label)}</b> <span>— ${playedText(input.animation, input.animations)}</span>` +
134
+ `<br>${gateLine(input.gate, input.version)}</header>`
135
+ );
136
+ }
137
+
138
+ /**
139
+ * One player's config. `animation` is left off a skeleton with none rather than
140
+ * set to null: the player checks for the key's presence, and the setup pose
141
+ * held still is the honest picture of a rig that has no animation to play.
142
+ */
143
+ function previewConfig(input: PreviewInput): Record<string, unknown> {
144
+ const rawDataURIs: Record<string, string> = {
145
+ [SKELETON_KEY]: dataUri('application/json', input.skeletonText),
146
+ [ATLAS_KEY]: dataUri('text/plain', input.atlasText),
147
+ };
148
+ for (const page of input.pages) rawDataURIs[page.name] = dataUri('image/png', page.bytes);
149
+ const config: Record<string, unknown> = {
150
+ skeleton: SKELETON_KEY,
151
+ atlas: ATLAS_KEY,
152
+ rawDataURIs,
153
+ animations: input.animations,
154
+ showControls: true,
155
+ alpha: false,
156
+ backgroundColor: backgroundHex(),
157
+ };
158
+ if (input.animation !== null) config.animation = input.animation;
159
+ return config;
160
+ }
161
+
162
+ /**
163
+ * The licence paragraph, spelled once for the one-candidate page and the page
164
+ * of panes so the two cannot drift. The ballot keeps its own copy of the same
165
+ * words: its bytes are held by `vote`'s controls, not moved by this card.
166
+ */
167
+ const PLAYER_LICENCE = ` It plays them in the Spine Web Player, which is NOT embedded: the script and
168
+ stylesheet below are loaded from unpkg. The Spine Runtimes are Copyright (c)
169
+ 2013-2025 Esoteric Software LLC and are licensed under the Spine Runtimes
170
+ License Agreement — https://esotericsoftware.com/spine-runtimes-license — which
171
+ requires each user of a product integrating them to hold a Spine Editor
172
+ license. Nothing owned by Esoteric Software is redistributed by rigc.`;
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // panes — the layout `vote` built first, shared rather than copied
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /**
179
+ * The grid panes sit in, as the style lines the ballot has always carried: one
180
+ * column per pane, one column on a narrow screen. `buildBallot` calls this too,
181
+ * so a ballot's bytes are what they were and the two pages cannot drift.
182
+ */
183
+ export function paneGridCss(count: number): string {
184
+ return ` #panes { display: grid; grid-template-columns: repeat(${count}, minmax(0, 1fr)); gap: 1px; background: rgba(0, 0, 0, 0.15); }
185
+ @media (max-width: 720px) { #panes { grid-template-columns: minmax(0, 1fr); } }
186
+ .pane { background: ${backgroundHex()}; display: flex; flex-direction: column; min-width: 0; }`;
187
+ }
188
+
189
+ /** The box a pane's player mounts in, as the ballot styles it. */
190
+ export const PANE_STAGE_CSS = ' .stage { height: 52vh; min-height: 260px; }';
191
+
192
+ /** One pane: a heading the caller writes, over the element its player mounts in. */
193
+ export function paneSection(stageId: string, heading: string): string {
194
+ return `<section class="pane">
195
+ ${heading}
196
+ <div class="stage" id="${stageId}"></div>
197
+ </section>`;
198
+ }
199
+
200
+ /** The element pane `i` (0-based) mounts its player in, numbered from 1 as the CLI numbers candidates. */
201
+ export function previewPaneId(i: number): string {
202
+ return `rigc-player-${i + 1}`;
203
+ }
204
+
205
+ /**
206
+ * A `data:` URI the player's downloader will take the fast path on.
207
+ *
208
+ * ⚠️ Base64 for the text assets too, not only for the PNGs. The downloader
209
+ * decides a value is a data URI rather than an alias by asking whether it
210
+ * contains a `.` — and a percent-encoded skeleton JSON is full of them, so the
211
+ * un-encoded form would be handed to `XMLHttpRequest` as a URL instead. Base64's
212
+ * alphabet has no `.` in it, so this always lands on the branch that decodes.
213
+ */
214
+ export function dataUri(mime: string, body: string | Uint8Array): string {
215
+ const base64 = (typeof body === 'string' ? Buffer.from(body, 'utf8') : Buffer.from(body)).toString('base64');
216
+ return `data:${mime};base64,${base64}`;
217
+ }
218
+
219
+ /** `#rrggbb` for the player's background, so a preview and `rigc render` agree. */
220
+ export function backgroundHex(): string {
221
+ return `#${BACKGROUND.slice(0, 3)
222
+ .map((c) => c.toString(16).padStart(2, '0'))
223
+ .join('')}`;
224
+ }
225
+
226
+ /** The five characters that can end an element or an attribute early. */
227
+ export function escapeHtml(text: string): string {
228
+ return text
229
+ .replace(/&/g, '&amp;')
230
+ .replace(/</g, '&lt;')
231
+ .replace(/>/g, '&gt;')
232
+ .replace(/"/g, '&quot;')
233
+ .replace(/'/g, '&#39;');
234
+ }
235
+
236
+ /**
237
+ * JSON for embedding inside a `<script>` element.
238
+ *
239
+ * The escape is on `<` alone and it is enough: an HTML parser ends a script at
240
+ * `</script`, and it cannot see one if no `<` survives. `<` is the same
241
+ * string to a JSON reader, so nothing about the value changes. Page names come
242
+ * out of a file somebody else wrote, which is exactly why this is not optional.
243
+ */
244
+ export function embeddedJson(value: unknown): string {
245
+ return JSON.stringify(value).replace(/</g, '\\u003c');
246
+ }
247
+
248
+ /**
249
+ * The whole page, as text.
250
+ *
251
+ * There is no template file and no build step: this is a `.ts` string, so the
252
+ * published package carries it the way it carries every other module, and the
253
+ * page it produces has no dependency of its own except the player URL above.
254
+ */
255
+ export function buildPreview(input: PreviewInput): string {
256
+ const config = previewConfig(input);
257
+ const label = escapeHtml(input.label);
258
+
259
+ return `<!doctype html>
260
+ <html lang="en">
261
+ <head>
262
+ <meta charset="utf-8">
263
+ <meta name="viewport" content="width=device-width, initial-scale=1">
264
+ <title>rigc preview — ${label}</title>
265
+ <!--
266
+ Generated by rigc ${escapeHtml(input.version)} — https://github.com/firejune/rigc
267
+
268
+ The skeleton, the atlas and every atlas page are embedded in this file as data
269
+ URIs, so it plays on its own with no server and no sibling files.
270
+
271
+ ${PLAYER_LICENCE}
272
+ -->
273
+ <link rel="stylesheet" href="${PLAYER_STYLE_URL}">
274
+ <style>
275
+ :root { color-scheme: light dark; }
276
+ html, body { margin: 0; height: 100%; }
277
+ body {
278
+ display: flex; flex-direction: column;
279
+ background: ${backgroundHex()};
280
+ color: #1a1a1a;
281
+ font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
282
+ }
283
+ header { padding: 10px 14px; border-bottom: 1px solid rgba(0, 0, 0, 0.15); }
284
+ header b { font-weight: 600; }
285
+ header span { opacity: 0.65; }
286
+ #rigc-player { flex: 1 1 auto; min-height: 0; }
287
+ #rigc-status { margin: 0; padding: 10px 14px; border-top: 1px solid rgba(0, 0, 0, 0.15); white-space: pre-wrap; }
288
+ #rigc-status[data-state="error"] { background: #7d1d1d; color: #fff; }
289
+ </style>
290
+ </head>
291
+ <body>
292
+ ${headerHtml(input)}
293
+ <div id="rigc-player"></div>
294
+ <p id="rigc-status">loading the Spine Web Player…</p>
295
+ <script src="${PLAYER_SCRIPT_URL}"></script>
296
+ <script>
297
+ (function () {
298
+ var status = document.getElementById('rigc-status');
299
+ var state = { status: 'loading', message: null, player: null };
300
+ window.rigcPreview = state;
301
+ function say(kind, message) {
302
+ state.status = kind;
303
+ state.message = message;
304
+ status.textContent = message;
305
+ status.setAttribute('data-state', kind);
306
+ }
307
+ if (typeof window.spine === 'undefined' || typeof window.spine.SpinePlayer !== 'function') {
308
+ say('error', 'The Spine Web Player did not load from ${PLAYER_SCRIPT_URL} — this page needs a network connection the first time it is opened.');
309
+ return;
310
+ }
311
+ var config = ${embeddedJson(config)};
312
+ config.success = function (player) {
313
+ state.player = player;
314
+ say('ready', 'playing in Spine Web Player ${PLAYER_LINE} — everything it is drawing is embedded in this file.');
315
+ };
316
+ config.error = function (player, message) {
317
+ state.player = player;
318
+ say('error', String(message));
319
+ };
320
+ try {
321
+ new window.spine.SpinePlayer('rigc-player', config);
322
+ } catch (err) {
323
+ say('error', String(err && err.message ? err.message : err));
324
+ }
325
+ })();
326
+ </script>
327
+ </body>
328
+ </html>
329
+ `;
330
+ }
331
+
332
+ /**
333
+ * Several candidates on one page, a pane per candidate in the order given
334
+ * (issue #837).
335
+ *
336
+ * ⭐ This page asks nothing, and that is the whole difference from the ballot
337
+ * whose layout it borrows. A ballot hides where each candidate came from
338
+ * because a voter who can see it is no longer comparing pictures; here the
339
+ * question is "show me these", so each pane is headed exactly as a
340
+ * one-candidate preview is — its path, what it plays, the gate's line for it —
341
+ * and each player offers every animation its own skeleton has. No manifest, no
342
+ * digests and no result form: there is no answer to record.
343
+ *
344
+ * Each pane plays its own candidate's animation (the one `--animation` names,
345
+ * or its own first). The ballot refuses two panes playing two animations
346
+ * because the labels are A and B and nothing would say so; here the header over
347
+ * each pane names what it plays.
348
+ */
349
+ export function buildPreviewPanes(inputs: PreviewInput[], version: string): string {
350
+ const configs = inputs.map((input) => previewConfig(input));
351
+ const panes = inputs.map((input, i) => paneSection(previewPaneId(i), headerHtml(input))).join('\n');
352
+
353
+ return `<!doctype html>
354
+ <html lang="en">
355
+ <head>
356
+ <meta charset="utf-8">
357
+ <meta name="viewport" content="width=device-width, initial-scale=1">
358
+ <title>rigc preview — ${inputs.length} candidates</title>
359
+ <!--
360
+ Generated by rigc ${escapeHtml(version)} — https://github.com/firejune/rigc
361
+
362
+ Every candidate's skeleton, atlas and atlas pages are embedded in this file as
363
+ data URIs, so it plays on its own with no server and no sibling files.
364
+
365
+ ${PLAYER_LICENCE}
366
+ -->
367
+ <link rel="stylesheet" href="${PLAYER_STYLE_URL}">
368
+ <style>
369
+ :root { color-scheme: light dark; }
370
+ html, body { margin: 0; min-height: 100%; }
371
+ body {
372
+ background: ${backgroundHex()};
373
+ color: #1a1a1a;
374
+ font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
375
+ }
376
+ header { padding: 10px 14px; border-bottom: 1px solid rgba(0, 0, 0, 0.15); overflow-wrap: anywhere; }
377
+ header b { font-weight: 600; }
378
+ header span { opacity: 0.65; }
379
+ ${paneGridCss(inputs.length)}
380
+ ${PANE_STAGE_CSS}
381
+ #rigc-status { margin: 0; padding: 10px 14px; border-top: 1px solid rgba(0, 0, 0, 0.15); white-space: pre-wrap; }
382
+ #rigc-status[data-state="error"] { background: #7d1d1d; color: #fff; }
383
+ </style>
384
+ </head>
385
+ <body>
386
+ <div id="panes">
387
+ ${panes}
388
+ </div>
389
+ <p id="rigc-status">loading the Spine Web Player…</p>
390
+ <script src="${PLAYER_SCRIPT_URL}"></script>
391
+ <script>
392
+ (function () {
393
+ var configs = ${embeddedJson(configs)};
394
+ var status = document.getElementById('rigc-status');
395
+ var state = { status: 'loading', message: null, players: [], ready: [], failed: [] };
396
+ window.rigcPreview = state;
397
+ function say(kind, message) {
398
+ state.status = kind;
399
+ state.message = message;
400
+ status.textContent = message;
401
+ status.setAttribute('data-state', kind);
402
+ }
403
+ if (typeof window.spine === 'undefined' || typeof window.spine.SpinePlayer !== 'function') {
404
+ say('error', 'The Spine Web Player did not load from ${PLAYER_SCRIPT_URL} — this page needs a network connection the first time it is opened.');
405
+ return;
406
+ }
407
+ for (var i = 0; i < configs.length; i++) {
408
+ (function (pane, config) {
409
+ config.success = function (player) {
410
+ state.players[pane - 1] = player;
411
+ if (state.ready.indexOf(pane) === -1) state.ready.push(pane);
412
+ if (state.failed.length === 0 && state.ready.length === configs.length) {
413
+ say('ready', 'all ' + configs.length + ' panes are playing in Spine Web Player ${PLAYER_LINE} — everything they draw is embedded in this file.');
414
+ }
415
+ };
416
+ config.error = function (player, message) {
417
+ state.players[pane - 1] = player;
418
+ state.failed.push('pane ' + pane + ': ' + String(message));
419
+ say('error', state.failed.join('\\n'));
420
+ };
421
+ try {
422
+ new window.spine.SpinePlayer('rigc-player-' + pane, config);
423
+ } catch (err) {
424
+ state.failed.push('pane ' + pane + ': ' + String(err && err.message ? err.message : err));
425
+ say('error', state.failed.join('\\n'));
426
+ }
427
+ })(i + 1, configs[i]);
428
+ }
429
+ })();
430
+ </script>
431
+ </body>
432
+ </html>
433
+ `;
434
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Every atlas-region lookup a skeleton's attachments ask for, read off its raw
3
+ * JSON (issue #589) — moved here unchanged from `./validate.ts` (issue #1052),
4
+ * which re-exports it.
5
+ *
6
+ * Its own module because two readers need it and only one of them links the
7
+ * runtime: `A08`'s runtime supplier in `./validate.ts`, and `explain`, which
8
+ * refuses art it cannot pose (`refuseUnposableArt`) and runs in an entry that
9
+ * links nothing of spine-core. The walk names no runtime class — it reads the
10
+ * file, and is measured against the runtime's loader by `PS127`.
11
+ */
12
+ import { attachmentRegionLookups, type AttachmentRegionJoin } from './assertions/region_lookups.ts';
13
+ import { isObj } from './assertions/values.ts';
14
+
15
+ /**
16
+ * Every atlas-region lookup `AtlasAttachmentLoader` will perform, read off the
17
+ * RAW skeleton JSON — before the loader is asked, which is the whole point.
18
+ *
19
+ * 🚨 This is a SECOND implementation of a join `spine-core` already performs,
20
+ * and the tree's standing judgment about a second opinion on somebody else's
21
+ * format is that it is measured rather than asserted: `PS127` runs the loader
22
+ * with its `findRegion` recording what it asked for, and compares. A wrong walk
23
+ * here would refuse correct foreign data by name, which is the one failure that
24
+ * would be worse than the silence #589 removed.
25
+ *
26
+ * Which entries resolve a region is the parser's list, not a guess:
27
+ * `SkeletonJson.readAttachment` (`dist/SkeletonJson.js:524-575`) calls the
28
+ * loader with a path for `region`, `mesh` and `linkedmesh` — a linked mesh
29
+ * resolves its own region before the `source` branch — and for nothing else.
30
+ * `type` defaults to `region` (`:527`), `name` to the placeholder (`:526`) and
31
+ * `path` to the name (`:529`, `:560`): three names that default into one
32
+ * another, which is why a report printing only the last of them cannot say
33
+ * what to change.
34
+ */
35
+ export function attachmentRegionJoins(raw: unknown): AttachmentRegionJoin[] {
36
+ const joins: AttachmentRegionJoin[] = [];
37
+ if (!isObj(raw) || !Array.isArray(raw.skins)) return joins;
38
+ for (const skin of raw.skins as unknown[]) {
39
+ if (!isObj(skin) || !isObj(skin.attachments)) continue;
40
+ const skinName = typeof skin.name === 'string' ? skin.name : '(unnamed)';
41
+ for (const [slot, entries] of Object.entries(skin.attachments)) {
42
+ if (!isObj(entries)) continue;
43
+ for (const [placeholder, entry] of Object.entries(entries)) {
44
+ if (!isObj(entry)) continue;
45
+ const type = entry.type === undefined ? 'region' : entry.type;
46
+ if (type !== 'region' && type !== 'mesh' && type !== 'linkedmesh') continue;
47
+ const name = typeof entry.name === 'string' ? entry.name : placeholder;
48
+ const path = typeof entry.path === 'string' ? entry.path : name;
49
+ joins.push({ skin: skinName, slot, placeholder, name, lookups: attachmentRegionLookups(entry.sequence, path) });
50
+ }
51
+ }
52
+ }
53
+ return joins;
54
+ }