@kieransimkin/dance-rudiments 0.2.4 → 0.4.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.
@@ -0,0 +1,324 @@
1
+ # Optional rig motion packages
2
+
3
+ Rig recordings are independent data packages. They are **not default rudiments,
4
+ dependencies of the base package, or site-wide assets**. A character page opts
5
+ into one exact rig profile and loads selected recordings. Leaving the page can
6
+ release its network requests, cached recordings and native instances.
7
+
8
+ The existing 1,731-pattern catalogue is unchanged. Importing the Python authoring
9
+ package or TypeScript module does not read any optional motion package. NumPy is
10
+ an optional **offline BVH import** dependency; Blender is only required for the
11
+ Blender import/retarget bridge. Neither is a browser runtime dependency.
12
+
13
+ These APIs are development source until a matching stable release is published.
14
+
15
+ ## Contents and identity
16
+
17
+ An independent package directory contains:
18
+
19
+ ```text
20
+ manifest.json
21
+ profile.json
22
+ clips/<clip-id>.json.gz
23
+ README.md
24
+ LICENSE.txt
25
+ THIRD_PARTY_NOTICES.md
26
+ SOURCES.json
27
+ ```
28
+
29
+ The manifest has format `dancerudiments.motion-package`, schema version 1, and an
30
+ independently versioned package ID. It declares one exact rig profile, its
31
+ SHA-256, canonical semantic type URI, ordered channel names, licence/provenance,
32
+ and descriptors for individual clips. Every descriptor includes a SHA-256,
33
+ stored/decoded byte lengths, encoding, absolute `start_pip`, `sample_count`, and
34
+ optional finite metadata. Catalogue reads need only this manifest.
35
+
36
+ `profile.json` is canonical ASCII JSON without a trailing newline. Its file
37
+ SHA-256 equals the existing `RigProfile.fingerprint`; consumers hash the actual
38
+ bytes instead of reserializing floats differently in JavaScript. Clip files
39
+ contain the standard `dancerudiments.recording` JSON, optionally gzip compressed.
40
+ The shared profile is stored once per package rather than repeated in every
41
+ clip. There is no second binary recording schema or second live sampler.
42
+
43
+ Compatibility requires **all three** of the scene's type URI, profile hash and
44
+ ordered dimensions. A matching humanoid label or similar bone names is not an
45
+ adequate rig binding. Source BVH/FBX rigs use the separate
46
+ `syn://type/danceflow/motion/skeletal/source/...` namespace. They must not be
47
+ represented as Rigify control recordings until explicitly retargeted.
48
+
49
+ The manifest is limited to 8 MiB and 10,000 descriptors; each profile, stored clip
50
+ and decoded clip is limited to 64 MiB. Each clip also obeys the native maximum of
51
+ 8,388,608 scalar values and signed 32-bit pip bounds. Relative paths cannot
52
+ traverse directories, contain URLs, collide case-insensitively, or replace root
53
+ notices. Importers reject duplicate JSON keys, non-finite samples, incomplete
54
+ quaternions and invalid discrete selectors.
55
+
56
+ ## Explicit local installation and loading
57
+
58
+ ```sh
59
+ dancerudiments-motion-pack install robot-1.0.0.zip --root ./motion-packs --sha256 <archive-sha256>
60
+ dancerudiments-motion-pack list --root ./motion-packs
61
+ dancerudiments-motion-pack inspect ./motion-packs/robot/1.0.0
62
+ ```
63
+
64
+ From a source checkout, use `python tools/motion_package.py` in place of the
65
+ installed command. Installation verifies a local ZIP and atomically creates
66
+ `<root>/<id>/<version>`; it preserves existing versions. It neither downloads nor
67
+ executes code from a package. Archive traversal, symlinks, undeclared files,
68
+ duplicates and configured size violations fail before installation. Every
69
+ declared payload is hash-checked; full recording validation occurs on loading.
70
+
71
+ ```python
72
+ from dancerudiments_authoring import MotionPackage, load_profile
73
+
74
+ scene_profile = load_profile("my-character.profile.json")
75
+ with MotionPackage("motion-packs/my_character/1.0.0",
76
+ expected_profile=scene_profile) as package:
77
+ choices = package.catalogue() # metadata only
78
+ motion = package.load(choices[0]["id"])
79
+ player = motion.player() # existing native SampledRecording
80
+ physical_channels = player.by_channel(player.start_pip)
81
+ package.unload(motion.name)
82
+ ```
83
+
84
+ Python caches at most one clip by default; `cache_size=0` disables caching.
85
+ `close()` drops the package's references. Objects deliberately retained by the
86
+ caller remain that caller's responsibility. `discover_motion_packages(root)`
87
+ only searches the directory the caller explicitly supplies; no global install
88
+ scan or default-library registration runs automatically.
89
+
90
+ `MotionPackageBuilder` creates packages from validated `RigMotion` objects.
91
+ `add_bvh_chunk()` accepts the explicitly validated vectorized offline import
92
+ result. `export_motion_package()` writes a deterministic independent ZIP, and
93
+ `resume=True` only resumes a package with identical identity, profile, licence
94
+ and provenance. Existing clip IDs cannot silently be overwritten.
95
+
96
+ ```sh
97
+ dancerudiments-motion-pack create first.motion.json second.motion.json \
98
+ --output ./motion-packs/my_character/1.0.0 --id my_character \
99
+ --title "My character movements" --license CC0-1.0 \
100
+ --license-url https://creativecommons.org/publicdomain/zero/1.0/ \
101
+ --attribution "My own capture" --source-url https://example.org/my-capture \
102
+ --timing-kind music
103
+ dancerudiments-motion-pack export ./motion-packs/my_character/1.0.0 my-character.zip
104
+ ```
105
+
106
+ Use the real source's licence and timing status. These command arguments record
107
+ the caller's declarations; a converter cannot grant rights or establish beats.
108
+
109
+ ## Browser pages and the beat clock
110
+
111
+ The package exposes `@kieransimkin/dance-rudiments/motion-packages` separately.
112
+ Dynamic import it inside the character page's lifecycle. The base module also
113
+ exports the APIs for existing consumers, with no asset-fetch side effects.
114
+
115
+ ```js
116
+ const {createMotionPackageLoader, createRecordingWasmAdapter} =
117
+ await import('@kieransimkin/dance-rudiments/motion-packages');
118
+
119
+ const response = await fetch(characterPage.recordingWasmUrl);
120
+ if (!response.ok) throw new Error(`WASM HTTP ${response.status}`);
121
+ const nativeModule = await WebAssembly.compile(await response.arrayBuffer());
122
+ const motions = createMotionPackageLoader({
123
+ manifestUrl: characterPage.motionManifestUrl,
124
+ packageId: characterPage.motionPackageId,
125
+ expectedProfile: characterRig.target,
126
+ nativeFactory: createRecordingWasmAdapter(nativeModule),
127
+ });
128
+
129
+ const catalogue = await motions.catalogue();
130
+ const clip = await motions.load(catalogue.clips[0].id);
131
+ // hostPip is an absolute integer from the existing 64-pip beat transport.
132
+ if (hostPip >= clip.startPip && hostPip < clip.startPip + clip.sampleCount) {
133
+ characterRig.applyChannels(clip.profile, clip.sample(hostPip));
134
+ }
135
+ // On character/page teardown:
136
+ motions.dispose();
137
+ ```
138
+
139
+ `characterRig.target` must come from the actual saved rig. Copying an untrusted
140
+ manifest's identity into this field does not establish compatibility. Applying
141
+ channels requires the host's rig evaluator; Rigify constraints and IK require a
142
+ capable evaluator or an explicitly baked/deformation-rig export. This library
143
+ does not silently add a mesh renderer to the XYZ visualizer.
144
+
145
+ The raw native recording accessor is exposed at the `./recording-wasm` package
146
+ subpath. Configure its hosted URL using the consuming app's build system. It is
147
+ compiled from the existing C++ recording implementation and hash-checked against
148
+ its sources when packaged. It is not fetched or instantiated on module import.
149
+
150
+ `createMotionPackageLoader()` is inert until called. `catalogue()` loads metadata;
151
+ `load(id)` fetches the profile once and only the selected clip. Handles provide
152
+ `sampleNormalized(pip)` for native values and `sample(pip)` for decoded physical
153
+ channels. In-flight requests are deduplicated, failed requests may be retried,
154
+ and each caller's abort only cancels that caller's wait. When no waiters remain,
155
+ the request can be cancelled. `unload(id)` invalidates that clip and releases its
156
+ native instance; `dispose()` releases everything owned by that loader.
157
+
158
+ The general browser loader retains requested clips until explicitly unloaded.
159
+ The complete [page example](../examples/motion-packages/page-motion.mjs) maintains
160
+ **one current chunk**, cancels obsolete requests on seeking, ignores late
161
+ results, subscribes to the host clock and cleans up on route exit. Repeated
162
+ frame ticks reuse a pending chunk request. Failures require explicit retry,
163
+ avoiding a request loop on every animation frame. The host keeps the beat clock;
164
+ the example creates no independent clock or sampler.
165
+
166
+ For a static server, serve `.json.gz` assets as raw `application/gzip`, **without
167
+ `Content-Encoding: gzip`**. Otherwise HTTP auto-decompression changes the stored
168
+ bytes before integrity validation. JSON may use `application/json`; WASM may
169
+ use `application/wasm`. Cross-origin hosting also needs ordinary CORS permission.
170
+ No signed-in fetches are required by the package format.
171
+
172
+ ## Source timing, complete actions and chunks
173
+
174
+ Unknown music timing is represented explicitly as `timing_kind: "time-only"`.
175
+ Its 60 BPM clock is a coordinate in which one beat equals one second, sampled at
176
+ 64 pips per second. This value is **not an estimated musical tempo**. The original
177
+ frame time, FPS, frame count, duration and source hash remain in provenance.
178
+
179
+ To match a dance to music, supply source BPM **and** beat-zero phase, or use the
180
+ existing `BeatMap`/`compile_capture` path with measured variable-tempo beat
181
+ anchors. Tempo alone does not determine phase. Target tempo changes then follow
182
+ the same absolute native pip clock. Foot-contact correction, transition design
183
+ and automatic audio beat detection are separate authoring work.
184
+
185
+ Long actions are split at contiguous pip boundaries into chunks with at most
186
+ 500,000 values. All chunks of one action stay together in one package. Metadata
187
+ records `original_clip_id`, `chunk_index`, `chunk_count`, `total_sample_count`
188
+ and exact source-time spans. Counts of source actions and recording chunks are
189
+ reported separately; splitting does not create new distinct performances.
190
+
191
+ Compilation includes every integer pip inside the valid original time interval,
192
+ including negative prebeat pips when phase requires them. It never extends or
193
+ holds the endpoint. The sub-pip head/tail remainder is recorded. Recordings stay
194
+ non-looping; a caller must deliberately author any loop or transition.
195
+
196
+ ## Importing optional source collections
197
+
198
+ Install offline acceleration only when needed:
199
+
200
+ ```sh
201
+ python -m pip install '.[motion-import]'
202
+ python tools/import_motion_collection.py download --sources accad
203
+ python tools/import_motion_collection.py build --sources accad
204
+ ```
205
+
206
+ The explicit source plan is [`motion-packages/sources.json`](../motion-packages/sources.json).
207
+ It records original public URLs, licence/attribution, byte/checksum constraints,
208
+ coordinate policies and output groups. Downloads go to the ignored project
209
+ cache. Converted packages go to ignored `motion-packs/`. Neither is included in
210
+ the base npm package, Python wheel/sdist or native default catalogue. A download
211
+ is an explicit command, never an installation hook.
212
+
213
+ The same commands accept `--sources lifelike_agility style100` for the dog
214
+ capture and 100STYLE locomotion collections. These are general motion sources;
215
+ 100STYLE's 100 styles are not 100 dance genres. Source and converted data can
216
+ require several gigabytes, so select collections deliberately.
217
+
218
+ Space-XR publishes individual files with two coordinate conventions. Its
219
+ pinned acquisition recipe keeps their original bytes, performer names, titles,
220
+ aliases and hashes, then assembles two deterministic local source ZIPs:
221
+
222
+ ```sh
223
+ python tools/acquire_space_xr.py --download
224
+ python tools/import_motion_collection.py build --sources space_xr_yup space_xr_zup
225
+ ```
226
+
227
+ Omit `--download` to require already cached originals. The 17 distinct files
228
+ include nine dances/improvisations, seven Laban exercises and one walk/arm
229
+ movement; repeated listing links are not counted as additional performances.
230
+ The adapted data remains **CC BY-SA 4.0**, with the licence and attribution
231
+ inside each optional package. Source plan entries marked `local_archive` are
232
+ assembled by their acquisition recipe, not fetched from a fictitious ZIP URL.
233
+
234
+ The CMU dance recipe selects 89 dance-specific files from a pinned public mirror
235
+ of the documented cgspeed conversion. Exact deduplication yields 84 source
236
+ sequences, including modern dance, salsa, Charleston and other named dances:
237
+
238
+ ```sh
239
+ python tools/acquire_cmu_dance.py --download
240
+ python tools/import_motion_collection.py build --sources cmu_dance
241
+ ```
242
+
243
+ It retains the converter's calibration first frame and custom CMU terms,
244
+ including **no direct resale of the data**, even after conversion. These terms
245
+ are independent of the software licence. Per-file original URLs, Git blobs,
246
+ SHA-256 values, titles and timing evidence are preserved. The 30 salsa trials
247
+ use the explicitly evidenced 1/60-second correction described below; the other
248
+ files retain their declared frame intervals. Musical beats remain unverified.
249
+
250
+ The Quaternius robot recipe extracts all 14 Actions from the explicitly supplied
251
+ original CC0 archive, including Dance. Run it in the pinned Blender runtime:
252
+
253
+ ```sh
254
+ blender --background --factory-startup --python tools/import_quaternius_robot.py -- \
255
+ --archive /path/to/Animated-Robot-Oct-2018.zip \
256
+ --work-dir .cache/motion-sources/robot-rebuild \
257
+ --output-dir motion-packs/quaternius_robot/1.0.0 \
258
+ --package-archive optional-motion-packs/quaternius_robot-1.0.0.zip
259
+ ```
260
+
261
+ Use a new work/output directory. This recipe downloads nothing, preserves the
262
+ source rig and forms one exact profile for its Actions. The separately supplied
263
+ Blender demonstration retains the original skinned robot and bakes native
264
+ DanceRudiments samples; the base library contains no robot mesh or recordings.
265
+
266
+ Bulk import publishes a complete original action in one atomic manifest update.
267
+ A failed later chunk leaves the prior complete catalogue intact. `--resume`
268
+ requires the same package identity, profile, licence and source plan; interrupted
269
+ clip assets are reused only if regenerated bytes match exactly. A private build
270
+ checkpoint makes interruption before the first manifest recoverable and is
271
+ excluded from exported packages. A new version/output directory is required
272
+ when changing conversion settings or source identity.
273
+
274
+ The importer parses bounded BVH text, preserves the declared hierarchy and Euler
275
+ orders, converts local rotations to continuous unit quaternions, and resamples
276
+ offline with linear translation and shortest-path SLERP. It reports malformed
277
+ files and deduplicates exact source-byte duplicates. Source FPS is read from
278
+ each file, not guessed from the collection name.
279
+
280
+ An explicitly reviewed per-file `frame_time_override` can correct a known bad
281
+ header. `override_bvh_frame_time(clip, frame_time_seconds="1/60", evidence={...})`
282
+ requires nonempty source evidence and retains the original byte hash, declared
283
+ header interval/FPS/duration, pose arrays and profile. Effective timing then
284
+ drives compilation, with the correction recorded in every chunk. It does not
285
+ infer or replace musical tempo. The CMU conversion is one concrete use: its
286
+ converter rewrote headers uniformly, while some original trials ran at 60 fps.
287
+
288
+ BVH does not declare physical units. Scale 1 preserves original numerical units
289
+ and is labelled `source-units-unverified`; an explicit caller scale is recorded.
290
+ Axis conventions and fixed location bounds are part of the profile identity.
291
+ The importer supports both additive BVH positions and Blender's absolute-local
292
+ position convention. Sources with position channels equal to their rest offsets
293
+ must use the latter to avoid doubled bone lengths. This is an explicit source
294
+ policy, not a silent heuristic.
295
+
296
+ The opt-in `root_offset_policy: "motion"` moves only the initial ROOT rest
297
+ translation into recorded root motion. It retains original root offsets in each
298
+ clip's provenance and leaves child proportions unchanged. Identical bodies can
299
+ then share a profile even when captures started at different floor positions.
300
+ The default `"retain"` preserves exact original rest origins. Both policies are
301
+ checked against Blender/native world-space poses; this is not anatomical retargeting.
302
+
303
+ For FBX, glTF/GLB and BLEND Actions, use the
304
+ [Blender import and explicit retarget guide](blender-motion-import.md). Imported
305
+ source rigs remain separately typed, and the exact source rest hierarchy is
306
+ available to reconstruct a compatible carrier.
307
+
308
+ ## Validation and ecosystem ownership
309
+
310
+ The package tests cover lazy reads, identity, tampering, bounded decompression,
311
+ atomic installation, path handling and native playback. TypeScript tests use
312
+ the actual checked-in C++ recording WASM, including cancellation and page/chunk
313
+ lifetime. Python-built gzip packages are consumed by that same browser API.
314
+ Pinned Blender tests compare source and reconstructed world transforms, exercise
315
+ multi-action/slot extraction and verify explicit Rigify FK mappings with native
316
+ skin deformation after saving/reopening.
317
+
318
+ DanceRudiments owns the reusable profile, package and recording APIs. Its existing
319
+ native playback engine is sufficient and remains shared. StemLab beat maps can
320
+ supply timing when music exists; the present motion-only sources do not warrant
321
+ running audio inference. DanceMoves/FrameRudiments can consume the page API after
322
+ a compatible shared release; this work does not claim those consumers were
323
+ released or deployed. Motion packages and program releases have separate version
324
+ identities, and each source's licence remains attached to its data.
@@ -0,0 +1,197 @@
1
+ # Versioned release demos and Pages
2
+
3
+ The **Release demo** workflow builds the exact tagged native catalogue, its
4
+ standalone visualizers, and a separate [3D mesh dance gallery](mesh-demos.md).
5
+ It packages the root visualizer and `meshes/` together, attaches verified
6
+ downloads to the same GitHub Release, and can deploy that complete site to
7
+ GitHub Pages. Building, attaching, deploying and checking the live site are
8
+ separate gates.
9
+
10
+ ## What changes on the website
11
+
12
+ The root remains the existing native pattern visualizer. A link in its header
13
+ opens `meshes/`, where two original low-poly skinned characters perform different
14
+ recorded dances. The standalone HTML download remains self-contained; the mesh
15
+ gallery is a separate HTTP-served bundle with locally bundled JavaScript, WASM,
16
+ rig profiles and selectively loaded motion files.
17
+
18
+ Earlier skeletal-motion changes added APIs, Rigify profiles, import tools and
19
+ optional packages. Those changes did not turn the XYZ pattern visualizer into a
20
+ mesh renderer. Before this release policy, a successful **Pattern visualizer**
21
+ main-branch deployment could republish the existing 2D viewer with no visible
22
+ 3D page. That result does not show that the skeletal API patch failed.
23
+
24
+ With this change, **Pattern visualizer** builds and tests main/PR previews as
25
+ artifacts only. **Release demo** is the sole Pages publisher. It deploys the
26
+ combined `dist/pages` tree, so a later main-branch preview cannot remove the
27
+ mesh gallery.
28
+
29
+ ## Trigger and source identity
30
+
31
+ The workflow runs on pushes of `vMAJOR.MINOR.PATCH` tags. It also accepts a manual
32
+ `release_tag` for an existing published release whose tagged source contains
33
+ these tools. It does not use a `release.published` event generated by another
34
+ workflow's `GITHUB_TOKEN`.
35
+
36
+ For a tag push, the checked-out commit must equal both the peeled tag and the
37
+ triggering push commit. A manual run checks that the requested release exists
38
+ and is published. Both paths validate the native, Python and npm versions with
39
+ `tools/release_version.py verify-tag`. Before attaching assets, the workflow
40
+ checks the remote tag again, including annotated tags, against the tested
41
+ commit.
42
+
43
+ Publish a **fresh release containing this change** through the existing release
44
+ process. Tags predating the gallery, including `v0.3.0`, do not contain its
45
+ builders or workflow. Manually rerunning one of those tags cannot add the new
46
+ code, and existing tags must remain unchanged.
47
+
48
+ GitHub's [workflow-trigger documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow)
49
+ explains why ordinary events created using `GITHUB_TOKEN` do not start another
50
+ workflow. The tag-triggered build works alongside the existing tag-release
51
+ workflow and waits for that workflow's GitHub Release before attaching assets.
52
+
53
+ ## Build and browser gates
54
+
55
+ The tagged job verifies all generated default collections, installs that
56
+ checkout's native Python extension, and calls the native `catalogue()` and
57
+ `sample()` APIs. It enumerates the actual catalogue rather than maintaining a
58
+ second list of movements. Required default names are checked against
59
+ `collections/defaults.json`, so a stale installed binary cannot silently
60
+ generate a smaller viewer.
61
+
62
+ The general catalogue builder embeds native samples, source notices, interface
63
+ code, CSS and a freshly compiled C++ WebAssembly snapshot sampler into one
64
+ offline HTML file. The renderer checks every pattern's native sample digest,
65
+ negative and extreme pip wrapping, desktop/mobile rendering, filters and
66
+ playback. The MIDI harness runs against the same full viewer. Club and
67
+ Dancefloor audition pages keep their separate existing browser gates.
68
+
69
+ The mesh stage then builds only the two pinned dance sources selected in
70
+ `tools/mesh_demo_sources.json`. This is an explicit workflow download, bounded
71
+ to those two hashed files. It uses the optional package loader and the existing
72
+ native recording WASM sampler. Browser QA checks:
73
+
74
+ - Both weighted triangle skins and their changing deformed vertices.
75
+ - Every normalized and physical channel at four reference pips per dance, plus
76
+ all source-space joint matrices against independently composed references.
77
+ - Play, pause, integer-pip seeking, preview speed, restart and complete-action
78
+ endpoint behavior.
79
+ - No initial rig/motion/WASM download, selected-package-only loading, and native
80
+ handle, geometry and cache disposal on switch and unload.
81
+ - Desktop and mobile layout, local-only runtime requests and browser errors.
82
+
83
+ `tools/package_release_pages.py prepare` adds only the root navigation link;
84
+ it preserves the original standalone HTML and its script/data bytes. The
85
+ complete-site browser report binds the resulting root and mesh files to their
86
+ SHA-256 hashes. `seal` accepts only the exact report schema, matching version
87
+ and commit, `scope: complete-site`, and successful coverage of both `boxbot`
88
+ and `prism`. Changed, missing, unchecked or symlinked assets fail sealing. A
89
+ local `--gallery-only` receipt cannot seal a release site.
90
+
91
+ ## Release downloads and reruns
92
+
93
+ The workflow retains the existing native viewer downloads and adds distinct
94
+ site/mesh assets:
95
+
96
+ | Asset | Contents |
97
+ | --- | --- |
98
+ | `DanceRudiments-demo-vX.Y.Z.html` and associated JSON, ZIP, PNG and checksum files | The tested standalone native catalogue viewer. |
99
+ | `DanceRudiments-visualizer.html`, `.json`, `.sha256` | Stable download names within that version's release. |
100
+ | `DanceRudiments-pages-vX.Y.Z.zip` | The complete root visualizer plus `meshes/`, notices and site manifest. |
101
+ | `DanceRudiments-pages-vX.Y.Z.json` and `.sha256` | Release identity, every site asset's size/hash, and download checksums. |
102
+ | `DanceRudiments-mesh-demos-vX.Y.Z.zip` | The mesh route under `meshes/`, including both optional motion packages and source notices. |
103
+ | `DanceRudiments-mesh-demos-vX.Y.Z-browser.json`, desktop/mobile PNGs and `.sha256` | Browser evidence for both skins, bound to the packaged bytes. |
104
+
105
+ Only the attachment job receives `contents: write`. It downloads every existing
106
+ same-name asset and compares its bytes **before uploading anything missing**.
107
+ Identical assets are retained; a different existing asset stops the run. New
108
+ attachments are downloaded again and compared. No existing release assets are
109
+ deleted or overwritten. A changed renderer/toolchain may produce different
110
+ bytes on a later rebuild; that does not authorize replacing an immutable
111
+ release attachment.
112
+
113
+ Pages ZIPs are written beside the site, never inside it. This avoids recursively
114
+ including a previous download in the next site artifact. The archives use
115
+ deterministic paths, timestamps and permissions.
116
+
117
+ ## Pages deployment and verification
118
+
119
+ Deployment uses the existing repository variable
120
+ `DANCERUDIMENTS_PAGES_ENABLED == 'true'`, the `github-pages` environment and its
121
+ approval rules. An already configured Pages site continues to use those
122
+ settings. The workflow reads existing Pages configuration with
123
+ `enablement: false`; it does not enable Pages or change protections, credentials
124
+ or environment settings.
125
+
126
+ Only the latest stable official GitHub Release may deploy the production site.
127
+ The job checks this before entering deployment, then checks again after waiting
128
+ for the shared `dancerudiments-pages` concurrency slot. Older releases may still
129
+ build and attach their own downloads; the workflow records a visible Pages
130
+ skip when their tag is no longer latest. This prevents a delayed historical
131
+ rebuild from replacing the newer site.
132
+
133
+ The deploy job alone has `pages: write` and `id-token: write`. It deploys the
134
+ already tested Pages artifact. Afterward, the verifier requests both directory
135
+ routes (`/` and `/meshes/`) and every public file, comparing the returned bytes
136
+ with the exact version/commit manifest. It also verifies the hosted manifest
137
+ itself. Precompressed `.json.gz` files must have identity content encoding;
138
+ HTTP decompression must not silently change the bytes the browser loader
139
+ hashes. `.nojekyll` is deployment metadata and need not have a public URL.
140
+
141
+ The `deployed-pages-verification` artifact records this separate live check. A
142
+ successful source test, browser run, upload or deployment action alone is not
143
+ reported as a verified live site. See GitHub's
144
+ [custom Pages workflow guide](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages)
145
+ for the existing environment and artifact requirements.
146
+
147
+ ## Local build
148
+
149
+ Use a checkout with matching package versions and the native build tools,
150
+ including `clang++` and `wasm-ld` for the standalone catalogue snapshot. Follow
151
+ the temporary-directory preflight in [AGENTS.md](../AGENTS.md) before expensive
152
+ builds. The workflow pins Playwright to the version shown below.
153
+
154
+ ```sh
155
+ python -m pip install '.[motion-import]' playwright==1.64.0
156
+ python -m playwright install chromium
157
+ npm ci
158
+
159
+ DEMO_VERSION=$(node -p 'require("./package.json").version')
160
+ DEMO_COMMIT=$(git rev-parse HEAD)
161
+ python tools/release_demo.py --version "$DEMO_VERSION" --commit "$DEMO_COMMIT" --output-dir dist/demo
162
+ python tools/test_midi_harness.py "dist/demo/DanceRudiments-demo-v$DEMO_VERSION.html"
163
+ python tools/render_release_demo.py "dist/demo/DanceRudiments-demo-v$DEMO_VERSION.html"
164
+
165
+ python tools/build_mesh_demo_assets.py --output-dir generated/mesh-demo-assets --cache-root .cache/motion-sources/cmu-dance --download
166
+ node tools/build_mesh_demo.mjs --assets-dir generated/mesh-demo-assets --output-dir dist/pages/meshes --version "$DEMO_VERSION" --commit "$DEMO_COMMIT"
167
+ python tools/package_release_pages.py prepare --pages-dir dist/pages --visualizer "dist/demo/DanceRudiments-demo-v$DEMO_VERSION.html" --version "$DEMO_VERSION" --commit "$DEMO_COMMIT"
168
+ python tools/test_mesh_demo_browser.py --site-dir dist/pages --output-dir dist/demo --prefix "DanceRudiments-mesh-demos-v$DEMO_VERSION"
169
+ python tools/package_release_pages.py seal --pages-dir dist/pages --output-dir dist/demo --qa-report "dist/demo/DanceRudiments-mesh-demos-v$DEMO_VERSION-browser.json" --version "$DEMO_VERSION" --commit "$DEMO_COMMIT"
170
+ ```
171
+
172
+ The asset builder requires a fresh output directory and uses only verified
173
+ cached sources when `--download` is omitted. Mesh-only development and the
174
+ `--chromium` override are documented in [mesh-demos.md](mesh-demos.md).
175
+
176
+ The standalone visualizer's default renderer checks `file://` navigation. Its
177
+ explicit `--in-memory` option records that narrower mode for environments that
178
+ forbid file navigation. The mesh gallery uses HTTP because its ES modules and
179
+ optional package loader fetch separate files.
180
+
181
+ ## Catalogue presentation and snapshot storage
182
+
183
+ The full catalogue remains available through collection, family, search and
184
+ review filters. Only visible cards animate; offscreen canvas backing stores are
185
+ released and restored while their phase comes from the shared clock. Personal
186
+ notes, comparison, local audio/BPM synchronization, oblique 3D paths and a
187
+ separate Z curve remain available. This generated viewer is a read-only native
188
+ snapshot; authoring export controls stay hidden.
189
+
190
+ Schema 2 stores movement tables once inside WASM, with a SHA-256 digest of the
191
+ native-exported little-endian float64 XYZ values per pattern. Signed zero is
192
+ normalized for those digests. The renderer reads samples in bounded batches and
193
+ also accepts legacy schema-1 snapshots containing decimal tables. Base64 is
194
+ decoded in bounded chunks. The general snapshot permits up to 4,096 patterns
195
+ and 4,194,304 samples; untrusted authoring packs retain their separate existing
196
+ limits. The 68-study Dancefloor page retains all 16 beat mappings, four-slot
197
+ comparison, synthesized audio and its exact scalar checks.