Dance library: bundled clips bind to any rigged model + repertoire + manual BPM

- models/dance_library.glb: skeleton-only GLB with the 13 Mixamo dances,
  loaded at startup and bound BY BONE NAME to whatever rigged GLB the
  user loads — new models dance immediately, no baking needed.
- Repertoire UI (Media tab): checkbox chips of the available clips, the
  director only picks the ticked ones (none = all); persisted.
- 'Aggiungi animazioni': extra .glb clips load directly; .fbx are
  converted on the fly by Blender via a new anim:convert IPC and the
  bundled scripts/fbx2animglb.py (asarUnpacked for packaged builds).
- Manual BPM with TAP tempo: 0 = follow the detected beat, otherwise a
  pure metronome drives the dance clock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-07-31 11:15:10 +02:00
1 parent 83fc655219
commit 97376b22c9
10 files changed
+340 -20

No files matched your search

+28
View File
@@ -108,6 +108,34 @@ async function ensureMicAccess() {
} catch (e) { /* getUserMedia will prompt again if needed */ }
}
// Convert a Mixamo FBX animation into a skeleton-only GLB clip via Blender
// (when installed): lets the user add moves to the dance library from the
// panel without any manual conversion step.
function blenderPath() {
const cands = ['/Applications/Blender.app/Contents/MacOS/Blender',
'/opt/homebrew/bin/blender', '/usr/local/bin/blender'];
for (const c of cands) if (fs.existsSync(c)) return c;
return null;
}
ipcMain.handle('anim:convert', async (_e, fbxPath) => {
const blender = blenderPath();
if (!blender) return { ok: false, error: 'Blender non trovato: serve per convertire gli FBX (oppure carica un .glb)' };
const outDir = path.join(app.getPath('userData'), 'anims');
fs.mkdirSync(outDir, { recursive: true });
const out = path.join(outDir, path.basename(fbxPath).replace(/\.fbx$/i, '') + '.glb');
// packaged app: the .py must live outside the asar for Blender to read it
const script = path.join(__dirname, 'scripts', 'fbx2animglb.py')
.replace('app.asar' + path.sep, 'app.asar.unpacked' + path.sep);
return new Promise((resolve) => {
execFile(blender, ['-b', '-P', script, '--', fbxPath, out],
{ maxBuffer: 1 << 24, timeout: 180000 }, (err, _so, se) => {
if (err || !fs.existsSync(out))
resolve({ ok: false, error: 'conversione fallita: ' + String(se || (err && err.message) || '').slice(-300) });
else resolve({ ok: true, path: out });
});
});
});
// Camera permission is requested lazily (only when an interactive preset is
// activated), not at startup: no camera prompt for users who never use it.
ipcMain.handle('cam:ensure', async () => {