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

+43 -10
View File
@@ -86,12 +86,48 @@ class Visualizer {
this.effect = Object.assign({}, this.effect, effect);
}
// Load a GLB model for the 'Modello 3D' family. Parsed lazily on the next
// render so the engine is only created when the family is active.
// The shared 3D engine, fed with any pending model, the bundled animation
// library and the persisted repertoire/BPM settings.
_ensureModel3d() {
if (!this.model3d) {
this.model3d = new window.ModelSim(this.gl);
if (this._libBuf) this.model3d.setAnimLibrary(this._libBuf);
if (this._extraLibs) this._extraLibs.forEach(b => {
try { this.model3d.addAnimLibrary(b); } catch (e) { /* già segnalato */ }
});
if (this._clipFilter) this.model3d.clipFilter = this._clipFilter;
if (this._manualBpm) this.model3d.manualBpm = this._manualBpm;
}
if (this._pendingGlb) { this.model3d.setModel(this._pendingGlb); this._pendingGlb = null; }
return this.model3d;
}
// Load a GLB model for the 'Modello 3D' family (parsed immediately so the
// panel can list its clips).
setModelData(buf) {
if (this.model3d) return this.model3d.setModel(buf);
this._pendingGlb = buf;
return true;
if (!window.ModelSim) { this._pendingGlb = buf; return true; }
return this._ensureModel3d().setModel(buf);
}
setAnimLibrary(buf) {
this._libBuf = buf;
if (this.model3d) this.model3d.setAnimLibrary(buf);
}
addAnimLibrary(buf) {
(this._extraLibs = this._extraLibs || []).push(buf);
if (!window.ModelSim) return [];
return this._ensureModel3d().addAnimLibrary(buf);
}
setClipFilter(names) {
this._clipFilter = new Set(names || []);
if (this.model3d) this.model3d.clipFilter = this._clipFilter;
}
setManualBpm(bpm) {
this._manualBpm = bpm || 0;
if (this.model3d) this.model3d.manualBpm = this._manualBpm;
}
// Upload a custom source (HTMLImageElement / HTMLCanvasElement) for the
@@ -112,8 +148,7 @@ class Visualizer {
// Release the camera as soon as a non-interactive preset takes over.
if (this.inter && !e.isInteractive) this.inter.suspend();
if (e.isModel3d && window.ModelSim) {
if (!this.model3d) this.model3d = new window.ModelSim(gl);
if (this._pendingGlb) { this.model3d.setModel(this._pendingGlb); this._pendingGlb = null; }
this._ensureModel3d();
// optional backdrop behind the model: a fluid mode ('ink'/'fire'/...)
// or any shader family ('fam:<index>'), tinted with the active palette
const bg = this.modelBg || 'gradient';
@@ -142,9 +177,7 @@ class Visualizer {
this.inter.customSource = this.customSource || null;
// The model-puppet and avatar modes drive the shared 3D renderer.
if ((e.interactiveMode === 'model' || e.interactiveMode === 'avatar') && window.ModelSim) {
if (!this.model3d) this.model3d = new window.ModelSim(gl);
if (this._pendingGlb) { this.model3d.setModel(this._pendingGlb); this._pendingGlb = null; }
this.inter.modelSim = this.model3d;
this.inter.modelSim = this._ensureModel3d();
}
this.inter.render(timeSec, audio, e, this.canvas);
return;