mujoco-react 0.2.0 → 1.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 +287 -48
- package/dist/index.d.ts +215 -135
- package/dist/index.js +1176 -795
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/components/ContactMarkers.tsx +19 -22
- package/src/components/Debug.tsx +173 -36
- package/src/components/DragInteraction.tsx +5 -3
- package/src/components/FlexRenderer.tsx +3 -2
- 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 +13 -8
- package/src/components/SelectionHighlight.tsx +2 -49
- package/src/components/TendonRenderer.tsx +93 -28
- package/src/components/TrajectoryPlayer.tsx +14 -10
- package/src/core/IkContext.tsx +40 -0
- package/src/core/MujocoCanvas.tsx +1 -5
- package/src/core/MujocoPhysics.tsx +79 -0
- package/src/core/MujocoProvider.tsx +12 -4
- package/src/core/MujocoSimProvider.tsx +56 -340
- package/src/core/SceneLoader.ts +45 -18
- 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 +18 -1
- package/src/types.ts +53 -26
|
@@ -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,13 +134,12 @@ 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;
|
|
169
141
|
paused?: boolean;
|
|
170
142
|
speed?: number;
|
|
171
|
-
interpolate?: boolean;
|
|
172
143
|
children: React.ReactNode;
|
|
173
144
|
}
|
|
174
145
|
|
|
@@ -185,7 +156,6 @@ export function MujocoSimProvider({
|
|
|
185
156
|
substeps,
|
|
186
157
|
paused,
|
|
187
158
|
speed,
|
|
188
|
-
interpolate,
|
|
189
159
|
children,
|
|
190
160
|
}: MujocoSimProviderProps) {
|
|
191
161
|
const { gl, camera } = useThree();
|
|
@@ -196,21 +166,11 @@ export function MujocoSimProvider({
|
|
|
196
166
|
const mjDataRef = useRef<MujocoData | null>(null);
|
|
197
167
|
const mujocoRef = useRef<MujocoModule>(mujoco);
|
|
198
168
|
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
169
|
const pausedRef = useRef(paused ?? false);
|
|
204
170
|
const speedRef = useRef(speed ?? 1);
|
|
205
171
|
const substepsRef = useRef(substeps ?? 1);
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const stepsToRunRef = useRef(0); // for single-step mode (spec 1.2)
|
|
209
|
-
|
|
210
|
-
// Interpolation state (spec 11.1)
|
|
211
|
-
const prevXposRef = useRef<Float64Array | null>(null);
|
|
212
|
-
const prevXquatRef = useRef<Float64Array | null>(null);
|
|
213
|
-
const interpAlphaRef = useRef(0);
|
|
172
|
+
const stepsToRunRef = useRef(0);
|
|
173
|
+
const loadGenRef = useRef(0);
|
|
214
174
|
|
|
215
175
|
const onSelectionRef = useRef(onSelection);
|
|
216
176
|
onSelectionRef.current = onSelection;
|
|
@@ -219,6 +179,7 @@ export function MujocoSimProvider({
|
|
|
219
179
|
|
|
220
180
|
const beforeStepCallbacks = useRef(new Set<PhysicsStepCallback>());
|
|
221
181
|
const afterStepCallbacks = useRef(new Set<PhysicsStepCallback>());
|
|
182
|
+
const resetCallbacks = useRef(new Set<() => void>());
|
|
222
183
|
|
|
223
184
|
configRef.current = config;
|
|
224
185
|
|
|
@@ -226,9 +187,8 @@ export function MujocoSimProvider({
|
|
|
226
187
|
useEffect(() => { pausedRef.current = paused ?? false; }, [paused]);
|
|
227
188
|
useEffect(() => { speedRef.current = speed ?? 1; }, [speed]);
|
|
228
189
|
useEffect(() => { substepsRef.current = substeps ?? 1; }, [substeps]);
|
|
229
|
-
useEffect(() => { interpolateRef.current = interpolate ?? false; }, [interpolate]);
|
|
230
190
|
|
|
231
|
-
// Sync gravity prop
|
|
191
|
+
// Sync gravity prop
|
|
232
192
|
useEffect(() => {
|
|
233
193
|
if (!gravity) return;
|
|
234
194
|
const model = mjModelRef.current;
|
|
@@ -238,7 +198,7 @@ export function MujocoSimProvider({
|
|
|
238
198
|
model.opt.gravity[2] = gravity[2];
|
|
239
199
|
}, [gravity]);
|
|
240
200
|
|
|
241
|
-
// Sync timestep prop
|
|
201
|
+
// Sync timestep prop
|
|
242
202
|
useEffect(() => {
|
|
243
203
|
if (timestep === undefined) return;
|
|
244
204
|
const model = mjModelRef.current;
|
|
@@ -246,66 +206,6 @@ export function MujocoSimProvider({
|
|
|
246
206
|
model.opt.timestep = timestep;
|
|
247
207
|
}, [timestep]);
|
|
248
208
|
|
|
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
209
|
// --- Load scene on mount ---
|
|
310
210
|
useEffect(() => {
|
|
311
211
|
let disposed = false;
|
|
@@ -321,8 +221,6 @@ export function MujocoSimProvider({
|
|
|
321
221
|
|
|
322
222
|
mjModelRef.current = result.mjModel;
|
|
323
223
|
mjDataRef.current = result.mjData;
|
|
324
|
-
siteIdRef.current = result.siteId;
|
|
325
|
-
gripperIdRef.current = result.gripperId;
|
|
326
224
|
|
|
327
225
|
// Apply declarative physics props after load
|
|
328
226
|
if (gravity && result.mjModel.opt?.gravity) {
|
|
@@ -334,10 +232,6 @@ export function MujocoSimProvider({
|
|
|
334
232
|
result.mjModel.opt.timestep = timestep;
|
|
335
233
|
}
|
|
336
234
|
|
|
337
|
-
if (ikTargetRef.current) {
|
|
338
|
-
syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
235
|
setStatus('ready');
|
|
342
236
|
} catch (e: unknown) {
|
|
343
237
|
if (!disposed) {
|
|
@@ -374,51 +268,12 @@ export function MujocoSimProvider({
|
|
|
374
268
|
}, [status]);
|
|
375
269
|
|
|
376
270
|
// --- Physics step (priority -1) ---
|
|
377
|
-
useFrame((
|
|
271
|
+
useFrame((_state, delta) => {
|
|
378
272
|
const model = mjModelRef.current;
|
|
379
273
|
const data = mjDataRef.current;
|
|
380
274
|
if (!model || !data) return;
|
|
381
275
|
|
|
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)
|
|
276
|
+
// Check single-step mode
|
|
422
277
|
const shouldStep = !pausedRef.current || stepsToRunRef.current > 0;
|
|
423
278
|
if (!shouldStep) return;
|
|
424
279
|
|
|
@@ -432,31 +287,17 @@ export function MujocoSimProvider({
|
|
|
432
287
|
cb(model, data);
|
|
433
288
|
}
|
|
434
289
|
|
|
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)
|
|
290
|
+
// Step physics with substeps
|
|
450
291
|
const numSubsteps = substepsRef.current;
|
|
451
292
|
if (stepsToRunRef.current > 0) {
|
|
452
|
-
// Single-step mode (spec 1.2)
|
|
453
293
|
for (let s = 0; s < stepsToRunRef.current; s++) {
|
|
454
294
|
mujoco.mj_step(model, data);
|
|
455
295
|
}
|
|
456
296
|
stepsToRunRef.current = 0;
|
|
457
297
|
} else {
|
|
458
298
|
const startSimTime = data.time;
|
|
459
|
-
const
|
|
299
|
+
const clampedDelta = Math.min(delta, 1 / 15); // cap to avoid spiral of death
|
|
300
|
+
const frameTime = clampedDelta * speedRef.current;
|
|
460
301
|
while (data.time - startSimTime < frameTime) {
|
|
461
302
|
for (let s = 0; s < numSubsteps; s++) {
|
|
462
303
|
mujoco.mj_step(model, data);
|
|
@@ -479,19 +320,16 @@ export function MujocoSimProvider({
|
|
|
479
320
|
const data = mjDataRef.current;
|
|
480
321
|
if (!model || !data) return;
|
|
481
322
|
|
|
482
|
-
gizmoAnimRef.current.active = false;
|
|
483
323
|
mujoco.mj_resetData(model, data);
|
|
484
324
|
|
|
485
325
|
const homeJoints = configRef.current.homeJoints;
|
|
486
326
|
if (homeJoints) {
|
|
487
|
-
|
|
327
|
+
const homeCount = Math.min(homeJoints.length, model.nu);
|
|
328
|
+
for (let i = 0; i < homeCount; i++) {
|
|
488
329
|
data.ctrl[i] = homeJoints[i];
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const qposAdr = model.jnt_qposadr[jointId];
|
|
493
|
-
data.qpos[qposAdr] = homeJoints[i];
|
|
494
|
-
}
|
|
330
|
+
const qposAdr = getActuatedScalarQposAdr(model, i);
|
|
331
|
+
if (qposAdr !== -1) {
|
|
332
|
+
data.qpos[qposAdr] = homeJoints[i];
|
|
495
333
|
}
|
|
496
334
|
}
|
|
497
335
|
}
|
|
@@ -499,61 +337,11 @@ export function MujocoSimProvider({
|
|
|
499
337
|
configRef.current.onReset?.(model, data);
|
|
500
338
|
mujoco.mj_forward(model, data);
|
|
501
339
|
|
|
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;
|
|
340
|
+
// Notify composable plugins (e.g. IkController)
|
|
341
|
+
for (const cb of resetCallbacks.current) {
|
|
342
|
+
cb();
|
|
515
343
|
}
|
|
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
|
-
);
|
|
344
|
+
}, [mujoco]);
|
|
557
345
|
|
|
558
346
|
const setSpeed = useCallback((multiplier: number) => {
|
|
559
347
|
speedRef.current = multiplier;
|
|
@@ -564,17 +352,14 @@ export function MujocoSimProvider({
|
|
|
564
352
|
return pausedRef.current;
|
|
565
353
|
}, []);
|
|
566
354
|
|
|
567
|
-
// spec 1.1: declarative pause
|
|
568
355
|
const setPaused = useCallback((p: boolean) => {
|
|
569
356
|
pausedRef.current = p;
|
|
570
357
|
}, []);
|
|
571
358
|
|
|
572
|
-
// spec 1.2: single-step mode
|
|
573
359
|
const step = useCallback((n = 1) => {
|
|
574
360
|
stepsToRunRef.current = n;
|
|
575
361
|
}, []);
|
|
576
362
|
|
|
577
|
-
// spec 1.3: simulation time access
|
|
578
363
|
const getTime = useCallback((): number => {
|
|
579
364
|
return mjDataRef.current?.time ?? 0;
|
|
580
365
|
}, []);
|
|
@@ -583,7 +368,6 @@ export function MujocoSimProvider({
|
|
|
583
368
|
return mjModelRef.current?.opt?.timestep ?? 0.002;
|
|
584
369
|
}, []);
|
|
585
370
|
|
|
586
|
-
// spec 4.1: state snapshot save/restore
|
|
587
371
|
const saveState = useCallback((): StateSnapshot => {
|
|
588
372
|
const data = mjDataRef.current;
|
|
589
373
|
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 +394,6 @@ export function MujocoSimProvider({
|
|
|
610
394
|
mujoco.mj_forward(model, data);
|
|
611
395
|
}, [mujoco]);
|
|
612
396
|
|
|
613
|
-
// spec 4.3: qpos/qvel direct set/get
|
|
614
397
|
const setQpos = useCallback((values: Float64Array | number[]) => {
|
|
615
398
|
const model = mjModelRef.current;
|
|
616
399
|
const data = mjDataRef.current;
|
|
@@ -635,18 +418,15 @@ export function MujocoSimProvider({
|
|
|
635
418
|
return mjDataRef.current ? new Float64Array(mjDataRef.current.qvel) : new Float64Array(0);
|
|
636
419
|
}, []);
|
|
637
420
|
|
|
638
|
-
// spec 3.1: ctrl set/get
|
|
639
421
|
const setCtrl = useCallback((nameOrValues: string | Record<string, number>, value?: number) => {
|
|
640
422
|
const model = mjModelRef.current;
|
|
641
423
|
const data = mjDataRef.current;
|
|
642
424
|
if (!model || !data) return;
|
|
643
425
|
|
|
644
426
|
if (typeof nameOrValues === 'string') {
|
|
645
|
-
// Single actuator by name
|
|
646
427
|
const id = findActuatorByName(model, nameOrValues);
|
|
647
428
|
if (id >= 0 && value !== undefined) data.ctrl[id] = value;
|
|
648
429
|
} else {
|
|
649
|
-
// Batch: { name: value, ... }
|
|
650
430
|
for (const [name, val] of Object.entries(nameOrValues)) {
|
|
651
431
|
const id = findActuatorByName(model, name);
|
|
652
432
|
if (id >= 0) data.ctrl[id] = val;
|
|
@@ -658,7 +438,6 @@ export function MujocoSimProvider({
|
|
|
658
438
|
return mjDataRef.current ? new Float64Array(mjDataRef.current.ctrl) : new Float64Array(0);
|
|
659
439
|
}, []);
|
|
660
440
|
|
|
661
|
-
// spec 8.1: force/torque API
|
|
662
441
|
const applyForce = useCallback((bodyName: string, force: THREE.Vector3, point?: THREE.Vector3) => {
|
|
663
442
|
const model = mjModelRef.current;
|
|
664
443
|
const data = mjDataRef.current;
|
|
@@ -711,7 +490,6 @@ export function MujocoSimProvider({
|
|
|
711
490
|
}
|
|
712
491
|
}, []);
|
|
713
492
|
|
|
714
|
-
// spec 2.1: sensor data
|
|
715
493
|
const getSensorData = useCallback((name: string): Float64Array | null => {
|
|
716
494
|
const model = mjModelRef.current;
|
|
717
495
|
const data = mjDataRef.current;
|
|
@@ -723,7 +501,6 @@ export function MujocoSimProvider({
|
|
|
723
501
|
return new Float64Array(data.sensordata.subarray(adr, adr + dim));
|
|
724
502
|
}, []);
|
|
725
503
|
|
|
726
|
-
// spec 2.4: contacts
|
|
727
504
|
const getContacts = useCallback((): ContactInfo[] => {
|
|
728
505
|
const model = mjModelRef.current;
|
|
729
506
|
const data = mjDataRef.current;
|
|
@@ -731,24 +508,20 @@ export function MujocoSimProvider({
|
|
|
731
508
|
const contacts: ContactInfo[] = [];
|
|
732
509
|
const ncon = data.ncon;
|
|
733
510
|
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
|
-
}
|
|
511
|
+
const c = getContact(data, i);
|
|
512
|
+
if (!c) break;
|
|
513
|
+
contacts.push({
|
|
514
|
+
geom1: c.geom1,
|
|
515
|
+
geom1Name: getName(model, model.name_geomadr[c.geom1]),
|
|
516
|
+
geom2: c.geom2,
|
|
517
|
+
geom2Name: getName(model, model.name_geomadr[c.geom2]),
|
|
518
|
+
pos: [c.pos[0], c.pos[1], c.pos[2]],
|
|
519
|
+
depth: c.dist,
|
|
520
|
+
});
|
|
747
521
|
}
|
|
748
522
|
return contacts;
|
|
749
523
|
}, []);
|
|
750
524
|
|
|
751
|
-
// spec 5.1: model introspection
|
|
752
525
|
const getBodies = useCallback((): BodyInfo[] => {
|
|
753
526
|
const model = mjModelRef.current;
|
|
754
527
|
if (!model) return [];
|
|
@@ -853,7 +626,6 @@ export function MujocoSimProvider({
|
|
|
853
626
|
return result;
|
|
854
627
|
}, []);
|
|
855
628
|
|
|
856
|
-
// spec 5.3: model options
|
|
857
629
|
const getModelOption = useCallback((): ModelOptions => {
|
|
858
630
|
const model = mjModelRef.current;
|
|
859
631
|
if (!model?.opt) return { timestep: 0.002, gravity: [0, 0, -9.81], integrator: 0 };
|
|
@@ -878,7 +650,6 @@ export function MujocoSimProvider({
|
|
|
878
650
|
model.opt.timestep = dt;
|
|
879
651
|
}, []);
|
|
880
652
|
|
|
881
|
-
// spec 7.1: physics raycast
|
|
882
653
|
const raycast = useCallback((origin: THREE.Vector3, direction: THREE.Vector3, maxDist = 100): RayHit | null => {
|
|
883
654
|
const model = mjModelRef.current;
|
|
884
655
|
const data = mjDataRef.current;
|
|
@@ -905,11 +676,10 @@ export function MujocoSimProvider({
|
|
|
905
676
|
distance: dist,
|
|
906
677
|
};
|
|
907
678
|
} catch {
|
|
908
|
-
return null;
|
|
679
|
+
return null;
|
|
909
680
|
}
|
|
910
681
|
}, [mujoco]);
|
|
911
682
|
|
|
912
|
-
// spec 4.2: keyframe improvements
|
|
913
683
|
const applyKeyframe = useCallback((nameOrIndex: string | number) => {
|
|
914
684
|
const model = mjModelRef.current;
|
|
915
685
|
const data = mjDataRef.current;
|
|
@@ -933,7 +703,6 @@ export function MujocoSimProvider({
|
|
|
933
703
|
const ctrlOffset = keyId * nu;
|
|
934
704
|
for (let i = 0; i < nu; i++) data.ctrl[i] = model.key_ctrl[ctrlOffset + i];
|
|
935
705
|
|
|
936
|
-
// Also restore qvel if available (spec 4.2)
|
|
937
706
|
if (model.key_qvel) {
|
|
938
707
|
const qvelOffset = keyId * model.nv;
|
|
939
708
|
for (let i = 0; i < model.nv; i++) data.qvel[i] = model.key_qvel[qvelOffset + i];
|
|
@@ -941,10 +710,11 @@ export function MujocoSimProvider({
|
|
|
941
710
|
|
|
942
711
|
mujoco.mj_forward(model, data);
|
|
943
712
|
|
|
944
|
-
|
|
945
|
-
|
|
713
|
+
// Notify composable plugins
|
|
714
|
+
for (const cb of resetCallbacks.current) {
|
|
715
|
+
cb();
|
|
946
716
|
}
|
|
947
|
-
}, [mujoco
|
|
717
|
+
}, [mujoco]);
|
|
948
718
|
|
|
949
719
|
const getKeyframeNames = useCallback((): string[] => {
|
|
950
720
|
const model = mjModelRef.current;
|
|
@@ -960,10 +730,9 @@ export function MujocoSimProvider({
|
|
|
960
730
|
return mjModelRef.current?.nkey ?? 0;
|
|
961
731
|
}, []);
|
|
962
732
|
|
|
963
|
-
// spec 9.1: runtime model swap
|
|
964
733
|
const loadSceneApi = useCallback(async (newConfig: SceneConfig): Promise<void> => {
|
|
734
|
+
const gen = ++loadGenRef.current;
|
|
965
735
|
try {
|
|
966
|
-
// Clean up current model
|
|
967
736
|
mjModelRef.current?.delete();
|
|
968
737
|
mjDataRef.current?.delete();
|
|
969
738
|
mjModelRef.current = null;
|
|
@@ -971,30 +740,24 @@ export function MujocoSimProvider({
|
|
|
971
740
|
setStatus('loading');
|
|
972
741
|
|
|
973
742
|
const result = await loadScene(mujoco, newConfig);
|
|
743
|
+
|
|
744
|
+
if (gen !== loadGenRef.current) {
|
|
745
|
+
result.mjModel.delete();
|
|
746
|
+
result.mjData.delete();
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
974
750
|
mjModelRef.current = result.mjModel;
|
|
975
751
|
mjDataRef.current = result.mjData;
|
|
976
|
-
siteIdRef.current = result.siteId;
|
|
977
|
-
gripperIdRef.current = result.gripperId;
|
|
978
752
|
configRef.current = newConfig;
|
|
979
753
|
|
|
980
|
-
if (ikTargetRef.current) {
|
|
981
|
-
syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
|
|
982
|
-
}
|
|
983
754
|
setStatus('ready');
|
|
984
755
|
} catch (e) {
|
|
756
|
+
if (gen !== loadGenRef.current) return;
|
|
985
757
|
setStatus('error');
|
|
986
758
|
throw e;
|
|
987
759
|
}
|
|
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
|
-
}, []);
|
|
760
|
+
}, [mujoco]);
|
|
998
761
|
|
|
999
762
|
const getCanvasSnapshot = useCallback(
|
|
1000
763
|
(width?: number, height?: number, mimeType = 'image/jpeg'): string => {
|
|
@@ -1020,9 +783,8 @@ export function MujocoSimProvider({
|
|
|
1020
783
|
virtCam.lookAt(lookAt);
|
|
1021
784
|
virtCam.updateMatrixWorld();
|
|
1022
785
|
virtCam.updateProjectionMatrix();
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
raycaster.setFromCamera(ndc, virtCam);
|
|
786
|
+
_projNdc.set(x * 2 - 1, -(y * 2 - 1));
|
|
787
|
+
_projRaycaster.setFromCamera(_projNdc, virtCam);
|
|
1026
788
|
const objects: THREE.Object3D[] = [];
|
|
1027
789
|
const scene = (camera as THREE.PerspectiveCamera).parent;
|
|
1028
790
|
if (scene) {
|
|
@@ -1030,12 +792,10 @@ export function MujocoSimProvider({
|
|
|
1030
792
|
if ((c as THREE.Mesh).isMesh) objects.push(c);
|
|
1031
793
|
});
|
|
1032
794
|
}
|
|
1033
|
-
const hits =
|
|
795
|
+
const hits = _projRaycaster.intersectObjects(objects);
|
|
1034
796
|
if (hits.length > 0) {
|
|
1035
797
|
const hitObj = hits[0].object;
|
|
1036
|
-
// Find geomId from the hit object's userData
|
|
1037
798
|
const geomId = hitObj.userData.geomID !== undefined ? hitObj.userData.geomID : -1;
|
|
1038
|
-
// Walk up to find bodyId
|
|
1039
799
|
let obj = hitObj;
|
|
1040
800
|
while (obj && obj.userData.bodyID === undefined && obj.parent) {
|
|
1041
801
|
obj = obj.parent;
|
|
@@ -1048,7 +808,7 @@ export function MujocoSimProvider({
|
|
|
1048
808
|
[camera, gl]
|
|
1049
809
|
);
|
|
1050
810
|
|
|
1051
|
-
// --- Domain randomization
|
|
811
|
+
// --- Domain randomization ---
|
|
1052
812
|
|
|
1053
813
|
const setBodyMass = useCallback((name: string, mass: number): void => {
|
|
1054
814
|
const model = mjModelRef.current;
|
|
@@ -1078,33 +838,6 @@ export function MujocoSimProvider({
|
|
|
1078
838
|
model.geom_size[id * 3 + 2] = size[2];
|
|
1079
839
|
}, []);
|
|
1080
840
|
|
|
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
841
|
// --- Assemble API ---
|
|
1109
842
|
const api = useMemo<MujocoSimAPI>(
|
|
1110
843
|
() => ({
|
|
@@ -1145,15 +878,8 @@ export function MujocoSimProvider({
|
|
|
1145
878
|
getKeyframeNames,
|
|
1146
879
|
getKeyframeCount,
|
|
1147
880
|
loadScene: loadSceneApi,
|
|
1148
|
-
setIkEnabled,
|
|
1149
|
-
moveTarget,
|
|
1150
|
-
syncTargetToSite,
|
|
1151
|
-
solveIK,
|
|
1152
|
-
getGizmoStats,
|
|
1153
881
|
getCanvasSnapshot,
|
|
1154
882
|
project2DTo3D,
|
|
1155
|
-
getCameraState,
|
|
1156
|
-
moveCameraTo,
|
|
1157
883
|
setBodyMass,
|
|
1158
884
|
setGeomFriction,
|
|
1159
885
|
setGeomSize,
|
|
@@ -1168,8 +894,7 @@ export function MujocoSimProvider({
|
|
|
1168
894
|
getSensorData, getContacts, getBodies, getJoints, getGeoms, getSites,
|
|
1169
895
|
getActuatorsApi, getSensors, getModelOption, setGravity, setTimestepApi,
|
|
1170
896
|
raycast, getKeyframeNames, getKeyframeCount, loadSceneApi,
|
|
1171
|
-
|
|
1172
|
-
getCanvasSnapshot, project2DTo3D, getCameraState, moveCameraTo,
|
|
897
|
+
getCanvasSnapshot, project2DTo3D,
|
|
1173
898
|
setBodyMass, setGeomFriction, setGeomSize,
|
|
1174
899
|
]
|
|
1175
900
|
);
|
|
@@ -1183,22 +908,13 @@ export function MujocoSimProvider({
|
|
|
1183
908
|
mjDataRef,
|
|
1184
909
|
mujocoRef,
|
|
1185
910
|
configRef,
|
|
1186
|
-
siteIdRef,
|
|
1187
|
-
gripperIdRef,
|
|
1188
|
-
ikEnabledRef,
|
|
1189
|
-
ikCalculatingRef,
|
|
1190
911
|
pausedRef,
|
|
1191
912
|
speedRef,
|
|
1192
913
|
substepsRef,
|
|
1193
|
-
ikTargetRef,
|
|
1194
|
-
genericIkRef,
|
|
1195
|
-
ikSolveFnRef,
|
|
1196
|
-
firstIkEnableRef,
|
|
1197
|
-
gizmoAnimRef,
|
|
1198
|
-
cameraAnimRef,
|
|
1199
914
|
onSelectionRef,
|
|
1200
915
|
beforeStepCallbacks,
|
|
1201
916
|
afterStepCallbacks,
|
|
917
|
+
resetCallbacks,
|
|
1202
918
|
status,
|
|
1203
919
|
}),
|
|
1204
920
|
[api, status]
|