mujoco-react 0.2.0 → 0.3.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 +206 -42
- package/dist/index.d.ts +175 -95
- package/dist/index.js +1137 -771
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ContactMarkers.tsx +12 -19
- package/src/components/Debug.tsx +168 -33
- package/src/components/DragInteraction.tsx +1 -1
- package/src/components/IkController.tsx +262 -0
- package/src/components/IkGizmo.tsx +17 -25
- package/src/components/SceneLights.tsx +2 -112
- package/src/components/SceneRenderer.tsx +8 -6
- package/src/components/SelectionHighlight.tsx +2 -49
- package/src/components/TendonRenderer.tsx +90 -26
- package/src/components/TrajectoryPlayer.tsx +14 -10
- package/src/core/IkContext.tsx +40 -0
- package/src/core/MujocoProvider.tsx +12 -4
- package/src/core/MujocoSimProvider.tsx +55 -331
- package/src/core/SceneLoader.ts +44 -11
- package/src/core/createController.tsx +91 -0
- package/src/hooks/useCameraAnimation.ts +102 -0
- package/src/hooks/useContacts.ts +52 -22
- package/src/hooks/useJointState.ts +18 -2
- package/src/hooks/useSceneLights.ts +117 -0
- package/src/hooks/useSelectionHighlight.ts +65 -0
- package/src/index.ts +16 -1
- package/src/types.ts +59 -22
|
@@ -14,14 +14,12 @@ import {
|
|
|
14
14
|
useState,
|
|
15
15
|
} from 'react';
|
|
16
16
|
import * as THREE from 'three';
|
|
17
|
-
import { MujocoData, MujocoModel, MujocoModule } from '../types';
|
|
18
|
-
import { GenericIK } from './GenericIK';
|
|
17
|
+
import { MujocoData, MujocoModel, MujocoModule, getContact } from '../types';
|
|
19
18
|
import {
|
|
20
19
|
ActuatorInfo,
|
|
21
20
|
BodyInfo,
|
|
22
21
|
ContactInfo,
|
|
23
22
|
GeomInfo,
|
|
24
|
-
IKSolveFn,
|
|
25
23
|
JointInfo,
|
|
26
24
|
ModelOptions,
|
|
27
25
|
MujocoSimAPI,
|
|
@@ -39,6 +37,7 @@ import {
|
|
|
39
37
|
findGeomByName,
|
|
40
38
|
findSensorByName,
|
|
41
39
|
findActuatorByName,
|
|
40
|
+
getActuatedScalarQposAdr,
|
|
42
41
|
getName,
|
|
43
42
|
} from './SceneLoader';
|
|
44
43
|
|
|
@@ -47,7 +46,6 @@ const JOINT_TYPE_NAMES = ['free', 'ball', 'slide', 'hinge'];
|
|
|
47
46
|
// ---- Geom type names ----
|
|
48
47
|
const GEOM_TYPE_NAMES = ['plane', 'hfield', 'sphere', 'capsule', 'ellipsoid', 'cylinder', 'box', 'mesh'];
|
|
49
48
|
// ---- Sensor type names (subset — MuJoCo has many) ----
|
|
50
|
-
// Sensor type names matching mjtSensor enum in mujoco WASM (mujoco-js 0.0.7)
|
|
51
49
|
const SENSOR_TYPE_NAMES: Record<number, string> = {
|
|
52
50
|
0: 'touch', 1: 'accelerometer', 2: 'velocimeter', 3: 'gyro',
|
|
53
51
|
4: 'force', 5: 'torque', 6: 'magnetometer', 7: 'rangefinder',
|
|
@@ -72,6 +70,8 @@ const _applyPoint = new Float64Array(3);
|
|
|
72
70
|
const _rayPnt = new Float64Array(3);
|
|
73
71
|
const _rayVec = new Float64Array(3);
|
|
74
72
|
const _rayGeomId = new Int32Array(1);
|
|
73
|
+
const _projRaycaster = new THREE.Raycaster();
|
|
74
|
+
const _projNdc = new THREE.Vector2();
|
|
75
75
|
|
|
76
76
|
// ---- Internal context types ----
|
|
77
77
|
|
|
@@ -81,43 +81,15 @@ export interface MujocoSimContextValue {
|
|
|
81
81
|
mjDataRef: React.RefObject<MujocoData | null>;
|
|
82
82
|
mujocoRef: React.RefObject<MujocoModule>;
|
|
83
83
|
configRef: React.RefObject<SceneConfig>;
|
|
84
|
-
siteIdRef: React.RefObject<number>;
|
|
85
|
-
gripperIdRef: React.RefObject<number>;
|
|
86
|
-
ikEnabledRef: React.RefObject<boolean>;
|
|
87
|
-
ikCalculatingRef: React.RefObject<boolean>;
|
|
88
84
|
pausedRef: React.RefObject<boolean>;
|
|
89
85
|
speedRef: React.RefObject<number>;
|
|
90
86
|
substepsRef: React.RefObject<number>;
|
|
91
|
-
ikTargetRef: React.RefObject<THREE.Group>;
|
|
92
|
-
genericIkRef: React.RefObject<GenericIK>;
|
|
93
|
-
ikSolveFnRef: React.RefObject<IKSolveFn>;
|
|
94
|
-
firstIkEnableRef: React.RefObject<boolean>;
|
|
95
|
-
gizmoAnimRef: React.RefObject<{
|
|
96
|
-
active: boolean;
|
|
97
|
-
startPos: THREE.Vector3;
|
|
98
|
-
endPos: THREE.Vector3;
|
|
99
|
-
startRot: THREE.Quaternion;
|
|
100
|
-
endRot: THREE.Quaternion;
|
|
101
|
-
startTime: number;
|
|
102
|
-
duration: number;
|
|
103
|
-
}>;
|
|
104
|
-
cameraAnimRef: React.RefObject<{
|
|
105
|
-
active: boolean;
|
|
106
|
-
startPos: THREE.Vector3;
|
|
107
|
-
endPos: THREE.Vector3;
|
|
108
|
-
startRot: THREE.Quaternion;
|
|
109
|
-
endRot: THREE.Quaternion;
|
|
110
|
-
startTarget: THREE.Vector3;
|
|
111
|
-
endTarget: THREE.Vector3;
|
|
112
|
-
startTime: number;
|
|
113
|
-
duration: number;
|
|
114
|
-
resolve: (() => void) | null;
|
|
115
|
-
}>;
|
|
116
87
|
onSelectionRef: React.RefObject<
|
|
117
88
|
((bodyId: number, name: string) => void) | undefined
|
|
118
89
|
>;
|
|
119
90
|
beforeStepCallbacks: React.RefObject<Set<PhysicsStepCallback>>;
|
|
120
91
|
afterStepCallbacks: React.RefObject<Set<PhysicsStepCallback>>;
|
|
92
|
+
resetCallbacks: React.RefObject<Set<() => void>>;
|
|
121
93
|
status: 'loading' | 'ready' | 'error';
|
|
122
94
|
}
|
|
123
95
|
|
|
@@ -162,7 +134,7 @@ interface MujocoSimProviderProps {
|
|
|
162
134
|
onError?: (error: Error) => void;
|
|
163
135
|
onStep?: (time: number) => void;
|
|
164
136
|
onSelection?: (bodyId: number, name: string) => void;
|
|
165
|
-
// Declarative physics config props
|
|
137
|
+
// Declarative physics config props
|
|
166
138
|
gravity?: [number, number, number];
|
|
167
139
|
timestep?: number;
|
|
168
140
|
substeps?: number;
|
|
@@ -196,18 +168,14 @@ export function MujocoSimProvider({
|
|
|
196
168
|
const mjDataRef = useRef<MujocoData | null>(null);
|
|
197
169
|
const mujocoRef = useRef<MujocoModule>(mujoco);
|
|
198
170
|
const configRef = useRef<SceneConfig>(config);
|
|
199
|
-
const siteIdRef = useRef(-1);
|
|
200
|
-
const gripperIdRef = useRef(-1);
|
|
201
|
-
const ikEnabledRef = useRef(false);
|
|
202
|
-
const ikCalculatingRef = useRef(false);
|
|
203
171
|
const pausedRef = useRef(paused ?? false);
|
|
204
172
|
const speedRef = useRef(speed ?? 1);
|
|
205
173
|
const substepsRef = useRef(substeps ?? 1);
|
|
206
174
|
const interpolateRef = useRef(interpolate ?? false);
|
|
207
|
-
const
|
|
208
|
-
const
|
|
175
|
+
const stepsToRunRef = useRef(0);
|
|
176
|
+
const loadGenRef = useRef(0);
|
|
209
177
|
|
|
210
|
-
// Interpolation state
|
|
178
|
+
// Interpolation state
|
|
211
179
|
const prevXposRef = useRef<Float64Array | null>(null);
|
|
212
180
|
const prevXquatRef = useRef<Float64Array | null>(null);
|
|
213
181
|
const interpAlphaRef = useRef(0);
|
|
@@ -219,6 +187,7 @@ export function MujocoSimProvider({
|
|
|
219
187
|
|
|
220
188
|
const beforeStepCallbacks = useRef(new Set<PhysicsStepCallback>());
|
|
221
189
|
const afterStepCallbacks = useRef(new Set<PhysicsStepCallback>());
|
|
190
|
+
const resetCallbacks = useRef(new Set<() => void>());
|
|
222
191
|
|
|
223
192
|
configRef.current = config;
|
|
224
193
|
|
|
@@ -228,7 +197,7 @@ export function MujocoSimProvider({
|
|
|
228
197
|
useEffect(() => { substepsRef.current = substeps ?? 1; }, [substeps]);
|
|
229
198
|
useEffect(() => { interpolateRef.current = interpolate ?? false; }, [interpolate]);
|
|
230
199
|
|
|
231
|
-
// Sync gravity prop
|
|
200
|
+
// Sync gravity prop
|
|
232
201
|
useEffect(() => {
|
|
233
202
|
if (!gravity) return;
|
|
234
203
|
const model = mjModelRef.current;
|
|
@@ -238,7 +207,7 @@ export function MujocoSimProvider({
|
|
|
238
207
|
model.opt.gravity[2] = gravity[2];
|
|
239
208
|
}, [gravity]);
|
|
240
209
|
|
|
241
|
-
// Sync timestep prop
|
|
210
|
+
// Sync timestep prop
|
|
242
211
|
useEffect(() => {
|
|
243
212
|
if (timestep === undefined) return;
|
|
244
213
|
const model = mjModelRef.current;
|
|
@@ -246,66 +215,6 @@ export function MujocoSimProvider({
|
|
|
246
215
|
model.opt.timestep = timestep;
|
|
247
216
|
}, [timestep]);
|
|
248
217
|
|
|
249
|
-
const ikTargetRef = useRef<THREE.Group>(new THREE.Group());
|
|
250
|
-
const genericIkRef = useRef<GenericIK>(new GenericIK(mujoco));
|
|
251
|
-
|
|
252
|
-
const gizmoAnimRef = useRef({
|
|
253
|
-
active: false,
|
|
254
|
-
startPos: new THREE.Vector3(),
|
|
255
|
-
endPos: new THREE.Vector3(),
|
|
256
|
-
startRot: new THREE.Quaternion(),
|
|
257
|
-
endRot: new THREE.Quaternion(),
|
|
258
|
-
startTime: 0,
|
|
259
|
-
duration: 1000,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const cameraAnimRef = useRef({
|
|
263
|
-
active: false,
|
|
264
|
-
startPos: new THREE.Vector3(),
|
|
265
|
-
endPos: new THREE.Vector3(),
|
|
266
|
-
startRot: new THREE.Quaternion(),
|
|
267
|
-
endRot: new THREE.Quaternion(),
|
|
268
|
-
startTarget: new THREE.Vector3(),
|
|
269
|
-
endTarget: new THREE.Vector3(),
|
|
270
|
-
startTime: 0,
|
|
271
|
-
duration: 0,
|
|
272
|
-
resolve: null as (() => void) | null,
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const orbitTargetRef = useRef(new THREE.Vector3(0, 0, 0));
|
|
276
|
-
|
|
277
|
-
// --- Helper: sync gizmo to actual MuJoCo site position ---
|
|
278
|
-
const syncGizmoToSite = useCallback((data: MujocoData, siteId: number, target: THREE.Group) => {
|
|
279
|
-
if (siteId === -1) return;
|
|
280
|
-
const sitePos = data.site_xpos.subarray(siteId * 3, siteId * 3 + 3);
|
|
281
|
-
const siteMat = data.site_xmat.subarray(siteId * 9, siteId * 9 + 9);
|
|
282
|
-
target.position.set(sitePos[0], sitePos[1], sitePos[2]);
|
|
283
|
-
const m = new THREE.Matrix4().set(
|
|
284
|
-
siteMat[0], siteMat[1], siteMat[2], 0,
|
|
285
|
-
siteMat[3], siteMat[4], siteMat[5], 0,
|
|
286
|
-
siteMat[6], siteMat[7], siteMat[8], 0,
|
|
287
|
-
0, 0, 0, 1
|
|
288
|
-
);
|
|
289
|
-
target.quaternion.setFromRotationMatrix(m);
|
|
290
|
-
}, []);
|
|
291
|
-
|
|
292
|
-
// IK solve function
|
|
293
|
-
const ikSolveFn = useCallback(
|
|
294
|
-
(pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]): number[] | null => {
|
|
295
|
-
const model = mjModelRef.current;
|
|
296
|
-
const data = mjDataRef.current;
|
|
297
|
-
if (!model || !data || siteIdRef.current === -1) return null;
|
|
298
|
-
return genericIkRef.current.solve(
|
|
299
|
-
model, data, siteIdRef.current,
|
|
300
|
-
configRef.current.numArmJoints ?? 7,
|
|
301
|
-
pos, quat, currentQ
|
|
302
|
-
);
|
|
303
|
-
},
|
|
304
|
-
[]
|
|
305
|
-
);
|
|
306
|
-
const ikSolveFnRef = useRef<IKSolveFn>(ikSolveFn);
|
|
307
|
-
ikSolveFnRef.current = ikSolveFn;
|
|
308
|
-
|
|
309
218
|
// --- Load scene on mount ---
|
|
310
219
|
useEffect(() => {
|
|
311
220
|
let disposed = false;
|
|
@@ -321,8 +230,6 @@ export function MujocoSimProvider({
|
|
|
321
230
|
|
|
322
231
|
mjModelRef.current = result.mjModel;
|
|
323
232
|
mjDataRef.current = result.mjData;
|
|
324
|
-
siteIdRef.current = result.siteId;
|
|
325
|
-
gripperIdRef.current = result.gripperId;
|
|
326
233
|
|
|
327
234
|
// Apply declarative physics props after load
|
|
328
235
|
if (gravity && result.mjModel.opt?.gravity) {
|
|
@@ -334,10 +241,6 @@ export function MujocoSimProvider({
|
|
|
334
241
|
result.mjModel.opt.timestep = timestep;
|
|
335
242
|
}
|
|
336
243
|
|
|
337
|
-
if (ikTargetRef.current) {
|
|
338
|
-
syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
244
|
setStatus('ready');
|
|
342
245
|
} catch (e: unknown) {
|
|
343
246
|
if (!disposed) {
|
|
@@ -374,51 +277,12 @@ export function MujocoSimProvider({
|
|
|
374
277
|
}, [status]);
|
|
375
278
|
|
|
376
279
|
// --- Physics step (priority -1) ---
|
|
377
|
-
useFrame((
|
|
280
|
+
useFrame(() => {
|
|
378
281
|
const model = mjModelRef.current;
|
|
379
282
|
const data = mjDataRef.current;
|
|
380
283
|
if (!model || !data) return;
|
|
381
284
|
|
|
382
|
-
//
|
|
383
|
-
const ga = gizmoAnimRef.current;
|
|
384
|
-
const target = ikTargetRef.current;
|
|
385
|
-
if (ga.active && target) {
|
|
386
|
-
const now = performance.now();
|
|
387
|
-
const elapsed = now - ga.startTime;
|
|
388
|
-
const t = Math.min(elapsed / ga.duration, 1.0);
|
|
389
|
-
const ease = 1 - Math.pow(1 - t, 3);
|
|
390
|
-
target.position.lerpVectors(ga.startPos, ga.endPos, ease);
|
|
391
|
-
target.quaternion.slerpQuaternions(ga.startRot, ga.endRot, ease);
|
|
392
|
-
if (t >= 1.0) ga.active = false;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Camera animation
|
|
396
|
-
const ca = cameraAnimRef.current;
|
|
397
|
-
if (ca.active) {
|
|
398
|
-
const now = performance.now();
|
|
399
|
-
const progress = Math.min((now - ca.startTime) / ca.duration, 1.0);
|
|
400
|
-
const ease =
|
|
401
|
-
progress < 0.5
|
|
402
|
-
? 4 * progress * progress * progress
|
|
403
|
-
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
|
404
|
-
camera.position.lerpVectors(ca.startPos, ca.endPos, ease);
|
|
405
|
-
camera.quaternion.slerpQuaternions(ca.startRot, ca.endRot, ease);
|
|
406
|
-
orbitTargetRef.current.lerpVectors(ca.startTarget, ca.endTarget, ease);
|
|
407
|
-
const orbitControls = state.controls as { target?: THREE.Vector3 };
|
|
408
|
-
if (orbitControls?.target) {
|
|
409
|
-
orbitControls.target.copy(orbitTargetRef.current);
|
|
410
|
-
}
|
|
411
|
-
if (progress >= 1.0) {
|
|
412
|
-
ca.active = false;
|
|
413
|
-
camera.position.copy(ca.endPos);
|
|
414
|
-
camera.quaternion.copy(ca.endRot);
|
|
415
|
-
orbitTargetRef.current.copy(ca.endTarget);
|
|
416
|
-
ca.resolve?.();
|
|
417
|
-
ca.resolve = null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Check single-step mode (spec 1.2)
|
|
285
|
+
// Check single-step mode
|
|
422
286
|
const shouldStep = !pausedRef.current || stepsToRunRef.current > 0;
|
|
423
287
|
if (!shouldStep) return;
|
|
424
288
|
|
|
@@ -432,24 +296,9 @@ export function MujocoSimProvider({
|
|
|
432
296
|
cb(model, data);
|
|
433
297
|
}
|
|
434
298
|
|
|
435
|
-
//
|
|
436
|
-
if (ikEnabledRef.current && target) {
|
|
437
|
-
ikCalculatingRef.current = true;
|
|
438
|
-
const numArm = configRef.current.numArmJoints ?? 7;
|
|
439
|
-
const currentQ: number[] = [];
|
|
440
|
-
for (let i = 0; i < numArm; i++) currentQ.push(data.qpos[i]);
|
|
441
|
-
const solution = ikSolveFnRef.current(target.position, target.quaternion, currentQ);
|
|
442
|
-
if (solution) {
|
|
443
|
-
for (let i = 0; i < numArm; i++) data.ctrl[i] = solution[i];
|
|
444
|
-
}
|
|
445
|
-
} else {
|
|
446
|
-
ikCalculatingRef.current = false;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Step physics with substeps (spec 1.1)
|
|
299
|
+
// Step physics with substeps
|
|
450
300
|
const numSubsteps = substepsRef.current;
|
|
451
301
|
if (stepsToRunRef.current > 0) {
|
|
452
|
-
// Single-step mode (spec 1.2)
|
|
453
302
|
for (let s = 0; s < stepsToRunRef.current; s++) {
|
|
454
303
|
mujoco.mj_step(model, data);
|
|
455
304
|
}
|
|
@@ -479,19 +328,16 @@ export function MujocoSimProvider({
|
|
|
479
328
|
const data = mjDataRef.current;
|
|
480
329
|
if (!model || !data) return;
|
|
481
330
|
|
|
482
|
-
gizmoAnimRef.current.active = false;
|
|
483
331
|
mujoco.mj_resetData(model, data);
|
|
484
332
|
|
|
485
333
|
const homeJoints = configRef.current.homeJoints;
|
|
486
334
|
if (homeJoints) {
|
|
487
|
-
|
|
335
|
+
const homeCount = Math.min(homeJoints.length, model.nu);
|
|
336
|
+
for (let i = 0; i < homeCount; i++) {
|
|
488
337
|
data.ctrl[i] = homeJoints[i];
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const qposAdr = model.jnt_qposadr[jointId];
|
|
493
|
-
data.qpos[qposAdr] = homeJoints[i];
|
|
494
|
-
}
|
|
338
|
+
const qposAdr = getActuatedScalarQposAdr(model, i);
|
|
339
|
+
if (qposAdr !== -1) {
|
|
340
|
+
data.qpos[qposAdr] = homeJoints[i];
|
|
495
341
|
}
|
|
496
342
|
}
|
|
497
343
|
}
|
|
@@ -499,61 +345,11 @@ export function MujocoSimProvider({
|
|
|
499
345
|
configRef.current.onReset?.(model, data);
|
|
500
346
|
mujoco.mj_forward(model, data);
|
|
501
347
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
firstIkEnableRef.current = true;
|
|
506
|
-
ikEnabledRef.current = false;
|
|
507
|
-
}, [mujoco, syncGizmoToSite]);
|
|
508
|
-
|
|
509
|
-
const setIkEnabled = useCallback((enabled: boolean) => {
|
|
510
|
-
ikEnabledRef.current = enabled;
|
|
511
|
-
const data = mjDataRef.current;
|
|
512
|
-
if (enabled && data && !gizmoAnimRef.current.active && ikTargetRef.current) {
|
|
513
|
-
syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
|
|
514
|
-
firstIkEnableRef.current = false;
|
|
348
|
+
// Notify composable plugins (e.g. IkController)
|
|
349
|
+
for (const cb of resetCallbacks.current) {
|
|
350
|
+
cb();
|
|
515
351
|
}
|
|
516
|
-
}, [
|
|
517
|
-
|
|
518
|
-
const syncTargetToSite = useCallback(() => {
|
|
519
|
-
const data = mjDataRef.current;
|
|
520
|
-
const target = ikTargetRef.current;
|
|
521
|
-
if (data && target) syncGizmoToSite(data, siteIdRef.current, target);
|
|
522
|
-
}, [syncGizmoToSite]);
|
|
523
|
-
|
|
524
|
-
const solveIK = useCallback(
|
|
525
|
-
(pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]): number[] | null => {
|
|
526
|
-
return ikSolveFnRef.current(pos, quat, currentQ);
|
|
527
|
-
},
|
|
528
|
-
[]
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
const moveTarget = useCallback(
|
|
532
|
-
(pos: THREE.Vector3, duration = 0) => {
|
|
533
|
-
if (!ikEnabledRef.current) setIkEnabled(true);
|
|
534
|
-
const target = ikTargetRef.current;
|
|
535
|
-
if (!target) return;
|
|
536
|
-
|
|
537
|
-
const targetPos = pos.clone();
|
|
538
|
-
const targetRot = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI, 0, 0));
|
|
539
|
-
|
|
540
|
-
if (duration > 0) {
|
|
541
|
-
const ga = gizmoAnimRef.current;
|
|
542
|
-
ga.active = true;
|
|
543
|
-
ga.startPos.copy(target.position);
|
|
544
|
-
ga.endPos.copy(targetPos);
|
|
545
|
-
ga.startRot.copy(target.quaternion);
|
|
546
|
-
ga.endRot.copy(targetRot);
|
|
547
|
-
ga.startTime = performance.now();
|
|
548
|
-
ga.duration = duration;
|
|
549
|
-
} else {
|
|
550
|
-
gizmoAnimRef.current.active = false;
|
|
551
|
-
target.position.copy(targetPos);
|
|
552
|
-
target.quaternion.copy(targetRot);
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
|
-
[setIkEnabled]
|
|
556
|
-
);
|
|
352
|
+
}, [mujoco]);
|
|
557
353
|
|
|
558
354
|
const setSpeed = useCallback((multiplier: number) => {
|
|
559
355
|
speedRef.current = multiplier;
|
|
@@ -564,17 +360,14 @@ export function MujocoSimProvider({
|
|
|
564
360
|
return pausedRef.current;
|
|
565
361
|
}, []);
|
|
566
362
|
|
|
567
|
-
// spec 1.1: declarative pause
|
|
568
363
|
const setPaused = useCallback((p: boolean) => {
|
|
569
364
|
pausedRef.current = p;
|
|
570
365
|
}, []);
|
|
571
366
|
|
|
572
|
-
// spec 1.2: single-step mode
|
|
573
367
|
const step = useCallback((n = 1) => {
|
|
574
368
|
stepsToRunRef.current = n;
|
|
575
369
|
}, []);
|
|
576
370
|
|
|
577
|
-
// spec 1.3: simulation time access
|
|
578
371
|
const getTime = useCallback((): number => {
|
|
579
372
|
return mjDataRef.current?.time ?? 0;
|
|
580
373
|
}, []);
|
|
@@ -583,7 +376,6 @@ export function MujocoSimProvider({
|
|
|
583
376
|
return mjModelRef.current?.opt?.timestep ?? 0.002;
|
|
584
377
|
}, []);
|
|
585
378
|
|
|
586
|
-
// spec 4.1: state snapshot save/restore
|
|
587
379
|
const saveState = useCallback((): StateSnapshot => {
|
|
588
380
|
const data = mjDataRef.current;
|
|
589
381
|
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) };
|
|
@@ -610,7 +402,6 @@ export function MujocoSimProvider({
|
|
|
610
402
|
mujoco.mj_forward(model, data);
|
|
611
403
|
}, [mujoco]);
|
|
612
404
|
|
|
613
|
-
// spec 4.3: qpos/qvel direct set/get
|
|
614
405
|
const setQpos = useCallback((values: Float64Array | number[]) => {
|
|
615
406
|
const model = mjModelRef.current;
|
|
616
407
|
const data = mjDataRef.current;
|
|
@@ -635,18 +426,15 @@ export function MujocoSimProvider({
|
|
|
635
426
|
return mjDataRef.current ? new Float64Array(mjDataRef.current.qvel) : new Float64Array(0);
|
|
636
427
|
}, []);
|
|
637
428
|
|
|
638
|
-
// spec 3.1: ctrl set/get
|
|
639
429
|
const setCtrl = useCallback((nameOrValues: string | Record<string, number>, value?: number) => {
|
|
640
430
|
const model = mjModelRef.current;
|
|
641
431
|
const data = mjDataRef.current;
|
|
642
432
|
if (!model || !data) return;
|
|
643
433
|
|
|
644
434
|
if (typeof nameOrValues === 'string') {
|
|
645
|
-
// Single actuator by name
|
|
646
435
|
const id = findActuatorByName(model, nameOrValues);
|
|
647
436
|
if (id >= 0 && value !== undefined) data.ctrl[id] = value;
|
|
648
437
|
} else {
|
|
649
|
-
// Batch: { name: value, ... }
|
|
650
438
|
for (const [name, val] of Object.entries(nameOrValues)) {
|
|
651
439
|
const id = findActuatorByName(model, name);
|
|
652
440
|
if (id >= 0) data.ctrl[id] = val;
|
|
@@ -658,7 +446,6 @@ export function MujocoSimProvider({
|
|
|
658
446
|
return mjDataRef.current ? new Float64Array(mjDataRef.current.ctrl) : new Float64Array(0);
|
|
659
447
|
}, []);
|
|
660
448
|
|
|
661
|
-
// spec 8.1: force/torque API
|
|
662
449
|
const applyForce = useCallback((bodyName: string, force: THREE.Vector3, point?: THREE.Vector3) => {
|
|
663
450
|
const model = mjModelRef.current;
|
|
664
451
|
const data = mjDataRef.current;
|
|
@@ -711,7 +498,6 @@ export function MujocoSimProvider({
|
|
|
711
498
|
}
|
|
712
499
|
}, []);
|
|
713
500
|
|
|
714
|
-
// spec 2.1: sensor data
|
|
715
501
|
const getSensorData = useCallback((name: string): Float64Array | null => {
|
|
716
502
|
const model = mjModelRef.current;
|
|
717
503
|
const data = mjDataRef.current;
|
|
@@ -723,7 +509,6 @@ export function MujocoSimProvider({
|
|
|
723
509
|
return new Float64Array(data.sensordata.subarray(adr, adr + dim));
|
|
724
510
|
}, []);
|
|
725
511
|
|
|
726
|
-
// spec 2.4: contacts
|
|
727
512
|
const getContacts = useCallback((): ContactInfo[] => {
|
|
728
513
|
const model = mjModelRef.current;
|
|
729
514
|
const data = mjDataRef.current;
|
|
@@ -731,24 +516,20 @@ export function MujocoSimProvider({
|
|
|
731
516
|
const contacts: ContactInfo[] = [];
|
|
732
517
|
const ncon = data.ncon;
|
|
733
518
|
for (let i = 0; i < ncon; i++) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
} catch {
|
|
745
|
-
break; // WASM contact access can fail
|
|
746
|
-
}
|
|
519
|
+
const c = getContact(data, i);
|
|
520
|
+
if (!c) break;
|
|
521
|
+
contacts.push({
|
|
522
|
+
geom1: c.geom1,
|
|
523
|
+
geom1Name: getName(model, model.name_geomadr[c.geom1]),
|
|
524
|
+
geom2: c.geom2,
|
|
525
|
+
geom2Name: getName(model, model.name_geomadr[c.geom2]),
|
|
526
|
+
pos: [c.pos[0], c.pos[1], c.pos[2]],
|
|
527
|
+
depth: c.dist,
|
|
528
|
+
});
|
|
747
529
|
}
|
|
748
530
|
return contacts;
|
|
749
531
|
}, []);
|
|
750
532
|
|
|
751
|
-
// spec 5.1: model introspection
|
|
752
533
|
const getBodies = useCallback((): BodyInfo[] => {
|
|
753
534
|
const model = mjModelRef.current;
|
|
754
535
|
if (!model) return [];
|
|
@@ -853,7 +634,6 @@ export function MujocoSimProvider({
|
|
|
853
634
|
return result;
|
|
854
635
|
}, []);
|
|
855
636
|
|
|
856
|
-
// spec 5.3: model options
|
|
857
637
|
const getModelOption = useCallback((): ModelOptions => {
|
|
858
638
|
const model = mjModelRef.current;
|
|
859
639
|
if (!model?.opt) return { timestep: 0.002, gravity: [0, 0, -9.81], integrator: 0 };
|
|
@@ -878,7 +658,6 @@ export function MujocoSimProvider({
|
|
|
878
658
|
model.opt.timestep = dt;
|
|
879
659
|
}, []);
|
|
880
660
|
|
|
881
|
-
// spec 7.1: physics raycast
|
|
882
661
|
const raycast = useCallback((origin: THREE.Vector3, direction: THREE.Vector3, maxDist = 100): RayHit | null => {
|
|
883
662
|
const model = mjModelRef.current;
|
|
884
663
|
const data = mjDataRef.current;
|
|
@@ -905,11 +684,10 @@ export function MujocoSimProvider({
|
|
|
905
684
|
distance: dist,
|
|
906
685
|
};
|
|
907
686
|
} catch {
|
|
908
|
-
return null;
|
|
687
|
+
return null;
|
|
909
688
|
}
|
|
910
689
|
}, [mujoco]);
|
|
911
690
|
|
|
912
|
-
// spec 4.2: keyframe improvements
|
|
913
691
|
const applyKeyframe = useCallback((nameOrIndex: string | number) => {
|
|
914
692
|
const model = mjModelRef.current;
|
|
915
693
|
const data = mjDataRef.current;
|
|
@@ -933,7 +711,6 @@ export function MujocoSimProvider({
|
|
|
933
711
|
const ctrlOffset = keyId * nu;
|
|
934
712
|
for (let i = 0; i < nu; i++) data.ctrl[i] = model.key_ctrl[ctrlOffset + i];
|
|
935
713
|
|
|
936
|
-
// Also restore qvel if available (spec 4.2)
|
|
937
714
|
if (model.key_qvel) {
|
|
938
715
|
const qvelOffset = keyId * model.nv;
|
|
939
716
|
for (let i = 0; i < model.nv; i++) data.qvel[i] = model.key_qvel[qvelOffset + i];
|
|
@@ -941,10 +718,11 @@ export function MujocoSimProvider({
|
|
|
941
718
|
|
|
942
719
|
mujoco.mj_forward(model, data);
|
|
943
720
|
|
|
944
|
-
|
|
945
|
-
|
|
721
|
+
// Notify composable plugins
|
|
722
|
+
for (const cb of resetCallbacks.current) {
|
|
723
|
+
cb();
|
|
946
724
|
}
|
|
947
|
-
}, [mujoco
|
|
725
|
+
}, [mujoco]);
|
|
948
726
|
|
|
949
727
|
const getKeyframeNames = useCallback((): string[] => {
|
|
950
728
|
const model = mjModelRef.current;
|
|
@@ -960,10 +738,9 @@ export function MujocoSimProvider({
|
|
|
960
738
|
return mjModelRef.current?.nkey ?? 0;
|
|
961
739
|
}, []);
|
|
962
740
|
|
|
963
|
-
// spec 9.1: runtime model swap
|
|
964
741
|
const loadSceneApi = useCallback(async (newConfig: SceneConfig): Promise<void> => {
|
|
742
|
+
const gen = ++loadGenRef.current;
|
|
965
743
|
try {
|
|
966
|
-
// Clean up current model
|
|
967
744
|
mjModelRef.current?.delete();
|
|
968
745
|
mjDataRef.current?.delete();
|
|
969
746
|
mjModelRef.current = null;
|
|
@@ -971,30 +748,24 @@ export function MujocoSimProvider({
|
|
|
971
748
|
setStatus('loading');
|
|
972
749
|
|
|
973
750
|
const result = await loadScene(mujoco, newConfig);
|
|
751
|
+
|
|
752
|
+
if (gen !== loadGenRef.current) {
|
|
753
|
+
result.mjModel.delete();
|
|
754
|
+
result.mjData.delete();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
974
758
|
mjModelRef.current = result.mjModel;
|
|
975
759
|
mjDataRef.current = result.mjData;
|
|
976
|
-
siteIdRef.current = result.siteId;
|
|
977
|
-
gripperIdRef.current = result.gripperId;
|
|
978
760
|
configRef.current = newConfig;
|
|
979
761
|
|
|
980
|
-
if (ikTargetRef.current) {
|
|
981
|
-
syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
|
|
982
|
-
}
|
|
983
762
|
setStatus('ready');
|
|
984
763
|
} catch (e) {
|
|
764
|
+
if (gen !== loadGenRef.current) return;
|
|
985
765
|
setStatus('error');
|
|
986
766
|
throw e;
|
|
987
767
|
}
|
|
988
|
-
}, [mujoco
|
|
989
|
-
|
|
990
|
-
const getGizmoStats = useCallback((): { pos: THREE.Vector3; rot: THREE.Euler } | null => {
|
|
991
|
-
const target = ikTargetRef.current;
|
|
992
|
-
if (!ikCalculatingRef.current || !target) return null;
|
|
993
|
-
return {
|
|
994
|
-
pos: target.position.clone(),
|
|
995
|
-
rot: new THREE.Euler().setFromQuaternion(target.quaternion),
|
|
996
|
-
};
|
|
997
|
-
}, []);
|
|
768
|
+
}, [mujoco]);
|
|
998
769
|
|
|
999
770
|
const getCanvasSnapshot = useCallback(
|
|
1000
771
|
(width?: number, height?: number, mimeType = 'image/jpeg'): string => {
|
|
@@ -1020,9 +791,8 @@ export function MujocoSimProvider({
|
|
|
1020
791
|
virtCam.lookAt(lookAt);
|
|
1021
792
|
virtCam.updateMatrixWorld();
|
|
1022
793
|
virtCam.updateProjectionMatrix();
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
raycaster.setFromCamera(ndc, virtCam);
|
|
794
|
+
_projNdc.set(x * 2 - 1, -(y * 2 - 1));
|
|
795
|
+
_projRaycaster.setFromCamera(_projNdc, virtCam);
|
|
1026
796
|
const objects: THREE.Object3D[] = [];
|
|
1027
797
|
const scene = (camera as THREE.PerspectiveCamera).parent;
|
|
1028
798
|
if (scene) {
|
|
@@ -1030,12 +800,10 @@ export function MujocoSimProvider({
|
|
|
1030
800
|
if ((c as THREE.Mesh).isMesh) objects.push(c);
|
|
1031
801
|
});
|
|
1032
802
|
}
|
|
1033
|
-
const hits =
|
|
803
|
+
const hits = _projRaycaster.intersectObjects(objects);
|
|
1034
804
|
if (hits.length > 0) {
|
|
1035
805
|
const hitObj = hits[0].object;
|
|
1036
|
-
// Find geomId from the hit object's userData
|
|
1037
806
|
const geomId = hitObj.userData.geomID !== undefined ? hitObj.userData.geomID : -1;
|
|
1038
|
-
// Walk up to find bodyId
|
|
1039
807
|
let obj = hitObj;
|
|
1040
808
|
while (obj && obj.userData.bodyID === undefined && obj.parent) {
|
|
1041
809
|
obj = obj.parent;
|
|
@@ -1048,7 +816,7 @@ export function MujocoSimProvider({
|
|
|
1048
816
|
[camera, gl]
|
|
1049
817
|
);
|
|
1050
818
|
|
|
1051
|
-
// --- Domain randomization
|
|
819
|
+
// --- Domain randomization ---
|
|
1052
820
|
|
|
1053
821
|
const setBodyMass = useCallback((name: string, mass: number): void => {
|
|
1054
822
|
const model = mjModelRef.current;
|
|
@@ -1078,33 +846,6 @@ export function MujocoSimProvider({
|
|
|
1078
846
|
model.geom_size[id * 3 + 2] = size[2];
|
|
1079
847
|
}, []);
|
|
1080
848
|
|
|
1081
|
-
const getCameraState = useCallback((): { position: THREE.Vector3; target: THREE.Vector3 } => {
|
|
1082
|
-
return { position: camera.position.clone(), target: orbitTargetRef.current.clone() };
|
|
1083
|
-
}, [camera]);
|
|
1084
|
-
|
|
1085
|
-
const moveCameraTo = useCallback(
|
|
1086
|
-
(position: THREE.Vector3, target: THREE.Vector3, durationMs: number): Promise<void> => {
|
|
1087
|
-
return new Promise((resolve) => {
|
|
1088
|
-
const ca = cameraAnimRef.current;
|
|
1089
|
-
ca.active = true;
|
|
1090
|
-
ca.startTime = performance.now();
|
|
1091
|
-
ca.duration = durationMs;
|
|
1092
|
-
ca.startPos.copy(camera.position);
|
|
1093
|
-
ca.startRot.copy(camera.quaternion);
|
|
1094
|
-
ca.startTarget.copy(orbitTargetRef.current);
|
|
1095
|
-
ca.endPos.copy(position);
|
|
1096
|
-
ca.endTarget.copy(target);
|
|
1097
|
-
const dummyCam = (camera as THREE.PerspectiveCamera).clone();
|
|
1098
|
-
dummyCam.position.copy(position);
|
|
1099
|
-
dummyCam.lookAt(target);
|
|
1100
|
-
ca.endRot.copy(dummyCam.quaternion);
|
|
1101
|
-
ca.resolve = resolve;
|
|
1102
|
-
setTimeout(resolve, durationMs + 100);
|
|
1103
|
-
});
|
|
1104
|
-
},
|
|
1105
|
-
[camera]
|
|
1106
|
-
);
|
|
1107
|
-
|
|
1108
849
|
// --- Assemble API ---
|
|
1109
850
|
const api = useMemo<MujocoSimAPI>(
|
|
1110
851
|
() => ({
|
|
@@ -1145,15 +886,8 @@ export function MujocoSimProvider({
|
|
|
1145
886
|
getKeyframeNames,
|
|
1146
887
|
getKeyframeCount,
|
|
1147
888
|
loadScene: loadSceneApi,
|
|
1148
|
-
setIkEnabled,
|
|
1149
|
-
moveTarget,
|
|
1150
|
-
syncTargetToSite,
|
|
1151
|
-
solveIK,
|
|
1152
|
-
getGizmoStats,
|
|
1153
889
|
getCanvasSnapshot,
|
|
1154
890
|
project2DTo3D,
|
|
1155
|
-
getCameraState,
|
|
1156
|
-
moveCameraTo,
|
|
1157
891
|
setBodyMass,
|
|
1158
892
|
setGeomFriction,
|
|
1159
893
|
setGeomSize,
|
|
@@ -1168,8 +902,7 @@ export function MujocoSimProvider({
|
|
|
1168
902
|
getSensorData, getContacts, getBodies, getJoints, getGeoms, getSites,
|
|
1169
903
|
getActuatorsApi, getSensors, getModelOption, setGravity, setTimestepApi,
|
|
1170
904
|
raycast, getKeyframeNames, getKeyframeCount, loadSceneApi,
|
|
1171
|
-
|
|
1172
|
-
getCanvasSnapshot, project2DTo3D, getCameraState, moveCameraTo,
|
|
905
|
+
getCanvasSnapshot, project2DTo3D,
|
|
1173
906
|
setBodyMass, setGeomFriction, setGeomSize,
|
|
1174
907
|
]
|
|
1175
908
|
);
|
|
@@ -1183,22 +916,13 @@ export function MujocoSimProvider({
|
|
|
1183
916
|
mjDataRef,
|
|
1184
917
|
mujocoRef,
|
|
1185
918
|
configRef,
|
|
1186
|
-
siteIdRef,
|
|
1187
|
-
gripperIdRef,
|
|
1188
|
-
ikEnabledRef,
|
|
1189
|
-
ikCalculatingRef,
|
|
1190
919
|
pausedRef,
|
|
1191
920
|
speedRef,
|
|
1192
921
|
substepsRef,
|
|
1193
|
-
ikTargetRef,
|
|
1194
|
-
genericIkRef,
|
|
1195
|
-
ikSolveFnRef,
|
|
1196
|
-
firstIkEnableRef,
|
|
1197
|
-
gizmoAnimRef,
|
|
1198
|
-
cameraAnimRef,
|
|
1199
922
|
onSelectionRef,
|
|
1200
923
|
beforeStepCallbacks,
|
|
1201
924
|
afterStepCallbacks,
|
|
925
|
+
resetCallbacks,
|
|
1202
926
|
status,
|
|
1203
927
|
}),
|
|
1204
928
|
[api, status]
|