feat(web): keep only MuJoCo presets and default to 30cm
This commit is contained in:
@@ -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.
|
||||
|
||||
81
src/main.js
81
src/main.js
@@ -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) => {
|
||||
|
||||
56
src/ui.js
56
src/ui.js
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user