Skip to content

Commit 28e636d

Browse files
committed
Catalogue upgrade: movement metadata, filterable gallery, +12 movements
Inspired by standard exercise-dataset taxonomy (used only as a coverage/schema yardstick — no third-party media or records copied; all movements authored and ROM-validated here). - Metadata: extend Preset with bodyPart / target muscle / equipment / difficulty and backfill all presets, so the library is tagged like an exercise DB. - Filterable gallery: Area / Gear / Level filters in the playground that narrow the Example dropdown (options derived from the data), with a live match count. - Coverage-gap batch (docs/coverage-gap.md): 12 new body-weight/prop movements filling the biggest renderable holes — plank-hold, mountain-climber, crunch, bicycle-crunch, lying leg raise, superman, forward-lunge, step-up, single-leg calf raise, triceps dips, jumping jacks, standing quad stretch — plus the previously-unregistered push-up. Doubles chest/core/back coverage. All ROM-clean (137 tests, examples auto-validated); playground build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018WCktDrKYZpJjCb2apbqWp
1 parent 480e28c commit 28e636d

17 files changed

Lines changed: 484 additions & 71 deletions

docs/coverage-gap.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Movement coverage — gap analysis
2+
3+
> Benchmarks Movit's curated library against a standard fitness taxonomy
4+
> (body-part × equipment × target muscle), the structure popularised by public
5+
> exercise datasets. **We use those datasets only as a yardstick for *coverage and
6+
> taxonomy* — never their copyrighted media, instructions, or records.** Every
7+
> Movit movement is authored from scratch and ROM-validated.
8+
9+
The point of this doc: find the holes in our library that the **current engine can
10+
already render**, so we author exactly the right movements next.
11+
12+
## What the engine can render today
13+
14+
Renderable: **body-weight** movements, plus our props **chair / wall / bar**, on a
15+
single FK figure with ground-lock, reach-IK, hip-hinge, lying/seated poses, and a
16+
single-DOF hand rig.
17+
18+
Not yet renderable (so *not* counted as gaps — they're roadmap items): dumbbell /
19+
barbell / cable / machine / band work (needs load + grip-to-implement), and
20+
anything needing a second person. In a typical exercise dataset these are the
21+
majority; the **body-weight subset (~25%)** is our addressable universe.
22+
23+
## Movit library today, by body part (51 movements)
24+
25+
| Body part | Have | Count | Verdict |
26+
| --- | --- | --- | --- |
27+
| Upper legs (quads/hams/glutes) | squat, deadlift, good-morning, hamstring-curl, hip-abduction, glute-bridge, box-squat, wall-sit, sit-to-stand, demi-plié, hip/knee ROM | ~12 | Strong, but **no lunges or step-ups** |
28+
| Core / waist | dead-bug, glute-bridge, hanging-knee-raise, spinal-twist, side-bend, cross-body-reach | ~6 | **No crunch / plank / leg-raise / climber** |
29+
| Shoulders | lateral-raise, shoulder-flexion, shoulder-abduction, shoulder-rolls, overhead-reach, arm-circles, port-de-bras | ~7 | Good |
30+
| Back | bent-over-row, deadlift, good-morning, cobra, dead-hang | ~5 | **No superman / extension holds** |
31+
| Chest | chest-opener (stretch only) | 1 | **Weak — push-up not even registered** |
32+
| Upper arms | biceps-curl, elbow-forearm | 2 | **No triceps work** |
33+
| Lower legs | heel-raises, relevé | 2 | Thin |
34+
| Neck | neck-rotation, neck-side-stretch | 2 | Fine for scope |
35+
| Hands | make-a-fist, pinch-grip, finger-spell, hand-wave | 4 | Fine for scope |
36+
| Full body / conditioning | front-kick, jab-cross, horse-stance, dance-phrase | ~4 | **No jacks / climbers / burpee-family** |
37+
38+
## Biggest renderable gaps → the authoring queue
39+
40+
Ordered by gap size × how cleanly the engine renders it:
41+
42+
1. **Core**`plank-hold`, `mountain-climber` (plank pose), `crunch`,
43+
`bicycle-crunch`, `supine-leg-raise` (supine pose). Highest-value, cleanest.
44+
2. **Chest** — register the existing `push-up`, add `wall-pushup` (wall prop).
45+
3. **Back**`superman` (prone pose).
46+
4. **Upper legs**`forward-lunge`, `step-up` (chair prop).
47+
5. **Upper arms**`triceps-dips` (chair/bar prop).
48+
6. **Lower legs**`single-leg-calf-raise`.
49+
7. **Conditioning / mobility**`jumping-jacks`, `standing-quad-stretch`
50+
(single-leg + reach-IK).
51+
52+
These 13–14 movements roughly **double our chest/core/back coverage** and close the
53+
most glaring holes — all on today's engine, no new primitives required.
54+
55+
## Metadata gap (drives the catalogue work)
56+
57+
A real exercise record carries `body-part, equipment, primary target, secondary
58+
muscles, difficulty`. Movit presets carried only `id, label, domain`. We now add
59+
`bodyPart / target / equipment / difficulty` to every preset and a **filterable
60+
gallery**, so the library is searchable like a proper exercise explorer — see
61+
`playground/src/presets.ts` and the filter row in the playground.

playground/play.html

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,20 @@
2727
<span class="tag">playground</span>
2828
</a>
2929
<div class="topbar-actions">
30+
<label class="field filter">
31+
<span>Area</span>
32+
<div class="select-wrap sm"><select id="f-body"></select></div>
33+
</label>
34+
<label class="field filter">
35+
<span>Gear</span>
36+
<div class="select-wrap sm"><select id="f-equip"></select></div>
37+
</label>
38+
<label class="field filter">
39+
<span>Level</span>
40+
<div class="select-wrap sm"><select id="f-level"></select></div>
41+
</label>
3042
<label class="field">
31-
<span>Example</span>
43+
<span>Example <span id="preset-count" class="count"></span></span>
3244
<div class="select-wrap">
3345
<select id="preset"></select>
3446
</div>

playground/src/main.ts

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,77 @@ function recompile(): void {
167167
}
168168
}
169169

