feat(web): keep only MuJoCo presets and default to 30cm

This commit is contained in:
5shekel
2026-02-19 10:41:47 +02:00
parent fd10d98160
commit 9e1eabf8a8
3 changed files with 8 additions and 131 deletions

View File

@@ -21,7 +21,7 @@
- tendon (`BE`) with automatic coupling to `AD`
- defaults normalized to a `45 cm` leg baseline while preserving original mechanism ratios
- mirrored automatically to far side (180° phase side)
- Presets save/load for named dimension setups (persisted in browser local storage).
- Presets tab for built-in MuJoCo playback traces (10cm/20cm/30cm).
- Foot trajectory traces for both sides.
- Optional linkage annotation overlay (joint and link labels).
- Optional real-time 3D viewport with orbit camera and camera reset.

View File

@@ -6,8 +6,6 @@ import { clearTrace, createTraceBuffer, pushTrace } from './trace.js';
import { updateDiagnostics, wireUI } from './ui.js';
import { BASE_DIMENSIONS_CM } from './geometry.js';
const PRESET_STORAGE_KEY = 'walkerSim.presets.v1';
const DEFAULT_LENGTH_CM = {
crank: BASE_DIMENSIONS_CM.crank,
leg: BASE_DIMENSIONS_CM.leg,
@@ -15,12 +13,12 @@ const DEFAULT_LENGTH_CM = {
body: BASE_DIMENSIONS_CM.body,
};
const BUILTIN_PRESET_NAME = 'Default 45cm leg';
const MUJOCO_TRACE_PRESETS = {
'MuJoCo 10cm trace': './sim/mujoco/model/mujoco_trace_10cm.json',
'MuJoCo 20cm trace': './sim/mujoco/model/mujoco_trace_20cm.json',
'MuJoCo 30cm trace': './sim/mujoco/model/mujoco_trace_30cm.json',
'MuJoCo 20cm trace': './sim/mujoco/model/mujoco_trace_20cm.json',
'MuJoCo 10cm trace': './sim/mujoco/model/mujoco_trace_10cm.json',
};
const DEFAULT_MUJOCO_TRACE_PRESET = 'MuJoCo 30cm trace';
const canvas = document.getElementById('sim-canvas');
const view3dPanelEl = document.getElementById('view-3d-panel');
@@ -283,11 +281,6 @@ function currentLengthScale() {
};
}
function sanitizePresetName(name) {
if (typeof name !== 'string') return '';
return name.trim().slice(0, 40);
}
function resetSimulation({ clearLengths = false } = {}) {
if (clearLengths) {
state.lengthCm = { ...DEFAULT_LENGTH_CM };
@@ -307,52 +300,6 @@ function resetSimulation({ clearLengths = false } = {}) {
state.pose = applyGrounding(raw);
}
function loadPersistedPresets() {
const presets = {
[BUILTIN_PRESET_NAME]: { ...DEFAULT_LENGTH_CM },
};
let parsed = null;
try {
parsed = JSON.parse(localStorage.getItem(PRESET_STORAGE_KEY) ?? 'null');
} catch {
parsed = null;
}
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
for (const [rawName, rawPreset] of Object.entries(parsed)) {
const name = sanitizePresetName(rawName);
if (!name || !rawPreset || typeof rawPreset !== 'object') continue;
const crank = Number(rawPreset.crank);
const leg = Number(rawPreset.leg);
const tendon = Number(rawPreset.tendon);
const body = Number(rawPreset.body);
if ([crank, leg, tendon, body].every(Number.isFinite)) {
presets[name] = { crank, leg, tendon, body };
}
}
}
return presets;
}
function persistPresets() {
const serializable = {};
for (const [name, preset] of Object.entries(state.presets)) {
if (name === BUILTIN_PRESET_NAME) continue;
serializable[name] = {
crank: preset.crank,
leg: preset.leg,
tendon: preset.tendon,
body: preset.body,
};
}
localStorage.setItem(PRESET_STORAGE_KEY, JSON.stringify(serializable));
}
state.presets = loadPersistedPresets();
refreshLayoutMode();
const ui = wireUI(state, {
@@ -363,24 +310,6 @@ const ui = wireUI(state, {
refresh() {
resetSimulation({ clearLengths: false });
},
savePreset(name) {
const presetName = sanitizePresetName(name);
if (!presetName) return null;
state.presets[presetName] = { ...state.lengthCm };
persistPresets();
return presetName;
},
loadPreset(name) {
const presetName = sanitizePresetName(name);
const preset = state.presets[presetName];
if (!preset) return false;
state.lengthCm = { ...preset };
resetSimulation({ clearLengths: false });
return true;
},
listPresets() {
return Object.keys(state.presets);
},
listMujocoTracePresets() {
return Object.keys(MUJOCO_TRACE_PRESETS);
},
@@ -672,6 +601,10 @@ if (mujocoPlaybackTimeEl) {
setMujocoPlaybackControlsVisible(false);
syncMujocoPlaybackControls();
void loadMujocoTraceFromUrl(MUJOCO_TRACE_PRESETS[DEFAULT_MUJOCO_TRACE_PRESET]).catch((err) => {
console.error('[MuJoCo overlay] Failed to load default preset', err);
});
/* ── Mobile toggle button wiring ── */
if (mobileToggleEl) {
mobileToggleEl.addEventListener('click', (e) => {

View File

@@ -34,22 +34,9 @@ export function wireUI(state, hooks) {
showAnnotations: state.showAnnotations,
show3d: state.show3d,
canShow3d: state.canShow3d,
presetName: '',
selectedPreset: '',
selectedMujocoPreset: '',
};
function presetOptions() {
const names = hooks.listPresets();
if (names.length === 0) return { none: '' };
const options = {};
for (const name of names) {
options[name] = name;
}
return options;
}
function mujocoPresetOptions() {
const names = hooks.listMujocoTracePresets?.() ?? [];
if (names.length === 0) return { none: '' };
@@ -61,32 +48,8 @@ export function wireUI(state, hooks) {
return options;
}
let presetBinding = null;
let mujocoPresetBinding = null;
function rebuildPresetList(preferredName = null) {
const options = presetOptions();
const optionValues = Object.values(options);
if (optionValues.length === 0) {
params.selectedPreset = '';
} else {
const next = preferredName && optionValues.includes(preferredName)
? preferredName
: optionValues[0];
params.selectedPreset = next;
}
if (presetBinding) {
presetBinding.dispose();
presetBinding = null;
}
presetBinding = presetsFolder.addBinding(params, 'selectedPreset', {
label: 'Saved preset',
options,
});
}
function rebuildMujocoPresetList(preferredName = null) {
const options = mujocoPresetOptions();
const optionValues = Object.values(options);
@@ -125,7 +88,6 @@ export function wireUI(state, hooks) {
motionFolder.addButton({ title: 'Reset' }).on('click', () => {
hooks.reset();
syncFromState();
rebuildPresetList(params.selectedPreset);
pane.refresh();
});
@@ -184,24 +146,6 @@ export function wireUI(state, hooks) {
const presetsFolder = pane.addFolder({ title: 'Presets', expanded: !isCompactViewport });
presetsFolder.expanded = !isCompactViewport;
presetsFolder.addBinding(params, 'presetName', { label: 'Preset name' });
rebuildPresetList();
presetsFolder.addButton({ title: 'Save preset' }).on('click', () => {
const savedName = hooks.savePreset(params.presetName);
if (!savedName) return;
params.presetName = savedName;
rebuildPresetList(savedName);
pane.refresh();
});
presetsFolder.addButton({ title: 'Load selected preset' }).on('click', () => {
const target = params.selectedPreset;
if (!target) return;
const loaded = hooks.loadPreset(target);
if (!loaded) return;
syncFromState();
rebuildPresetList(target);
pane.refresh();
});
rebuildMujocoPresetList();
presetsFolder.addButton({ title: 'Load MuJoCo preset' }).on('click', async () => {
const target = params.selectedMujocoPreset;