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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- 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 <dir></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, '&')
|
|
230
|
+
.replace(/</g, '<')
|
|
231
|
+
.replace(/>/g, '>')
|
|
232
|
+
.replace(/"/g, '"')
|
|
233
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|