170-
// --- Presets (grouped by domain so the dropdown shows the breadth) ---
171-
const presetGroups = new Map<string, HTMLOptGroupElement>();
172-
for (const p of PRESETS) {
173-
let group = presetGroups.get(p.domain);
174-
if (!group) {
175-
group = document.createElement("optgroup");
176-
group.label = p.domain;
177-
presetGroups.set(p.domain, group);
178-
presetSel.append(group);
170+
// --- Presets: a filterable gallery (body part / equipment / difficulty) ---
171+
// The catalogue is tagged like a standard exercise DB, so the dropdown can be
172+
// narrowed the way an exercise explorer would. Options for each filter are
173+
// derived from the data so they never drift.
174+
const fBody = $<HTMLSelectElement>("f-body");
175+
const fEquip = $<HTMLSelectElement>("f-equip");
176+
const fLevel = $<HTMLSelectElement>("f-level");
177+
const presetCount = $<HTMLSpanElement>("preset-count");
178+
179+
function fillFilter(sel: HTMLSelectElement, values: string[], allLabel: string): void {
180+
sel.innerHTML = "";
181+
const all = document.createElement("option");
182+
all.value = "";
183+
all.textContent = allLabel;
184+
sel.append(all);
185+
for (const v of [...new Set(values)].sort()) {
186+
const opt = document.createElement("option");
187+
opt.value = v;
188+
opt.textContent = v;
189+
sel.append(opt);
179190
}
180-
const opt = document.createElement("option");
181-
opt.value = p.id;
182-
opt.textContent = p.label;
183-
group.append(opt);
184191
}
192+
fillFilter(fBody, PRESETS.map((p) => p.bodyPart), "All areas");
193+
fillFilter(fEquip, PRESETS.map((p) => p.equipment), "All gear");
194+
fillFilter(fLevel, PRESETS.map((p) => p.difficulty), "All levels");
195+
196+
/** Rebuild the Example dropdown from the presets matching the active filters. */
197+
function rebuildPresetOptions(): void {
198+
const matches = PRESETS.filter(
199+
(p) =>
200+
(!fBody.value || p.bodyPart === fBody.value) &&
201+
(!fEquip.value || p.equipment === fEquip.value) &&
202+
(!fLevel.value || p.difficulty === fLevel.value),
203+
);
204+
presetSel.innerHTML = "";
205+
const groups = new Map<string, HTMLOptGroupElement>();
206+
for (const p of matches) {
207+
let group = groups.get(p.domain);
208+
if (!group) {
209+
group = document.createElement("optgroup");
210+
group.label = p.domain;
211+
groups.set(p.domain, group);
212+
presetSel.append(group);
213+
}
214+
const opt = document.createElement("option");
215+
opt.value = p.id;
216+
opt.textContent = `${p.label} · ${p.target}`;
217+
group.append(opt);
218+
}
219+
presetCount.textContent = `(${matches.length})`;
220+
if (matches.length === 0) {
221+
const opt = document.createElement("option");
222+
opt.textContent = "No matches — clear filters";
223+
opt.disabled = true;
224+
presetSel.append(opt);
225+
}
226+
}
227+
rebuildPresetOptions();
228+
229+
for (const f of [fBody, fEquip, fLevel]) {
230+
f.addEventListener("change", () => {
231+
rebuildPresetOptions();
232+
// Auto-load the first match so the viewer always reflects the filter.
233+
const first = PRESETS.find((p) => p.id === presetSel.value);
234+
if (first) {
235+
editorApi.setValue(first.source);
236+
recompile();
237+
}
238+
});
239+
}
240+
185241
presetSel.addEventListener("change", () => {
186242
const preset = PRESETS.find((p) => p.id === presetSel.value);
187243
if (preset) {

0 commit comments

Comments
 (0)