@kieransimkin/dance-rudiments 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kieran Simkin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # DanceRudiments
2
+
3
+ DanceRudiments is a small C++17 library of deterministic rhythmic position functions. One integer pip is `1/64` of a beat. Every rudiment owns a 64-, 128-, or 256-pip loop and wraps any positive or negative input into that loop before sampling.
4
+
5
+ Outputs are dimensionless offsets, normally in `[-1, 1]`. The caller chooses pixels, CSS units, metres, or another scale. There is deliberately no time interpolation in the public API: renderers advance with integer pips and get one exact sample per pip.
6
+
7
+ ## Included rudiments
8
+
9
+ The general motions are `bounce`, `sway`, `circle`, `figure_eight`, `step_touch`, `box_step`, `helix`, and `clay_background`.
10
+
11
+ The first drum-derived set is `single_stroke_roll`, `multiple_bounce_roll`, `double_stroke_roll`, `single_paradiddle`, `flam`, `drag`, and `five_stroke_roll`. The first six mirror the fundamentals in Vic Firth's Tier One learning sequence and collectively cover the roll, diddle, flam, and drag families in the Percussive Arts Society's 40 International Drum Rudiments. A right-hand stroke moves right, a left-hand stroke moves left, and both rise slightly. Every stroke is a raised-cosine (`sin²`) gesture: zero displacement at the start, smooth attack, a rounded peak, and smooth decay back to zero before the next motion. Accents use greater amplitude rather than an instantaneous position jump.
12
+
13
+ Source references: [Percussive Arts Society International Drum Rudiments](https://pas.org/rudiments/) and [Vic Firth 40 Essential Rudiments](https://ae.vicfirth.com/education/40-essential-rudiments/), accessed 28 September 2026.
14
+
15
+ `clay_background` preserves the translation waypoints from the Clay/Stars `ks-particle-dance` background treatment as a normalized 256-pip lookup table. The source effect's opacity, rotation, and scale are not position offsets, so they are intentionally excluded.
16
+
17
+ ## Build and test the C++ core
18
+
19
+ ```powershell
20
+ cmake -S . -B build -G Ninja
21
+ cmake --build build
22
+ ctest --test-dir build --output-on-failure
23
+ ```
24
+
25
+ ## Python binding
26
+
27
+ Install pybind11 in your chosen environment, then configure with `-DDANCERUDIMENTS_BUILD_PYTHON=ON`. The module exposes `sample(name, pip_count)`, the named functions, and `catalogue()`.
28
+
29
+ ## TypeScript/WASM binding
30
+
31
+ Configure under Emscripten with `emcmake cmake -DDANCERUDIMENTS_BUILD_WASM=ON`, build `dancerudiments_wasm`, import its generated ES module, then call `bindNative(await createDanceRudiments())` from `bindings/typescript/index.ts`.
32
+
33
+ The dependency-free harness uses a browser-native mirror of the same discrete definitions so it can run without a toolchain. Its tests validate modulo wrapping and seam behaviour. Production TypeScript consumers should use the WASM binding so C++ remains the single runtime authority.
34
+
35
+ ## Harness
36
+
37
+ Serve the repository root over HTTP and open `harness/index.html`. The harness provides play/pause, exact pip stepping, BPM, amplitude, trails, a numeric readout, and reduced-motion-aware manual operation.
38
+
39
+ ```powershell
40
+ python -m http.server 4173
41
+ ```
42
+
43
+ Then open `http://localhost:4173/harness/`.
44
+
45
+ ## Continuous integration and releases
46
+
47
+ `.github/workflows/ci.yml` builds and tests the C++ core on Linux, Windows, and macOS; builds and imports the Python package; compiles/tests the TypeScript and browser runtime; and creates a Conan package on every push to `main` and every pull request.
48
+
49
+ Publishing is deliberately tied to a GitHub release with a `vMAJOR.MINOR.PATCH` tag. Before creating the release, update the matching version in `CMakeLists.txt`, `pyproject.toml`, and `package.json`. `.github/workflows/release.yml` rejects mismatches before it publishes anything, then:
50
+
51
+ - builds platform Python wheels and a source distribution and publishes them to PyPI using OIDC Trusted Publishing;
52
+ - builds the TypeScript wrapper and C++ WebAssembly module and publishes `@kieransimkin/dance-rudiments` to npm using npm Trusted Publishing and provenance;
53
+ - builds installable C++ archives for Linux, Windows, and macOS and attaches them, along with Python distributions, to the GitHub release; and
54
+ - builds a Conan package and uploads it only when a separate Conan remote has been configured. ConanCenter packages are submitted through `conan-center-index`; they are not directly uploaded by this repository.
55
+
56
+ One-time registry configuration is required before the first release:
57
+
58
+ 1. On PyPI, create a pending Trusted Publisher for owner `kieransimkin`, repository `DanceRudiments`, workflow `release.yml`, environment `pypi`, and project name `dancerudiments`.
59
+ 2. On npm, configure the package's GitHub Actions Trusted Publisher for `kieransimkin/DanceRudiments`, workflow `release.yml`, environment `npm`, with direct publishing allowed. Because npm Trusted Publishers are configured from an existing package's settings, the first scoped-package registration may require a granular `NPM_TOKEN` secret in the `npm` environment. Remove that bootstrap token after Trusted Publishing is configured.
60
+ 3. Create GitHub environments named `pypi`, `npm`, and `conan`; add required reviewers if desired.
61
+ 4. For an optional private or organisational Conan repository, set environment variable `CONAN_REMOTE_URL` and secrets `CONAN_LOGIN_USERNAME` and `CONAN_PASSWORD` in the `conan` environment. If they are absent, the recipe is built and verified but not uploaded.
62
+
63
+ The publishing jobs use short-lived OIDC identity for PyPI and npm and do not require long-lived PyPI or npm tokens. See the official [PyPI Trusted Publisher](https://docs.pypi.org/trusted-publishers/using-a-publisher/) and [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) documentation.
64
+
65
+ ## Motion safety
66
+
67
+ Apply an output to a bounded subject. DanceRudiments changes position only; it must not be used to add a repetitive full-frame tint, brightness, flash, or colour-grade effect. Under `prefers-reduced-motion`, do not autoplay the harness or production motion.
68
+
69
+ ## Potential problems
70
+
71
+ ### npm cache access is denied on Windows
72
+
73
+ - **Symptom (28 September 2026):** `npm pack --dry-run` failed with `EPERM: operation not permitted, open 'C:\Users\Kieran\AppData\Local\npm-cache\_cacache\tmp\…'` after the TypeScript tests had passed.
74
+ - **Cause:** the shared user cache could not create its temporary file. npm's Windows issue tracker records this class of `EPERM` failure, including cases involving cache files and real-time scanning; the exact process holding this particular file was not identified.
75
+ - **Corrective action:** keep the shared cache intact and run package validation with a repository-local cache: `npm pack --dry-run --cache .npm-cache`. The cache directory is ignored by Git.
76
+ - **Verification:** the dry-run package completed with the local cache. This workaround changes only npm's disposable cache location and does not change the package contents.
77
+ - **Research:** [npm CLI Windows EPERM report](https://github.com/npm/cli/issues/8072) and [npm CLI cache-isolation guidance](https://github.com/npm/cli/issues/1785), accessed 28 September 2026.
78
+
79
+ ### CMake stalls while detecting the C++ compiler ABI on this host
80
+
81
+ - **Symptom (28 September 2026):** CMake 3.30 with Ninja and the project-available MinGW GCC 16.2 compiler stopped after `Detecting CXX compiler ABI info` for more than 90 seconds. Compiler identification itself succeeded, and no diagnostic failure was emitted.
82
+ - **Cause:** unknown. Similar CMake reports show that this stage is a nested `try_compile`, but the available reports do not establish the cause on this Windows/Z-drive setup.
83
+ - **Working validation route:** compile the exact core and test sources directly with `g++ -std=c++17 -I include src/dance_rudiments.cpp tests/cpp/test_main.cpp -o build/dancerudiments_tests.exe`, then run the executable. Do not weaken or hard-code CMake's portable compiler detection merely to hide the local stall.
84
+ - **Verification:** the direct GCC build completed and all C++ assertions passed. This verifies the library sources with the available compiler; it does not claim that CMake configuration succeeded on this host.
85
+ - **Research:** [CMake discussion of a compiler-ABI detection hang](https://discourse.cmake.org/t/how-to-debug-detecting-c-compiler-abi-info-hanging-cygwin-on-github-actions/4580), accessed 28 September 2026.
86
+
87
+ ### Node test isolation is denied
88
+
89
+ - **Symptom (28 September 2026):** `node --test "tests/typescript/*.test.js"` failed before assertions with `Error: spawn EPERM`.
90
+ - **Cause:** the restricted Windows host denied Node's child-process worker spawn.
91
+ - **Corrective action:** run `node --test --test-isolation=none "tests/typescript/*.test.js"`.
92
+ - **Verification:** both runtime suites passed in the single process. Keep tests free of shared mutable global state when adding more files.
93
+
94
+ ### An isolated Python build selects unavailable NMake on Windows
95
+
96
+ - **Symptom (28 September 2026):** the first scikit-build-core attempt failed during configuration with `Running 'nmake' '-?' failed with: no such file or directory` and `CMAKE_CXX_COMPILER not set`.
97
+ - **Cause supported by current evidence:** CMake selected the `NMake Makefiles` generator although this host provides Ninja and MinGW GCC, not NMake/MSVC. The first `uv run` invocation also attempted an unnecessary editable install of the current project before running the requested build command.
98
+ - **Corrective action:** set `CMAKE_GENERATOR=Ninja` for this local toolchain and use `uv run --no-project --with build python -m build`, leaving CI runners free to select their native supported compiler environment.
99
+ - **Verification:** the isolated build then produced `dancerudiments-0.1.0.tar.gz` and a CPython 3.13 Windows wheel, with the C++ extension compiled and installed successfully.
100
+ - **Research:** CMake documents `CMAKE_GENERATOR` as the supported generator-selection mechanism, while scikit-build-core documents Ninja selection and its `ninja.make-fallback` behaviour. Sources: [CMake generator environment variable](https://cmake.org/cmake/help/latest/envvar/CMAKE_GENERATOR.html) and [scikit-build-core configuration](https://scikit-build-core.readthedocs.io/en/stable/configuration/), accessed 28 September 2026.
101
+
102
+ ### A MinGW-built Python wheel cannot locate its C++ runtime DLLs
103
+
104
+ - **Symptom (28 September 2026):** the first locally built Windows wheel installed successfully but `import dancerudiments` failed with `ImportError: DLL load failed while importing dancerudiments: The specified module could not be found.`
105
+ - **Cause supported by current evidence:** the MinGW-built extension dynamically referenced GCC runtime libraries that were available in the compiler directory but not in the isolated Python environment. This local toolchain differs from cibuildwheel's normal Windows MSVC environment.
106
+ - **Corrective action:** inspect the built `.pyd` with the matching toolchain's `objdump -p`. It identified `libwinpthread-1.dll` as the remaining non-system dependency after the standard C++ runtimes were made static. Only for MinGW, link with `-static-libgcc -static-libstdc++` and install the compiler's matching `libwinpthread-1.dll` beside the extension; do not apply that bundling to MSVC or other platforms.
107
+ - **Verification:** rebuild the wheel and import it in a fresh isolated environment; require the catalogue and modulo-sampling smoke assertions to pass. The release workflow independently runs cibuildwheel's installed-wheel test on every platform.
108
+ - **Research:** GCC documents `-static-libstdc++` as linking the C++ runtime statically without making the whole module static, and the pybind11 issue tracker records the same generic Windows import symptom when a compiler runtime DLL is missing. Sources: [GCC link options](https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html) and [pybind11 missing-runtime discussion](https://github.com/pybind/pybind11/issues/2771), accessed 28 September 2026.
109
+
110
+ ### Conan detects an unavailable future Visual Studio generator
111
+
112
+ - **Symptom (28 September 2026):** local `conan profile detect` selected `msvc` version 195 and generated `Visual Studio 18 2026`, while CMake 3.30 on this host only exposes Visual Studio generators through 2022. The recipe consequently failed before compiling.
113
+ - **Cause:** the host's compiler discovery evidence is inconsistent: no usable `cl.exe` is on the command path, but Conan detected a newer MSVC installation than the installed CMake understands. This is a local toolchain/profile mismatch, not a recipe failure established across supported runners.
114
+ - **Corrective action:** do not commit the guessed local profile or hard-code a generator in the portable recipe. CI detects and builds the recipe on `ubuntu-latest` with its supported native profile; Windows consumers should use a profile naming an installed compiler and generator combination.
115
+ - **Verification:** require the GitHub Actions Conan job to complete `conan create` from the committed recipe. Until that run passes, local recipe syntax/export is verified but the Conan binary build remains pending.
116
+ - **Research:** Conan's profile detector warns that detected profiles are guesses and not guaranteed stable; CMake documents that the selected generator must match an available build environment. See [CMake's user interaction guide](https://cmake.org/cmake/help/latest/guide/user-interaction/index.html), accessed 28 September 2026.
@@ -0,0 +1,19 @@
1
+ export type Offset3 = Readonly<{
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ }>;
6
+ export type RudimentName = "bounce" | "sway" | "circle" | "figure_eight" | "step_touch" | "box_step" | "helix" | "clay_background" | "single_stroke_roll" | "double_stroke_roll" | "multiple_bounce_roll" | "single_paradiddle" | "flam" | "drag" | "five_stroke_roll";
7
+ export type RudimentInfo = Readonly<{
8
+ name: RudimentName;
9
+ description: string;
10
+ periodPips: 64 | 128 | 256;
11
+ dimensions: 1 | 2 | 3;
12
+ }>;
13
+ type NativeModule = {
14
+ sample(name: string, pipCount: number): Offset3;
15
+ };
16
+ export declare const catalogue: readonly RudimentInfo[];
17
+ export declare function bindNative(module: NativeModule): void;
18
+ export declare function sample(name: RudimentName, pipCount: number): Offset3;
19
+ export {};
@@ -0,0 +1,26 @@
1
+ export const catalogue = [
2
+ { name: "bounce", description: "One-beat vertical bounce", periodPips: 64, dimensions: 1 },
3
+ { name: "sway", description: "Two-beat side-to-side sway", periodPips: 128, dimensions: 1 },
4
+ { name: "circle", description: "One-beat circular orbit", periodPips: 64, dimensions: 2 },
5
+ { name: "figure_eight", description: "Two-beat figure eight", periodPips: 128, dimensions: 2 },
6
+ { name: "step_touch", description: "Two-beat side step with a small lift", periodPips: 128, dimensions: 2 },
7
+ { name: "box_step", description: "Four-beat softened square path", periodPips: 256, dimensions: 2 },
8
+ { name: "helix", description: "Four-beat double-turn helix", periodPips: 256, dimensions: 3 },
9
+ { name: "clay_background", description: "Clay/Stars four-beat accented background drift", periodPips: 256, dimensions: 2 },
10
+ { name: "single_stroke_roll", description: "Alternating R/L strokes with smooth attack and decay", periodPips: 64, dimensions: 2 },
11
+ { name: "double_stroke_roll", description: "RRLL strokes with a softer second motion", periodPips: 64, dimensions: 2 },
12
+ { name: "multiple_bounce_roll", description: "Decaying same-hand bounce clusters", periodPips: 64, dimensions: 2 },
13
+ { name: "single_paradiddle", description: "RLRR LRLL with accented lead strokes", periodPips: 128, dimensions: 2 },
14
+ { name: "flam", description: "Grace motion flowing into an opposite-hand primary motion", periodPips: 64, dimensions: 2 },
15
+ { name: "drag", description: "Two grace motions flowing into an opposite-hand primary motion", periodPips: 64, dimensions: 2 },
16
+ { name: "five_stroke_roll", description: "Two diddles resolving to an accented fifth motion", periodPips: 128, dimensions: 2 }
17
+ ];
18
+ let native;
19
+ export function bindNative(module) { native = module; }
20
+ export function sample(name, pipCount) {
21
+ if (!native)
22
+ throw new Error("DanceRudiments WASM is not bound. Call bindNative(await createDanceRudiments()).");
23
+ if (!Number.isInteger(pipCount))
24
+ throw new TypeError("pipCount must be an integer");
25
+ return native.sample(name, pipCount);
26
+ }