mujoco-react 10.9.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/{chunk-5WFYM73E.js → chunk-UCQ5MJP4.js} +2 -2
- package/dist/chunk-UCQ5MJP4.js.map +1 -0
- package/dist/index.d.ts +21 -23
- package/dist/index.js +563 -427
- package/dist/index.js.map +1 -1
- package/dist/onnx.d.ts +1 -1
- package/dist/spark.d.ts +1 -1
- package/dist/spark.js +1 -1
- package/dist/{types-Bu8Aki_j.d.ts → types-BP8iiYei.d.ts} +48 -5
- package/package.json +6 -5
- package/src/components/FlexRenderer.tsx +13 -14
- package/src/core/GenericIK.ts +247 -232
- package/src/core/MujocoCanvas.tsx +2 -0
- package/src/core/MujocoPhysics.tsx +3 -0
- package/src/core/MujocoProvider.tsx +23 -9
- package/src/core/MujocoSimProvider.tsx +31 -34
- package/src/core/SceneLoader.ts +63 -30
- package/src/core/engineState.ts +58 -0
- package/src/hooks/useActuators.ts +3 -13
- package/src/hooks/useControlGroup.ts +2 -2
- package/src/hooks/useCtrl.ts +7 -6
- package/src/hooks/useGamepad.ts +3 -3
- package/src/hooks/useKeyboardTeleop.ts +2 -2
- package/src/index.ts +2 -0
- package/src/rendering/flexTopology.ts +24 -0
- package/src/types.ts +42 -4
- package/dist/chunk-5WFYM73E.js.map +0 -1
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { captureSimulationState, restoreSimulationState } from './engineState';
|
|
7
|
+
import { getActuatorInfo, writeActuatorControl } from './SceneLoader';
|
|
8
|
+
import type { ActuatorControlValue, MujocoIntegrator } from '../types';
|
|
9
|
+
|
|
6
10
|
import { useFrame, useThree } from '@react-three/fiber';
|
|
7
11
|
import {
|
|
8
12
|
createContext,
|
|
@@ -544,6 +548,7 @@ interface MujocoSimProviderProps {
|
|
|
544
548
|
// Declarative physics config props
|
|
545
549
|
gravity?: [number, number, number];
|
|
546
550
|
timestep?: number;
|
|
551
|
+
integrator?: MujocoIntegrator;
|
|
547
552
|
substeps?: number;
|
|
548
553
|
paused?: boolean;
|
|
549
554
|
speed?: number;
|
|
@@ -563,6 +568,7 @@ export function MujocoSimProvider({
|
|
|
563
568
|
onSelection,
|
|
564
569
|
gravity,
|
|
565
570
|
timestep,
|
|
571
|
+
integrator,
|
|
566
572
|
substeps,
|
|
567
573
|
paused,
|
|
568
574
|
speed,
|
|
@@ -627,6 +633,16 @@ export function MujocoSimProvider({
|
|
|
627
633
|
model.opt.gravity[2] = gravity[2];
|
|
628
634
|
}, [gravity]);
|
|
629
635
|
|
|
636
|
+
const integratorRef = useRef(integrator);
|
|
637
|
+
integratorRef.current = integrator;
|
|
638
|
+
const applyIntegrator = (model: MujocoModel) => {
|
|
639
|
+
const value = integratorRef.current;
|
|
640
|
+
if (value !== undefined) model.opt.integrator = { Euler: 0, RK4: 1, implicit: 2, implicitfast: 3, discrete: 4 }[value];
|
|
641
|
+
};
|
|
642
|
+
useEffect(() => {
|
|
643
|
+
if (mjModelRef.current) applyIntegrator(mjModelRef.current);
|
|
644
|
+
}, [integrator]);
|
|
645
|
+
|
|
630
646
|
// Sync timestep prop
|
|
631
647
|
useEffect(() => {
|
|
632
648
|
if (timestep === undefined) return;
|
|
@@ -683,12 +699,13 @@ export function MujocoSimProvider({
|
|
|
683
699
|
try {
|
|
684
700
|
const result = await loadScene(mujoco, buildMergedConfig(config));
|
|
685
701
|
if (disposed) {
|
|
686
|
-
result.mjModel.delete();
|
|
687
702
|
result.mjData.delete();
|
|
703
|
+
result.mjModel.delete();
|
|
688
704
|
return;
|
|
689
705
|
}
|
|
690
706
|
|
|
691
707
|
mujocoRef.current = mujoco;
|
|
708
|
+
applyIntegrator(result.mjModel);
|
|
692
709
|
mjModelRef.current = result.mjModel;
|
|
693
710
|
mjDataRef.current = result.mjData;
|
|
694
711
|
physicsAccumulatorRef.current = 0;
|
|
@@ -717,8 +734,8 @@ export function MujocoSimProvider({
|
|
|
717
734
|
|
|
718
735
|
return () => {
|
|
719
736
|
disposed = true;
|
|
720
|
-
mjModelRef.current?.delete();
|
|
721
737
|
mjDataRef.current?.delete();
|
|
738
|
+
mjModelRef.current?.delete();
|
|
722
739
|
mjModelRef.current = null;
|
|
723
740
|
mjDataRef.current = null;
|
|
724
741
|
physicsAccumulatorRef.current = 0;
|
|
@@ -927,27 +944,16 @@ export function MujocoSimProvider({
|
|
|
927
944
|
const saveState = useCallback((): StateSnapshot => {
|
|
928
945
|
const data = mjDataRef.current;
|
|
929
946
|
if (!data) return { time: 0, qpos: new Float64Array(0), qvel: new Float64Array(0), ctrl: new Float64Array(0), act: new Float64Array(0), qfrc_applied: new Float64Array(0) };
|
|
930
|
-
return
|
|
931
|
-
time: data.time,
|
|
932
|
-
qpos: new Float64Array(data.qpos),
|
|
933
|
-
qvel: new Float64Array(data.qvel),
|
|
934
|
-
ctrl: new Float64Array(data.ctrl),
|
|
935
|
-
act: new Float64Array(data.act),
|
|
936
|
-
qfrc_applied: new Float64Array(data.qfrc_applied),
|
|
937
|
-
};
|
|
947
|
+
return captureSimulationState(mujocoRef.current, mjModelRef.current!, data);
|
|
938
948
|
}, []);
|
|
939
949
|
|
|
940
950
|
const restoreState = useCallback((snapshot: StateSnapshot) => {
|
|
941
951
|
const model = mjModelRef.current;
|
|
942
952
|
const data = mjDataRef.current;
|
|
943
953
|
if (!model || !data) return;
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
data.ctrl.set(snapshot.ctrl);
|
|
948
|
-
if (snapshot.act.length > 0) data.act.set(snapshot.act);
|
|
949
|
-
data.qfrc_applied.set(snapshot.qfrc_applied);
|
|
950
|
-
mujocoRef.current.mj_forward(model, data);
|
|
954
|
+
restoreSimulationState(mujocoRef.current, model, data, snapshot);
|
|
955
|
+
physicsAccumulatorRef.current = 0;
|
|
956
|
+
interpolationStateRef.current.valid = false;
|
|
951
957
|
}, [mujoco]);
|
|
952
958
|
|
|
953
959
|
const setQpos = useCallback((values: Float64Array | number[]) => {
|
|
@@ -974,18 +980,18 @@ export function MujocoSimProvider({
|
|
|
974
980
|
return mjDataRef.current ? new Float64Array(mjDataRef.current.qvel) : new Float64Array(0);
|
|
975
981
|
}, []);
|
|
976
982
|
|
|
977
|
-
const setCtrl = useCallback((nameOrValues: string | Record<string,
|
|
983
|
+
const setCtrl = useCallback((nameOrValues: string | Record<string, ActuatorControlValue>, value?: ActuatorControlValue) => {
|
|
978
984
|
const model = mjModelRef.current;
|
|
979
985
|
const data = mjDataRef.current;
|
|
980
986
|
if (!model || !data) return;
|
|
981
987
|
|
|
982
988
|
if (typeof nameOrValues === 'string') {
|
|
983
989
|
const id = findActuatorByName(model, nameOrValues);
|
|
984
|
-
if (
|
|
990
|
+
if (value !== undefined) writeActuatorControl(model, data, id, value);
|
|
985
991
|
} else {
|
|
986
992
|
for (const [name, val] of Object.entries(nameOrValues)) {
|
|
987
993
|
const id = findActuatorByName(model, name);
|
|
988
|
-
|
|
994
|
+
writeActuatorControl(model, data, id, val);
|
|
989
995
|
}
|
|
990
996
|
}
|
|
991
997
|
}, []);
|
|
@@ -1152,17 +1158,7 @@ export function MujocoSimProvider({
|
|
|
1152
1158
|
const getActuatorsApi = useCallback((): ActuatorInfo[] => {
|
|
1153
1159
|
const model = mjModelRef.current;
|
|
1154
1160
|
if (!model) return [];
|
|
1155
|
-
const result:
|
|
1156
|
-
for (let i = 0; i < model.nu; i++) {
|
|
1157
|
-
const hasRange = model.actuator_ctrlrange[2 * i] < model.actuator_ctrlrange[2 * i + 1];
|
|
1158
|
-
result.push({
|
|
1159
|
-
id: i,
|
|
1160
|
-
name: getName(model, model.name_actuatoradr[i]),
|
|
1161
|
-
range: hasRange
|
|
1162
|
-
? [model.actuator_ctrlrange[2 * i], model.actuator_ctrlrange[2 * i + 1]]
|
|
1163
|
-
: [-Infinity, Infinity],
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1161
|
+
const result = Array.from({ length: model.nactuator }, (_, id) => getActuatorInfo(model, id));
|
|
1166
1162
|
return result;
|
|
1167
1163
|
}, []);
|
|
1168
1164
|
|
|
@@ -1466,8 +1462,8 @@ export function MujocoSimProvider({
|
|
|
1466
1462
|
const loadSceneApi = useCallback(async (newConfig: SceneConfig): Promise<void> => {
|
|
1467
1463
|
const gen = ++loadGenRef.current;
|
|
1468
1464
|
try {
|
|
1469
|
-
mjModelRef.current?.delete();
|
|
1470
1465
|
mjDataRef.current?.delete();
|
|
1466
|
+
mjModelRef.current?.delete();
|
|
1471
1467
|
mjModelRef.current = null;
|
|
1472
1468
|
mjDataRef.current = null;
|
|
1473
1469
|
setStatus('loading');
|
|
@@ -1475,12 +1471,13 @@ export function MujocoSimProvider({
|
|
|
1475
1471
|
const result = await loadScene(mujoco, buildMergedConfig(newConfig));
|
|
1476
1472
|
|
|
1477
1473
|
if (gen !== loadGenRef.current) {
|
|
1478
|
-
result.mjModel.delete();
|
|
1479
1474
|
result.mjData.delete();
|
|
1475
|
+
result.mjModel.delete();
|
|
1480
1476
|
return;
|
|
1481
1477
|
}
|
|
1482
1478
|
|
|
1483
|
-
|
|
1479
|
+
applyIntegrator(result.mjModel);
|
|
1480
|
+
mjModelRef.current = result.mjModel;
|
|
1484
1481
|
mjDataRef.current = result.mjData;
|
|
1485
1482
|
physicsAccumulatorRef.current = 0;
|
|
1486
1483
|
interpolationStateRef.current.valid = false;
|
package/src/core/SceneLoader.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { initializeDataThreads } from './engineState';
|
|
7
|
+
|
|
6
8
|
import type {
|
|
7
9
|
ActuatedJointInfo,
|
|
8
10
|
ActuatorInfo,
|
|
@@ -54,7 +56,7 @@ export function findSiteByName(mjModel: MujocoModel, name: string): number {
|
|
|
54
56
|
* Find an actuator by name in the MuJoCo model. Returns -1 if not found.
|
|
55
57
|
*/
|
|
56
58
|
export function findActuatorByName(mjModel: MujocoModel, name: string): number {
|
|
57
|
-
for (let i = 0; i < mjModel.
|
|
59
|
+
for (let i = 0; i < mjModel.nactuator; i++) {
|
|
58
60
|
if (getName(mjModel, mjModel.name_actuatoradr[i]).includes(name)) return i;
|
|
59
61
|
}
|
|
60
62
|
return -1;
|
|
@@ -138,7 +140,7 @@ export function findTendonByName(mjModel: MujocoModel, name: string): number {
|
|
|
138
140
|
* Returns -1 for non-joint transmissions and multi-DOF joints.
|
|
139
141
|
*/
|
|
140
142
|
export function getActuatedScalarQposAdr(mjModel: MujocoModel, actuatorId: number): number {
|
|
141
|
-
if (actuatorId < 0 || actuatorId >= mjModel.
|
|
143
|
+
if (actuatorId < 0 || actuatorId >= mjModel.nactuator) return -1;
|
|
142
144
|
|
|
143
145
|
// mjTRN_JOINT=0, mjTRN_JOINTINPARENT=1. Other transmission types don't map ctrl to a single qpos.
|
|
144
146
|
const trnType = mjModel.actuator_trntype?.[actuatorId];
|
|
@@ -166,7 +168,7 @@ function isScalarJoint(mjModel: MujocoModel, jointId: number): boolean {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
function getActuatorJointId(mjModel: MujocoModel, actuatorId: number): number {
|
|
169
|
-
if (actuatorId < 0 || actuatorId >= mjModel.
|
|
171
|
+
if (actuatorId < 0 || actuatorId >= mjModel.nactuator) return -1;
|
|
170
172
|
const trnType = mjModel.actuator_trntype?.[actuatorId];
|
|
171
173
|
if (trnType !== undefined && trnType !== 0 && trnType !== 1) return -1;
|
|
172
174
|
const jointId = mjModel.actuator_trnid[2 * actuatorId];
|
|
@@ -189,15 +191,35 @@ function getJointInfo(mjModel: MujocoModel, jointId: number): JointInfo {
|
|
|
189
191
|
};
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
function getActuatorInfo(mjModel: MujocoModel, actuatorId: number): ActuatorInfo {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
export function getActuatorInfo(mjModel: MujocoModel, actuatorId: number): ActuatorInfo {
|
|
195
|
+
const ctrlAdr = mjModel.actuator_ctrladr[actuatorId];
|
|
196
|
+
const ctrlCount = mjModel.actuator_ctrlnum[actuatorId];
|
|
197
|
+
const ranges = Array.from({ length: ctrlCount }, (_, input): [number, number] => {
|
|
198
|
+
const adr = ctrlAdr + input;
|
|
199
|
+
const lo = mjModel.actuator_ctrlrange[2 * adr], hi = mjModel.actuator_ctrlrange[2 * adr + 1];
|
|
200
|
+
return lo < hi ? [lo, hi] : [-Infinity, Infinity];
|
|
201
|
+
});
|
|
202
|
+
return { id: actuatorId, name: getName(mjModel, mjModel.name_actuatoradr[actuatorId]),
|
|
203
|
+
ctrlAdr, ctrlCount, ranges, range: ranges[0] ?? [-Infinity, Infinity] };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Resolve one input; scalar-only callers reject ambiguous multi-input actuators. */
|
|
207
|
+
export function getActuatorControlAddress(model: MujocoModel, id: number, input?: number): number {
|
|
208
|
+
if (id < 0) return -1;
|
|
209
|
+
const count = model.actuator_ctrlnum[id];
|
|
210
|
+
if (input === undefined && count !== 1) throw new Error('This actuator requires an explicit input index or a complete control vector');
|
|
211
|
+
const index = input ?? 0;
|
|
212
|
+
if (!Number.isInteger(index) || index < 0 || index >= count) throw new Error('Actuator input index out of range');
|
|
213
|
+
return model.actuator_ctrladr[id] + index;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function writeActuatorControl(model: MujocoModel, data: MujocoData, id: number, value: number | readonly number[]): void {
|
|
217
|
+
if (id < 0) return;
|
|
218
|
+
const values = typeof value === 'number' ? [value] : value;
|
|
219
|
+
if (values.length !== model.actuator_ctrlnum[id] || !values.every(Number.isFinite)) {
|
|
220
|
+
throw new Error('Actuator control must contain one finite value per input');
|
|
221
|
+
}
|
|
222
|
+
data.ctrl.set(values, model.actuator_ctrladr[id]);
|
|
201
223
|
}
|
|
202
224
|
|
|
203
225
|
function includesResourceName(names: readonly string[], name: string): boolean {
|
|
@@ -251,7 +273,7 @@ function orderedActuatorIdsFromSelector(
|
|
|
251
273
|
.filter((id) => id >= 0 && getActuatorJointId(mjModel, id) >= 0);
|
|
252
274
|
}
|
|
253
275
|
const ids: number[] = [];
|
|
254
|
-
for (let i = 0; i < mjModel.
|
|
276
|
+
for (let i = 0; i < mjModel.nactuator; i++) {
|
|
255
277
|
if (getActuatorJointId(mjModel, i) < 0) continue;
|
|
256
278
|
const info = getActuatorInfo(mjModel, i);
|
|
257
279
|
if (matchesSelector(info, selector)) ids.push(i);
|
|
@@ -295,9 +317,9 @@ function unique(values: number[]): number[] {
|
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
function findActuatorForJoint(mjModel: MujocoModel, jointId: number, preferredActuatorIds?: number[]): number {
|
|
298
|
-
const search = preferredActuatorIds ?? Array.from({ length: mjModel.
|
|
320
|
+
const search = preferredActuatorIds ?? Array.from({ length: mjModel.nactuator }, (_, i) => i);
|
|
299
321
|
for (const actuatorId of search) {
|
|
300
|
-
if (getActuatorJointId(mjModel, actuatorId) === jointId) return actuatorId;
|
|
322
|
+
if (mjModel.actuator_ctrlnum[actuatorId] === 1 && getActuatorJointId(mjModel, actuatorId) === jointId) return actuatorId;
|
|
301
323
|
}
|
|
302
324
|
return -1;
|
|
303
325
|
}
|
|
@@ -325,12 +347,12 @@ function buildControlGroup(
|
|
|
325
347
|
if (actuatorId >= 0) {
|
|
326
348
|
const actuator = getActuatorInfo(mjModel, actuatorId);
|
|
327
349
|
actuators.push(actuator);
|
|
328
|
-
ctrlAdr.push(actuatorId);
|
|
350
|
+
ctrlAdr.push(getActuatorControlAddress(mjModel, actuatorId));
|
|
329
351
|
joints.push({
|
|
330
352
|
...joint,
|
|
331
353
|
actuatorId,
|
|
332
354
|
actuatorName: actuator.name,
|
|
333
|
-
ctrlAdr: actuatorId,
|
|
355
|
+
ctrlAdr: getActuatorControlAddress(mjModel, actuatorId),
|
|
334
356
|
ctrlRange: actuator.range,
|
|
335
357
|
});
|
|
336
358
|
} else {
|
|
@@ -372,15 +394,15 @@ function buildControlGroup(
|
|
|
372
394
|
|
|
373
395
|
export function getActuatedJoints(mjModel: MujocoModel): ActuatedJointInfo[] {
|
|
374
396
|
const result: ActuatedJointInfo[] = [];
|
|
375
|
-
for (let actuatorId = 0; actuatorId < mjModel.
|
|
397
|
+
for (let actuatorId = 0; actuatorId < mjModel.nactuator; actuatorId++) {
|
|
376
398
|
const jointId = getActuatorJointId(mjModel, actuatorId);
|
|
377
|
-
if (jointId < 0) continue;
|
|
399
|
+
if (jointId < 0 || mjModel.actuator_ctrlnum[actuatorId] !== 1) continue;
|
|
378
400
|
const actuator = getActuatorInfo(mjModel, actuatorId);
|
|
379
401
|
result.push({
|
|
380
402
|
...getJointInfo(mjModel, jointId),
|
|
381
403
|
actuatorId,
|
|
382
404
|
actuatorName: actuator.name,
|
|
383
|
-
ctrlAdr: actuatorId,
|
|
405
|
+
ctrlAdr: getActuatorControlAddress(mjModel, actuatorId),
|
|
384
406
|
ctrlRange: actuator.range,
|
|
385
407
|
});
|
|
386
408
|
}
|
|
@@ -388,8 +410,8 @@ export function getActuatedJoints(mjModel: MujocoModel): ActuatedJointInfo[] {
|
|
|
388
410
|
}
|
|
389
411
|
|
|
390
412
|
export function getControlMap(mjModel: MujocoModel): ControlGroupInfo {
|
|
391
|
-
const actuatorIds = Array.from({ length: mjModel.
|
|
392
|
-
.filter((id) => getActuatorJointId(mjModel, id) >= 0);
|
|
413
|
+
const actuatorIds = Array.from({ length: mjModel.nactuator }, (_, i) => i)
|
|
414
|
+
.filter((id) => mjModel.actuator_ctrlnum[id] === 1 && getActuatorJointId(mjModel, id) >= 0);
|
|
393
415
|
const jointIds = actuatorIds.map((id) => getActuatorJointId(mjModel, id));
|
|
394
416
|
return buildControlGroup(mjModel, jointIds, actuatorIds) ?? createContiguousControlGroup(mjModel, 0);
|
|
395
417
|
}
|
|
@@ -400,6 +422,7 @@ export function resolveControlGroup(
|
|
|
400
422
|
): ControlGroupInfo | null {
|
|
401
423
|
if (selector.actuators) {
|
|
402
424
|
const actuatorIds = orderedActuatorIdsFromSelector(mjModel, selector.actuators);
|
|
425
|
+
for (const id of actuatorIds) getActuatorControlAddress(mjModel, id);
|
|
403
426
|
const jointIds = actuatorIds.map((id) => getActuatorJointId(mjModel, id));
|
|
404
427
|
return buildControlGroup(mjModel, jointIds, actuatorIds);
|
|
405
428
|
}
|
|
@@ -422,6 +445,9 @@ export function resolveControlGroup(
|
|
|
422
445
|
}
|
|
423
446
|
|
|
424
447
|
export function createContiguousControlGroup(mjModel: MujocoModel, count: number): ControlGroupInfo {
|
|
448
|
+
if (count > 0 && (mjModel.nactuator !== mjModel.nu || Array.from(mjModel.actuator_ctrlnum).some((n) => n !== 1))) {
|
|
449
|
+
throw new Error('Legacy numJoints control groups require scalar-input actuators; use named controls');
|
|
450
|
+
}
|
|
425
451
|
const n = Math.max(0, Math.min(count, mjModel.nq, mjModel.nu));
|
|
426
452
|
const joints: ControlJointInfo[] = [];
|
|
427
453
|
const actuators: ActuatorInfo[] = [];
|
|
@@ -837,14 +863,18 @@ async function loadSceneFromFiles(
|
|
|
837
863
|
onProgress?.('Loading model...');
|
|
838
864
|
const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
|
|
839
865
|
const mjData = new mujoco.MjData(mjModel);
|
|
840
|
-
|
|
841
|
-
|
|
866
|
+
try {
|
|
867
|
+
initializeDataThreads(mujoco, mjData);
|
|
868
|
+
applyInitialPose(mjModel, mjData, config);
|
|
869
|
+
mujoco.mj_forward(mjModel, mjData);
|
|
842
870
|
|
|
843
|
-
|
|
871
|
+
return { mjModel, mjData };
|
|
872
|
+
} catch (error) { mjData.delete(); mjModel.delete(); throw error; }
|
|
844
873
|
}
|
|
845
874
|
|
|
846
875
|
function applyInitialPose(mjModel: MujocoModel, mjData: MujocoData, config: SceneConfig) {
|
|
847
876
|
if (!config.homeJoints) return;
|
|
877
|
+
if (config.homeJoints.length && (mjModel.nactuator !== mjModel.nu || Array.from(mjModel.actuator_ctrlnum).some((n) => n !== 1))) throw new Error('homeJoints requires scalar-input actuators; use a keyframe for multi-input models');
|
|
848
878
|
const homeCount = Math.min(config.homeJoints.length, Math.max(mjModel.nu, mjModel.nq));
|
|
849
879
|
for (let i = 0; i < homeCount; i++) {
|
|
850
880
|
if (i < mjModel.nu) mjData.ctrl[i] = config.homeJoints[i];
|
|
@@ -946,14 +976,17 @@ export async function loadScene(
|
|
|
946
976
|
onProgress?.('Loading model...');
|
|
947
977
|
const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
|
|
948
978
|
const mjData = new mujoco.MjData(mjModel);
|
|
979
|
+
try {
|
|
980
|
+
initializeDataThreads(mujoco, mjData);
|
|
949
981
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
982
|
+
// 6. Set initial pose — set both ctrl and qpos so robot starts at home.
|
|
983
|
+
// If homeJoints is not provided, keep raw MuJoCo defaults.
|
|
984
|
+
applyInitialPose(mjModel, mjData, config);
|
|
953
985
|
|
|
954
|
-
|
|
986
|
+
mujoco.mj_forward(mjModel, mjData);
|
|
955
987
|
|
|
956
|
-
|
|
988
|
+
return { mjModel, mjData };
|
|
989
|
+
} catch (error) { mjData.delete(); mjModel.delete(); throw error; }
|
|
957
990
|
}
|
|
958
991
|
|
|
959
992
|
/**
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { MujocoModule, MujocoModel, MujocoData, StateSnapshot } from '../types';
|
|
2
|
+
|
|
3
|
+
const threadCounts = new WeakMap<MujocoModule, number>();
|
|
4
|
+
export function configureModuleThreads(module: MujocoModule, count: number) {
|
|
5
|
+
threadCounts.set(module, count);
|
|
6
|
+
}
|
|
7
|
+
export function initializeDataThreads(module: MujocoModule, data: MujocoData) {
|
|
8
|
+
const count = threadCounts.get(module) ?? 0;
|
|
9
|
+
if (count > 0) module.mju_threadpool(data, count);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function captureSimulationState(
|
|
13
|
+
module: MujocoModule,
|
|
14
|
+
model: MujocoModel,
|
|
15
|
+
data: MujocoData,
|
|
16
|
+
): StateSnapshot {
|
|
17
|
+
const signature = module.mjtState.mjSTATE_INTEGRATION.value;
|
|
18
|
+
const buffer = new module.DoubleBuffer(module.mj_stateSize(model, signature));
|
|
19
|
+
try {
|
|
20
|
+
module.mj_getState(model, data, buffer, signature);
|
|
21
|
+
return {
|
|
22
|
+
time: data.time,
|
|
23
|
+
qpos: new Float64Array(data.qpos),
|
|
24
|
+
qvel: new Float64Array(data.qvel),
|
|
25
|
+
ctrl: new Float64Array(data.ctrl),
|
|
26
|
+
act: new Float64Array(data.act),
|
|
27
|
+
qfrc_applied: new Float64Array(data.qfrc_applied),
|
|
28
|
+
integration: { signature, values: new Float64Array(buffer.GetView()), model },
|
|
29
|
+
};
|
|
30
|
+
} finally {
|
|
31
|
+
buffer.delete();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function restoreSimulationState(
|
|
36
|
+
module: MujocoModule,
|
|
37
|
+
model: MujocoModel,
|
|
38
|
+
data: MujocoData,
|
|
39
|
+
snapshot: StateSnapshot,
|
|
40
|
+
) {
|
|
41
|
+
const integration = snapshot.integration;
|
|
42
|
+
if (
|
|
43
|
+
integration &&
|
|
44
|
+
(integration.model !== model ||
|
|
45
|
+
integration.values.length !== module.mj_stateSize(model, integration.signature))
|
|
46
|
+
) {
|
|
47
|
+
throw new Error('Snapshot belongs to a different model; capture a new snapshot after loading a scene');
|
|
48
|
+
}
|
|
49
|
+
for (const key of ['qpos', 'qvel', 'ctrl', 'act', 'qfrc_applied'] as const) {
|
|
50
|
+
if (snapshot[key].length !== data[key].length)
|
|
51
|
+
throw new Error(`Snapshot ${key} size does not match model`);
|
|
52
|
+
}
|
|
53
|
+
if (integration) module.mj_setState(model, data, Array.from(integration.values), integration.signature);
|
|
54
|
+
// Preserve editable legacy fields; the packed state supplies mocap/history/etc.
|
|
55
|
+
data.time = snapshot.time;
|
|
56
|
+
for (const key of ['qpos', 'qvel', 'ctrl', 'act', 'qfrc_applied'] as const) data[key].set(snapshot[key]);
|
|
57
|
+
module.mj_forward(model, data);
|
|
58
|
+
}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { useMemo } from 'react';
|
|
7
7
|
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
8
|
-
import {
|
|
8
|
+
import { getActuatorInfo } from '../core/SceneLoader';
|
|
9
9
|
import type { ActuatorInfo } from '../types';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Returns a stable array of actuator metadata for building control UIs.
|
|
13
|
-
* Computed once when the model loads.
|
|
13
|
+
* Computed once when the model loads. Use ctrlAdr and ctrlCount to address controls.
|
|
14
14
|
*/
|
|
15
15
|
export function useActuators(): ActuatorInfo[] {
|
|
16
16
|
const { mjModelRef, status } = useMujocoContext();
|
|
@@ -20,17 +20,7 @@ export function useActuators(): ActuatorInfo[] {
|
|
|
20
20
|
const model = mjModelRef.current;
|
|
21
21
|
if (!model) return [];
|
|
22
22
|
|
|
23
|
-
const actuators:
|
|
24
|
-
for (let i = 0; i < model.nu; i++) {
|
|
25
|
-
const name = getName(model, model.name_actuatoradr[i]);
|
|
26
|
-
const lo = model.actuator_ctrlrange[i * 2];
|
|
27
|
-
const hi = model.actuator_ctrlrange[i * 2 + 1];
|
|
28
|
-
const hasRange = lo < hi;
|
|
29
|
-
const range: [number, number] = hasRange
|
|
30
|
-
? [lo, hi]
|
|
31
|
-
: [-Infinity, Infinity];
|
|
32
|
-
actuators.push({ id: i, name, range });
|
|
33
|
-
}
|
|
23
|
+
const actuators = Array.from({ length: model.nactuator }, (_, id) => getActuatorInfo(model, id));
|
|
34
24
|
return actuators;
|
|
35
25
|
}, [status, mjModelRef]);
|
|
36
26
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { useCallback, useEffect, useId, useMemo, useRef } from 'react';
|
|
11
|
-
import { findActuatorByName } from '../core/SceneLoader';
|
|
11
|
+
import { findActuatorByName, getActuatorControlAddress } from '../core/SceneLoader';
|
|
12
12
|
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
13
13
|
import { useControlWriter, type ControlWriterOptions } from './useControlWriter';
|
|
14
14
|
import type { Actuators } from '../types';
|
|
@@ -150,7 +150,7 @@ export function useControlGroup<K extends Actuators>(
|
|
|
150
150
|
if (id < 0) {
|
|
151
151
|
console.warn(`[mujoco-react] useControlGroup: actuator "${name}" was not found.`);
|
|
152
152
|
}
|
|
153
|
-
return id;
|
|
153
|
+
return getActuatorControlAddress(model, id);
|
|
154
154
|
});
|
|
155
155
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
156
156
|
}, [mjModelRef, status, namesKey]);
|
package/src/hooks/useCtrl.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { useEffect, useRef, useMemo } from 'react';
|
|
9
9
|
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
10
|
-
import { findActuatorByName } from '../core/SceneLoader';
|
|
10
|
+
import { findActuatorByName, getActuatorControlAddress } from '../core/SceneLoader';
|
|
11
11
|
import type { Actuators, CtrlHandle } from '../types';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -16,7 +16,7 @@ import type { Actuators, CtrlHandle } from '../types';
|
|
|
16
16
|
* Returns a `CtrlHandle` with `read()` and `write()` methods that
|
|
17
17
|
* operate directly on `data.ctrl` without causing React re-renders.
|
|
18
18
|
*/
|
|
19
|
-
export function useCtrl(name: Actuators): CtrlHandle {
|
|
19
|
+
export function useCtrl(name: Actuators, input?: number): CtrlHandle {
|
|
20
20
|
const { mjModelRef, mjDataRef, status } = useMujocoContext();
|
|
21
21
|
const actuatorIdRef = useRef(-1);
|
|
22
22
|
const rangeRef = useRef<[number, number]>([0, 0]);
|
|
@@ -25,14 +25,15 @@ export function useCtrl(name: Actuators): CtrlHandle {
|
|
|
25
25
|
const model = mjModelRef.current;
|
|
26
26
|
if (!model || status !== 'ready') return;
|
|
27
27
|
const id = findActuatorByName(model, name);
|
|
28
|
-
|
|
28
|
+
const adr = getActuatorControlAddress(model, id, input);
|
|
29
|
+
actuatorIdRef.current = adr;
|
|
29
30
|
if (id >= 0) {
|
|
30
31
|
rangeRef.current = [
|
|
31
|
-
model.actuator_ctrlrange[
|
|
32
|
-
model.actuator_ctrlrange[
|
|
32
|
+
model.actuator_ctrlrange[adr * 2],
|
|
33
|
+
model.actuator_ctrlrange[adr * 2 + 1],
|
|
33
34
|
];
|
|
34
35
|
}
|
|
35
|
-
}, [name, status, mjModelRef]);
|
|
36
|
+
}, [name, input, status, mjModelRef]);
|
|
36
37
|
|
|
37
38
|
return useMemo<CtrlHandle>(() => ({
|
|
38
39
|
read() {
|
package/src/hooks/useGamepad.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { useEffect, useRef } from 'react';
|
|
9
9
|
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
10
|
-
import { findActuatorByName } from '../core/SceneLoader';
|
|
10
|
+
import { findActuatorByName, getActuatorControlAddress } from '../core/SceneLoader';
|
|
11
11
|
|
|
12
12
|
interface GamepadConfig {
|
|
13
13
|
/** Map gamepad axis index to actuator name. */
|
|
@@ -43,10 +43,10 @@ export function useGamepad(config: GamepadConfig) {
|
|
|
43
43
|
axisCacheRef.current.clear();
|
|
44
44
|
buttonCacheRef.current.clear();
|
|
45
45
|
for (const [idx, name] of Object.entries(config.axes ?? {})) {
|
|
46
|
-
axisCacheRef.current.set(Number(idx), findActuatorByName(model, name));
|
|
46
|
+
axisCacheRef.current.set(Number(idx), getActuatorControlAddress(model, findActuatorByName(model, name)));
|
|
47
47
|
}
|
|
48
48
|
for (const [idx, name] of Object.entries(config.buttons ?? {})) {
|
|
49
|
-
buttonCacheRef.current.set(Number(idx), findActuatorByName(model, name));
|
|
49
|
+
buttonCacheRef.current.set(Number(idx), getActuatorControlAddress(model, findActuatorByName(model, name)));
|
|
50
50
|
}
|
|
51
51
|
}, [config.axes, config.buttons, status, mjModelRef]);
|
|
52
52
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { useEffect, useRef } from 'react';
|
|
9
9
|
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
10
|
-
import { findActuatorByName } from '../core/SceneLoader';
|
|
10
|
+
import { findActuatorByName, getActuatorControlAddress } from '../core/SceneLoader';
|
|
11
11
|
import type { KeyboardTeleopConfig } from '../types';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -37,7 +37,7 @@ export function useKeyboardTeleop(config: KeyboardTeleopConfig) {
|
|
|
37
37
|
const cache = new Map<string, number>();
|
|
38
38
|
for (const binding of Object.values(config.bindings)) {
|
|
39
39
|
if (!cache.has(binding.actuator)) {
|
|
40
|
-
cache.set(binding.actuator, findActuatorByName(model, binding.actuator));
|
|
40
|
+
cache.set(binding.actuator, getActuatorControlAddress(model, findActuatorByName(model, binding.actuator)));
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
actuatorCacheRef.current = cache;
|
package/src/index.ts
CHANGED
|
@@ -313,6 +313,7 @@ export type {
|
|
|
313
313
|
GeomInfo,
|
|
314
314
|
SiteInfo,
|
|
315
315
|
ActuatorInfo,
|
|
316
|
+
ActuatorControlValue,
|
|
316
317
|
SensorInfo,
|
|
317
318
|
CameraInfo,
|
|
318
319
|
// Contacts
|
|
@@ -324,6 +325,7 @@ export type {
|
|
|
324
325
|
ImagePointProjectionResult,
|
|
325
326
|
// Model options
|
|
326
327
|
ModelOptions,
|
|
328
|
+
MujocoIntegrator,
|
|
327
329
|
// Trajectory
|
|
328
330
|
TrajectoryFrame,
|
|
329
331
|
TrajectoryData,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MujocoModel } from '../types';
|
|
2
|
+
|
|
3
|
+
/** MuJoCo flex topology uses vertex indices local to each flex. */
|
|
4
|
+
export function getFlexTopology(
|
|
5
|
+
model: MujocoModel,
|
|
6
|
+
id: number,
|
|
7
|
+
): { kind: 'mesh' | 'lines' | 'points'; indices: number[] } {
|
|
8
|
+
const dim = model.flex_dim[id];
|
|
9
|
+
if (dim === 3) {
|
|
10
|
+
const start = model.flex_shelldataadr[id];
|
|
11
|
+
return {
|
|
12
|
+
kind: 'mesh',
|
|
13
|
+
indices: Array.from(model.flex_shell.subarray(start, start + 3 * model.flex_shellnum[id])),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (dim === 1 || dim === 2) {
|
|
17
|
+
const start = model.flex_elemdataadr[id];
|
|
18
|
+
return {
|
|
19
|
+
kind: dim === 2 ? 'mesh' : 'lines',
|
|
20
|
+
indices: Array.from(model.flex_elem.subarray(start, start + (dim + 1) * model.flex_elemnum[id])),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { kind: 'points', indices: [] };
|
|
24
|
+
}
|