mujoco-react 0.1.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 +209 -45
- package/dist/index.d.ts +180 -97
- package/dist/index.js +1148 -772
- 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/MujocoCanvas.tsx +6 -3
- package/src/core/MujocoProvider.tsx +12 -4
- package/src/core/MujocoSimProvider.tsx +69 -334
- 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
|
|
|
@@ -157,11 +129,12 @@ export function useAfterPhysicsStep(callback: PhysicsStepCallback) {
|
|
|
157
129
|
interface MujocoSimProviderProps {
|
|
158
130
|
mujoco: MujocoModule;
|
|
159
131
|
config: SceneConfig;
|
|
132
|
+
apiRef?: React.ForwardedRef<MujocoSimAPI>;
|
|
160
133
|
onReady?: (api: MujocoSimAPI) => void;
|
|
161
134
|
onError?: (error: Error) => void;
|
|
162
135
|
onStep?: (time: number) => void;
|
|
163
136
|
onSelection?: (bodyId: number, name: string) => void;
|
|
164
|
-
// Declarative physics config props
|
|
137
|
+
// Declarative physics config props
|
|
165
138
|
gravity?: [number, number, number];
|
|
166
139
|
timestep?: number;
|
|
167
140
|
substeps?: number;
|
|
@@ -174,6 +147,7 @@ interface MujocoSimProviderProps {
|
|
|
174
147
|
export function MujocoSimProvider({
|
|
175
148
|
mujoco,
|
|
176
149
|
config,
|
|
150
|
+
apiRef: externalApiRef,
|
|
177
151
|
onReady,
|
|
178
152
|
onError,
|
|
179
153
|
onStep,
|
|
@@ -194,18 +168,14 @@ export function MujocoSimProvider({
|
|
|
194
168
|
const mjDataRef = useRef<MujocoData | null>(null);
|
|
195
169
|
const mujocoRef = useRef<MujocoModule>(mujoco);
|
|
196
170
|
const configRef = useRef<SceneConfig>(config);
|
|
197
|
-
const siteIdRef = useRef(-1);
|
|
198
|
-
const gripperIdRef = useRef(-1);
|
|
199
|
-
const ikEnabledRef = useRef(false);
|
|
200
|
-
const ikCalculatingRef = useRef(false);
|
|
201
171
|
const pausedRef = useRef(paused ?? false);
|
|
202
172
|
const speedRef = useRef(speed ?? 1);
|
|
203
173
|
const substepsRef = useRef(substeps ?? 1);
|
|
204
174
|
const interpolateRef = useRef(interpolate ?? false);
|
|
205
|
-
const
|
|
206
|
-
const
|
|
175
|
+
const stepsToRunRef = useRef(0);
|
|
176
|
+
const loadGenRef = useRef(0);
|
|
207
177
|
|
|
208
|
-
// Interpolation state
|
|
178
|
+
// Interpolation state
|
|
209
179
|
const prevXposRef = useRef<Float64Array | null>(null);
|
|
210
180
|
const prevXquatRef = useRef<Float64Array | null>(null);
|
|
211
181
|
const interpAlphaRef = useRef(0);
|
|
@@ -217,6 +187,7 @@ export function MujocoSimProvider({
|
|
|
217
187
|
|
|
218
188
|
const beforeStepCallbacks = useRef(new Set<PhysicsStepCallback>());
|
|
219
189
|
const afterStepCallbacks = useRef(new Set<PhysicsStepCallback>());
|
|
190
|
+
const resetCallbacks = useRef(new Set<() => void>());
|
|
220
191
|
|
|
221
192
|
configRef.current = config;
|
|
222
193
|
|
|
@@ -226,7 +197,7 @@ export function MujocoSimProvider({
|
|
|
226
197
|
useEffect(() => { substepsRef.current = substeps ?? 1; }, [substeps]);
|
|
227
198
|
useEffect(() => { interpolateRef.current = interpolate ?? false; }, [interpolate]);
|
|
228
199
|
|
|
229
|
-
// Sync gravity prop
|
|
200
|
+
// Sync gravity prop
|
|
230
201
|
useEffect(() => {
|
|
231
202
|
if (!gravity) return;
|
|
232
203
|
const model = mjModelRef.current;
|
|
@@ -236,7 +207,7 @@ export function MujocoSimProvider({
|
|
|
236
207
|
model.opt.gravity[2] = gravity[2];
|
|
237
208
|
}, [gravity]);
|
|
238
209
|
|
|
239
|
-
// Sync timestep prop
|
|
210
|
+
// Sync timestep prop
|
|
240
211
|
useEffect(() => {
|
|
241
212
|
if (timestep === undefined) return;
|
|
242
213
|
const model = mjModelRef.current;
|
|
@@ -244,66 +215,6 @@ export function MujocoSimProvider({
|
|
|
244
215
|
model.opt.timestep = timestep;
|
|
245
216
|
}, [timestep]);
|
|
246
217
|
|
|
247
|
-
const ikTargetRef = useRef<THREE.Group>(new THREE.Group());
|
|
248
|
-
const genericIkRef = useRef<GenericIK>(new GenericIK(mujoco));
|
|
249
|
-
|
|
250
|
-
const gizmoAnimRef = useRef({
|
|
251
|
-
active: false,
|
|
252
|
-
startPos: new THREE.Vector3(),
|
|
253
|
-
endPos: new THREE.Vector3(),
|
|
254
|
-
startRot: new THREE.Quaternion(),
|
|
255
|
-
endRot: new THREE.Quaternion(),
|
|
256
|
-
startTime: 0,
|
|
257
|
-
duration: 1000,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const cameraAnimRef = useRef({
|
|
261
|
-
active: false,
|
|
262
|
-
startPos: new THREE.Vector3(),
|
|
263
|
-
endPos: new THREE.Vector3(),
|
|
264
|
-
startRot: new THREE.Quaternion(),
|
|
265
|
-
endRot: new THREE.Quaternion(),
|
|
266
|
-
startTarget: new THREE.Vector3(),
|
|
267
|
-
endTarget: new THREE.Vector3(),
|
|
268
|
-
startTime: 0,
|
|
269
|
-
duration: 0,
|
|
270
|
-
resolve: null as (() => void) | null,
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const orbitTargetRef = useRef(new THREE.Vector3(0, 0, 0));
|
|
274
|
-
|
|
275
|
-
// --- Helper: sync gizmo to actual MuJoCo site position ---
|
|
276
|
-
const syncGizmoToSite = useCallback((data: MujocoData, siteId: number, target: THREE.Group) => {
|
|
277
|
-
if (siteId === -1) return;
|
|
278
|
-
const sitePos = data.site_xpos.subarray(siteId * 3, siteId * 3 + 3);
|
|
279
|
-
const siteMat = data.site_xmat.subarray(siteId * 9, siteId * 9 + 9);
|
|
280
|
-
target.position.set(sitePos[0], sitePos[1], sitePos[2]);
|
|
281
|
-
const m = new THREE.Matrix4().set(
|
|
282
|
-
siteMat[0], siteMat[1], siteMat[2], 0,
|
|
283
|
-
siteMat[3], siteMat[4], siteMat[5], 0,
|
|
284
|
-
siteMat[6], siteMat[7], siteMat[8], 0,
|
|
285
|
-
0, 0, 0, 1
|
|
286
|
-
);
|
|
287
|
-
target.quaternion.setFromRotationMatrix(m);
|
|
288
|
-
}, []);
|
|
289
|
-
|
|
290
|
-
// IK solve function
|
|
291
|
-
const ikSolveFn = useCallback(
|
|
292
|
-
(pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]): number[] | null => {
|
|
293
|
-
const model = mjModelRef.current;
|
|
294
|
-
const data = mjDataRef.current;
|
|
295
|
-
if (!model || !data || siteIdRef.current === -1) return null;
|
|
296
|
-
return genericIkRef.current.solve(
|
|
297
|
-
model, data, siteIdRef.current,
|
|
298
|
-
configRef.current.numArmJoints ?? 7,
|
|
299
|
-
pos, quat, currentQ
|
|
300
|
-
);
|
|
301
|
-
},
|
|
302
|
-
[]
|
|
303
|
-
);
|
|
304
|
-
const ikSolveFnRef = useRef<IKSolveFn>(ikSolveFn);
|
|
305
|
-
ikSolveFnRef.current = ikSolveFn;
|
|
306
|
-
|
|
307
218
|
// --- Load scene on mount ---
|
|
308
219
|
useEffect(() => {
|
|
309
220
|
let disposed = false;
|
|
@@ -319,8 +230,6 @@ export function MujocoSimProvider({
|
|
|
319
230
|
|
|
320
231
|
mjModelRef.current = result.mjModel;
|
|
321
232
|
mjDataRef.current = result.mjData;
|
|
322
|
-
siteIdRef.current = result.siteId;
|
|
323
|
-
gripperIdRef.current = result.gripperId;
|
|
324
233
|
|
|
325
234
|
// Apply declarative physics props after load
|
|
326
235
|
if (gravity && result.mjModel.opt?.gravity) {
|
|
@@ -332,10 +241,6 @@ export function MujocoSimProvider({
|
|
|
332
241
|
result.mjModel.opt.timestep = timestep;
|
|
333
242
|
}
|
|
334
243
|
|
|
335
|
-
if (ikTargetRef.current) {
|
|
336
|
-
syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
244
|
setStatus('ready');
|
|
340
245
|
} catch (e: unknown) {
|
|
341
246
|
if (!disposed) {
|
|
@@ -355,59 +260,29 @@ export function MujocoSimProvider({
|
|
|
355
260
|
};
|
|
356
261
|
}, [mujoco, config]);
|
|
357
262
|
|
|
358
|
-
// Fire onReady when status changes to ready
|
|
263
|
+
// Fire onReady and assign external ref when status changes to ready
|
|
359
264
|
useEffect(() => {
|
|
360
|
-
if (status === 'ready'
|
|
361
|
-
|
|
265
|
+
if (status === 'ready') {
|
|
266
|
+
const api = apiRef.current;
|
|
267
|
+
if (onReady) onReady(api);
|
|
268
|
+
// Assign the forwarded ref
|
|
269
|
+
if (externalApiRef) {
|
|
270
|
+
if (typeof externalApiRef === 'function') {
|
|
271
|
+
externalApiRef(api);
|
|
272
|
+
} else {
|
|
273
|
+
(externalApiRef as React.MutableRefObject<MujocoSimAPI | null>).current = api;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
362
276
|
}
|
|
363
277
|
}, [status]);
|
|
364
278
|
|
|
365
279
|
// --- Physics step (priority -1) ---
|
|
366
|
-
useFrame((
|
|
280
|
+
useFrame(() => {
|
|
367
281
|
const model = mjModelRef.current;
|
|
368
282
|
const data = mjDataRef.current;
|
|
369
283
|
if (!model || !data) return;
|
|
370
284
|
|
|
371
|
-
//
|
|
372
|
-
const ga = gizmoAnimRef.current;
|
|
373
|
-
const target = ikTargetRef.current;
|
|
374
|
-
if (ga.active && target) {
|
|
375
|
-
const now = performance.now();
|
|
376
|
-
const elapsed = now - ga.startTime;
|
|
377
|
-
const t = Math.min(elapsed / ga.duration, 1.0);
|
|
378
|
-
const ease = 1 - Math.pow(1 - t, 3);
|
|
379
|
-
target.position.lerpVectors(ga.startPos, ga.endPos, ease);
|
|
380
|
-
target.quaternion.slerpQuaternions(ga.startRot, ga.endRot, ease);
|
|
381
|
-
if (t >= 1.0) ga.active = false;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Camera animation
|
|
385
|
-
const ca = cameraAnimRef.current;
|
|
386
|
-
if (ca.active) {
|
|
387
|
-
const now = performance.now();
|
|
388
|
-
const progress = Math.min((now - ca.startTime) / ca.duration, 1.0);
|
|
389
|
-
const ease =
|
|
390
|
-
progress < 0.5
|
|
391
|
-
? 4 * progress * progress * progress
|
|
392
|
-
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
|
393
|
-
camera.position.lerpVectors(ca.startPos, ca.endPos, ease);
|
|
394
|
-
camera.quaternion.slerpQuaternions(ca.startRot, ca.endRot, ease);
|
|
395
|
-
orbitTargetRef.current.lerpVectors(ca.startTarget, ca.endTarget, ease);
|
|
396
|
-
const orbitControls = state.controls as { target?: THREE.Vector3 };
|
|
397
|
-
if (orbitControls?.target) {
|
|
398
|
-
orbitControls.target.copy(orbitTargetRef.current);
|
|
399
|
-
}
|
|
400
|
-
if (progress >= 1.0) {
|
|
401
|
-
ca.active = false;
|
|
402
|
-
camera.position.copy(ca.endPos);
|
|
403
|
-
camera.quaternion.copy(ca.endRot);
|
|
404
|
-
orbitTargetRef.current.copy(ca.endTarget);
|
|
405
|
-
ca.resolve?.();
|
|
406
|
-
ca.resolve = null;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Check single-step mode (spec 1.2)
|
|
285
|
+
// Check single-step mode
|
|
411
286
|
const shouldStep = !pausedRef.current || stepsToRunRef.current > 0;
|
|
412
287
|
if (!shouldStep) return;
|
|
413
288
|
|
|
@@ -421,24 +296,9 @@ export function MujocoSimProvider({
|
|
|
421
296
|
cb(model, data);
|
|
422
297
|
}
|
|
423
298
|
|
|
424
|
-
//
|
|
425
|
-
if (ikEnabledRef.current && target) {
|
|
426
|
-
ikCalculatingRef.current = true;
|
|
427
|
-
const numArm = configRef.current.numArmJoints ?? 7;
|
|
428
|
-
const currentQ: number[] = [];
|
|
429
|
-
for (let i = 0; i < numArm; i++) currentQ.push(data.qpos[i]);
|
|
430
|
-
const solution = ikSolveFnRef.current(target.position, target.quaternion, currentQ);
|
|
431
|
-
if (solution) {
|
|
432
|
-
for (let i = 0; i < numArm; i++) data.ctrl[i] = solution[i];
|
|
433
|
-
}
|
|
434
|
-
} else {
|
|
435
|
-
ikCalculatingRef.current = false;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Step physics with substeps (spec 1.1)
|
|
299
|
+
// Step physics with substeps
|
|
439
300
|
const numSubsteps = substepsRef.current;
|
|
440
301
|
if (stepsToRunRef.current > 0) {
|
|
441
|
-
// Single-step mode (spec 1.2)
|
|
442
302
|
for (let s = 0; s < stepsToRunRef.current; s++) {
|
|
443
303
|
mujoco.mj_step(model, data);
|
|
444
304
|
}
|
|
@@ -468,19 +328,16 @@ export function MujocoSimProvider({
|
|
|
468
328
|
const data = mjDataRef.current;
|
|
469
329
|
if (!model || !data) return;
|
|
470
330
|
|
|
471
|
-
gizmoAnimRef.current.active = false;
|
|
472
331
|
mujoco.mj_resetData(model, data);
|
|
473
332
|
|
|
474
333
|
const homeJoints = configRef.current.homeJoints;
|
|
475
334
|
if (homeJoints) {
|
|
476
|
-
|
|
335
|
+
const homeCount = Math.min(homeJoints.length, model.nu);
|
|
336
|
+
for (let i = 0; i < homeCount; i++) {
|
|
477
337
|
data.ctrl[i] = homeJoints[i];
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const qposAdr = model.jnt_qposadr[jointId];
|
|
482
|
-
data.qpos[qposAdr] = homeJoints[i];
|
|
483
|
-
}
|
|
338
|
+
const qposAdr = getActuatedScalarQposAdr(model, i);
|
|
339
|
+
if (qposAdr !== -1) {
|
|
340
|
+
data.qpos[qposAdr] = homeJoints[i];
|
|
484
341
|
}
|
|
485
342
|
}
|
|
486
343
|
}
|
|
@@ -488,61 +345,11 @@ export function MujocoSimProvider({
|
|
|
488
345
|
configRef.current.onReset?.(model, data);
|
|
489
346
|
mujoco.mj_forward(model, data);
|
|
490
347
|
|
|
491
|
-
|
|
492
|
-
|
|
348
|
+
// Notify composable plugins (e.g. IkController)
|
|
349
|
+
for (const cb of resetCallbacks.current) {
|
|
350
|
+
cb();
|
|
493
351
|
}
|
|
494
|
-
|
|
495
|
-
ikEnabledRef.current = false;
|
|
496
|
-
}, [mujoco, syncGizmoToSite]);
|
|
497
|
-
|
|
498
|
-
const setIkEnabled = useCallback((enabled: boolean) => {
|
|
499
|
-
ikEnabledRef.current = enabled;
|
|
500
|
-
const data = mjDataRef.current;
|
|
501
|
-
if (enabled && data && !gizmoAnimRef.current.active && ikTargetRef.current) {
|
|
502
|
-
syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
|
|
503
|
-
firstIkEnableRef.current = false;
|
|
504
|
-
}
|
|
505
|
-
}, [syncGizmoToSite]);
|
|
506
|
-
|
|
507
|
-
const syncTargetToSite = useCallback(() => {
|
|
508
|
-
const data = mjDataRef.current;
|
|
509
|
-
const target = ikTargetRef.current;
|
|
510
|
-
if (data && target) syncGizmoToSite(data, siteIdRef.current, target);
|
|
511
|
-
}, [syncGizmoToSite]);
|
|
512
|
-
|
|
513
|
-
const solveIK = useCallback(
|
|
514
|
-
(pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]): number[] | null => {
|
|
515
|
-
return ikSolveFnRef.current(pos, quat, currentQ);
|
|
516
|
-
},
|
|
517
|
-
[]
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
const moveTarget = useCallback(
|
|
521
|
-
(pos: THREE.Vector3, duration = 0) => {
|
|
522
|
-
if (!ikEnabledRef.current) setIkEnabled(true);
|
|
523
|
-
const target = ikTargetRef.current;
|
|
524
|
-
if (!target) return;
|
|
525
|
-
|
|
526
|
-
const targetPos = pos.clone();
|
|
527
|
-
const targetRot = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI, 0, 0));
|
|
528
|
-
|
|
529
|
-
if (duration > 0) {
|
|
530
|
-
const ga = gizmoAnimRef.current;
|
|
531
|
-
ga.active = true;
|
|
532
|
-
ga.startPos.copy(target.position);
|
|
533
|
-
ga.endPos.copy(targetPos);
|
|
534
|
-
ga.startRot.copy(target.quaternion);
|
|
535
|
-
ga.endRot.copy(targetRot);
|
|
536
|
-
ga.startTime = performance.now();
|
|
537
|
-
ga.duration = duration;
|
|
538
|
-
} else {
|
|
539
|
-
gizmoAnimRef.current.active = false;
|
|
540
|
-
target.position.copy(targetPos);
|
|
541
|
-
target.quaternion.copy(targetRot);
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
[setIkEnabled]
|
|
545
|
-
);
|
|
352
|
+
}, [mujoco]);
|
|
546
353
|
|
|
547
354
|
const setSpeed = useCallback((multiplier: number) => {
|
|
548
355
|
speedRef.current = multiplier;
|
|
@@ -553,17 +360,14 @@ export function MujocoSimProvider({
|
|
|
553
360
|
return pausedRef.current;
|
|
554
361
|
}, []);
|
|
555
362
|
|
|
556
|
-
// spec 1.1: declarative pause
|
|
557
363
|
const setPaused = useCallback((p: boolean) => {
|
|
558
364
|
pausedRef.current = p;
|
|
559
365
|
}, []);
|
|
560
366
|
|
|
561
|
-
// spec 1.2: single-step mode
|
|
562
367
|
const step = useCallback((n = 1) => {
|
|
563
368
|
stepsToRunRef.current = n;
|
|
564
369
|
}, []);
|
|
565
370
|
|
|
566
|
-
// spec 1.3: simulation time access
|
|
567
371
|
const getTime = useCallback((): number => {
|
|
568
372
|
return mjDataRef.current?.time ?? 0;
|
|
569
373
|
}, []);
|
|
@@ -572,7 +376,6 @@ export function MujocoSimProvider({
|
|
|
572
376
|
return mjModelRef.current?.opt?.timestep ?? 0.002;
|
|
573
377
|
}, []);
|
|
574
378
|
|
|
575
|
-
// spec 4.1: state snapshot save/restore
|
|
576
379
|
const saveState = useCallback((): StateSnapshot => {
|
|
577
380
|
const data = mjDataRef.current;
|
|
578
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) };
|
|
@@ -599,7 +402,6 @@ export function MujocoSimProvider({
|
|
|
599
402
|
mujoco.mj_forward(model, data);
|
|
600
403
|
}, [mujoco]);
|
|
601
404
|
|
|
602
|
-
// spec 4.3: qpos/qvel direct set/get
|
|
603
405
|
const setQpos = useCallback((values: Float64Array | number[]) => {
|
|
604
406
|
const model = mjModelRef.current;
|
|
605
407
|
const data = mjDataRef.current;
|
|
@@ -624,18 +426,15 @@ export function MujocoSimProvider({
|
|
|
624
426
|
return mjDataRef.current ? new Float64Array(mjDataRef.current.qvel) : new Float64Array(0);
|
|
625
427
|
}, []);
|
|
626
428
|
|
|
627
|
-
// spec 3.1: ctrl set/get
|
|
628
429
|
const setCtrl = useCallback((nameOrValues: string | Record<string, number>, value?: number) => {
|
|
629
430
|
const model = mjModelRef.current;
|
|
630
431
|
const data = mjDataRef.current;
|
|
631
432
|
if (!model || !data) return;
|
|
632
433
|
|
|
633
434
|
if (typeof nameOrValues === 'string') {
|
|
634
|
-
// Single actuator by name
|
|
635
435
|
const id = findActuatorByName(model, nameOrValues);
|
|
636
436
|
if (id >= 0 && value !== undefined) data.ctrl[id] = value;
|
|
637
437
|
} else {
|
|
638
|
-
// Batch: { name: value, ... }
|
|
639
438
|
for (const [name, val] of Object.entries(nameOrValues)) {
|
|
640
439
|
const id = findActuatorByName(model, name);
|
|
641
440
|
if (id >= 0) data.ctrl[id] = val;
|
|
@@ -647,7 +446,6 @@ export function MujocoSimProvider({
|
|
|
647
446
|
return mjDataRef.current ? new Float64Array(mjDataRef.current.ctrl) : new Float64Array(0);
|
|
648
447
|
}, []);
|
|
649
448
|
|
|
650
|
-
// spec 8.1: force/torque API
|
|
651
449
|
const applyForce = useCallback((bodyName: string, force: THREE.Vector3, point?: THREE.Vector3) => {
|
|
652
450
|
const model = mjModelRef.current;
|
|
653
451
|
const data = mjDataRef.current;
|
|
@@ -700,7 +498,6 @@ export function MujocoSimProvider({
|
|
|
700
498
|
}
|
|
701
499
|
}, []);
|
|
702
500
|
|
|
703
|
-
// spec 2.1: sensor data
|
|
704
501
|
const getSensorData = useCallback((name: string): Float64Array | null => {
|
|
705
502
|
const model = mjModelRef.current;
|
|
706
503
|
const data = mjDataRef.current;
|
|
@@ -712,7 +509,6 @@ export function MujocoSimProvider({
|
|
|
712
509
|
return new Float64Array(data.sensordata.subarray(adr, adr + dim));
|
|
713
510
|
}, []);
|
|
714
511
|
|
|
715
|
-
// spec 2.4: contacts
|
|
716
512
|
const getContacts = useCallback((): ContactInfo[] => {
|
|
717
513
|
const model = mjModelRef.current;
|
|
718
514
|
const data = mjDataRef.current;
|
|
@@ -720,24 +516,20 @@ export function MujocoSimProvider({
|
|
|
720
516
|
const contacts: ContactInfo[] = [];
|
|
721
517
|
const ncon = data.ncon;
|
|
722
518
|
for (let i = 0; i < ncon; i++) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
} catch {
|
|
734
|
-
break; // WASM contact access can fail
|
|
735
|
-
}
|
|
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
|
+
});
|
|
736
529
|
}
|
|
737
530
|
return contacts;
|
|
738
531
|
}, []);
|
|
739
532
|
|
|
740
|
-
// spec 5.1: model introspection
|
|
741
533
|
const getBodies = useCallback((): BodyInfo[] => {
|
|
742
534
|
const model = mjModelRef.current;
|
|
743
535
|
if (!model) return [];
|
|
@@ -842,7 +634,6 @@ export function MujocoSimProvider({
|
|
|
842
634
|
return result;
|
|
843
635
|
}, []);
|
|
844
636
|
|
|
845
|
-
// spec 5.3: model options
|
|
846
637
|
const getModelOption = useCallback((): ModelOptions => {
|
|
847
638
|
const model = mjModelRef.current;
|
|
848
639
|
if (!model?.opt) return { timestep: 0.002, gravity: [0, 0, -9.81], integrator: 0 };
|
|
@@ -867,7 +658,6 @@ export function MujocoSimProvider({
|
|
|
867
658
|
model.opt.timestep = dt;
|
|
868
659
|
}, []);
|
|
869
660
|
|
|
870
|
-
// spec 7.1: physics raycast
|
|
871
661
|
const raycast = useCallback((origin: THREE.Vector3, direction: THREE.Vector3, maxDist = 100): RayHit | null => {
|
|
872
662
|
const model = mjModelRef.current;
|
|
873
663
|
const data = mjDataRef.current;
|
|
@@ -894,11 +684,10 @@ export function MujocoSimProvider({
|
|
|
894
684
|
distance: dist,
|
|
895
685
|
};
|
|
896
686
|
} catch {
|
|
897
|
-
return null;
|
|
687
|
+
return null;
|
|
898
688
|
}
|
|
899
689
|
}, [mujoco]);
|
|
900
690
|
|
|
901
|
-
// spec 4.2: keyframe improvements
|
|
902
691
|
const applyKeyframe = useCallback((nameOrIndex: string | number) => {
|
|
903
692
|
const model = mjModelRef.current;
|
|
904
693
|
const data = mjDataRef.current;
|
|
@@ -922,7 +711,6 @@ export function MujocoSimProvider({
|
|
|
922
711
|
const ctrlOffset = keyId * nu;
|
|
923
712
|
for (let i = 0; i < nu; i++) data.ctrl[i] = model.key_ctrl[ctrlOffset + i];
|
|
924
713
|
|
|
925
|
-
// Also restore qvel if available (spec 4.2)
|
|
926
714
|
if (model.key_qvel) {
|
|
927
715
|
const qvelOffset = keyId * model.nv;
|
|
928
716
|
for (let i = 0; i < model.nv; i++) data.qvel[i] = model.key_qvel[qvelOffset + i];
|
|
@@ -930,10 +718,11 @@ export function MujocoSimProvider({
|
|
|
930
718
|
|
|
931
719
|
mujoco.mj_forward(model, data);
|
|
932
720
|
|
|
933
|
-
|
|
934
|
-
|
|
721
|
+
// Notify composable plugins
|
|
722
|
+
for (const cb of resetCallbacks.current) {
|
|
723
|
+
cb();
|
|
935
724
|
}
|
|
936
|
-
}, [mujoco
|
|
725
|
+
}, [mujoco]);
|
|
937
726
|
|
|
938
727
|
const getKeyframeNames = useCallback((): string[] => {
|
|
939
728
|
const model = mjModelRef.current;
|
|
@@ -949,10 +738,9 @@ export function MujocoSimProvider({
|
|
|
949
738
|
return mjModelRef.current?.nkey ?? 0;
|
|
950
739
|
}, []);
|
|
951
740
|
|
|
952
|
-
// spec 9.1: runtime model swap
|
|
953
741
|
const loadSceneApi = useCallback(async (newConfig: SceneConfig): Promise<void> => {
|
|
742
|
+
const gen = ++loadGenRef.current;
|
|
954
743
|
try {
|
|
955
|
-
// Clean up current model
|
|
956
744
|
mjModelRef.current?.delete();
|
|
957
745
|
mjDataRef.current?.delete();
|
|
958
746
|
mjModelRef.current = null;
|
|
@@ -960,30 +748,24 @@ export function MujocoSimProvider({
|
|
|
960
748
|
setStatus('loading');
|
|
961
749
|
|
|
962
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
|
+
|
|
963
758
|
mjModelRef.current = result.mjModel;
|
|
964
759
|
mjDataRef.current = result.mjData;
|
|
965
|
-
siteIdRef.current = result.siteId;
|
|
966
|
-
gripperIdRef.current = result.gripperId;
|
|
967
760
|
configRef.current = newConfig;
|
|
968
761
|
|
|
969
|
-
if (ikTargetRef.current) {
|
|
970
|
-
syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
|
|
971
|
-
}
|
|
972
762
|
setStatus('ready');
|
|
973
763
|
} catch (e) {
|
|
764
|
+
if (gen !== loadGenRef.current) return;
|
|
974
765
|
setStatus('error');
|
|
975
766
|
throw e;
|
|
976
767
|
}
|
|
977
|
-
}, [mujoco
|
|
978
|
-
|
|
979
|
-
const getGizmoStats = useCallback((): { pos: THREE.Vector3; rot: THREE.Euler } | null => {
|
|
980
|
-
const target = ikTargetRef.current;
|
|
981
|
-
if (!ikCalculatingRef.current || !target) return null;
|
|
982
|
-
return {
|
|
983
|
-
pos: target.position.clone(),
|
|
984
|
-
rot: new THREE.Euler().setFromQuaternion(target.quaternion),
|
|
985
|
-
};
|
|
986
|
-
}, []);
|
|
768
|
+
}, [mujoco]);
|
|
987
769
|
|
|
988
770
|
const getCanvasSnapshot = useCallback(
|
|
989
771
|
(width?: number, height?: number, mimeType = 'image/jpeg'): string => {
|
|
@@ -1009,9 +791,8 @@ export function MujocoSimProvider({
|
|
|
1009
791
|
virtCam.lookAt(lookAt);
|
|
1010
792
|
virtCam.updateMatrixWorld();
|
|
1011
793
|
virtCam.updateProjectionMatrix();
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
raycaster.setFromCamera(ndc, virtCam);
|
|
794
|
+
_projNdc.set(x * 2 - 1, -(y * 2 - 1));
|
|
795
|
+
_projRaycaster.setFromCamera(_projNdc, virtCam);
|
|
1015
796
|
const objects: THREE.Object3D[] = [];
|
|
1016
797
|
const scene = (camera as THREE.PerspectiveCamera).parent;
|
|
1017
798
|
if (scene) {
|
|
@@ -1019,12 +800,10 @@ export function MujocoSimProvider({
|
|
|
1019
800
|
if ((c as THREE.Mesh).isMesh) objects.push(c);
|
|
1020
801
|
});
|
|
1021
802
|
}
|
|
1022
|
-
const hits =
|
|
803
|
+
const hits = _projRaycaster.intersectObjects(objects);
|
|
1023
804
|
if (hits.length > 0) {
|
|
1024
805
|
const hitObj = hits[0].object;
|
|
1025
|
-
// Find geomId from the hit object's userData
|
|
1026
806
|
const geomId = hitObj.userData.geomID !== undefined ? hitObj.userData.geomID : -1;
|
|
1027
|
-
// Walk up to find bodyId
|
|
1028
807
|
let obj = hitObj;
|
|
1029
808
|
while (obj && obj.userData.bodyID === undefined && obj.parent) {
|
|
1030
809
|
obj = obj.parent;
|
|
@@ -1037,7 +816,7 @@ export function MujocoSimProvider({
|
|
|
1037
816
|
[camera, gl]
|
|
1038
817
|
);
|
|
1039
818
|
|
|
1040
|
-
// --- Domain randomization
|
|
819
|
+
// --- Domain randomization ---
|
|
1041
820
|
|
|
1042
821
|
const setBodyMass = useCallback((name: string, mass: number): void => {
|
|
1043
822
|
const model = mjModelRef.current;
|
|
@@ -1067,33 +846,6 @@ export function MujocoSimProvider({
|
|
|
1067
846
|
model.geom_size[id * 3 + 2] = size[2];
|
|
1068
847
|
}, []);
|
|
1069
848
|
|
|
1070
|
-
const getCameraState = useCallback((): { position: THREE.Vector3; target: THREE.Vector3 } => {
|
|
1071
|
-
return { position: camera.position.clone(), target: orbitTargetRef.current.clone() };
|
|
1072
|
-
}, [camera]);
|
|
1073
|
-
|
|
1074
|
-
const moveCameraTo = useCallback(
|
|
1075
|
-
(position: THREE.Vector3, target: THREE.Vector3, durationMs: number): Promise<void> => {
|
|
1076
|
-
return new Promise((resolve) => {
|
|
1077
|
-
const ca = cameraAnimRef.current;
|
|
1078
|
-
ca.active = true;
|
|
1079
|
-
ca.startTime = performance.now();
|
|
1080
|
-
ca.duration = durationMs;
|
|
1081
|
-
ca.startPos.copy(camera.position);
|
|
1082
|
-
ca.startRot.copy(camera.quaternion);
|
|
1083
|
-
ca.startTarget.copy(orbitTargetRef.current);
|
|
1084
|
-
ca.endPos.copy(position);
|
|
1085
|
-
ca.endTarget.copy(target);
|
|
1086
|
-
const dummyCam = (camera as THREE.PerspectiveCamera).clone();
|
|
1087
|
-
dummyCam.position.copy(position);
|
|
1088
|
-
dummyCam.lookAt(target);
|
|
1089
|
-
ca.endRot.copy(dummyCam.quaternion);
|
|
1090
|
-
ca.resolve = resolve;
|
|
1091
|
-
setTimeout(resolve, durationMs + 100);
|
|
1092
|
-
});
|
|
1093
|
-
},
|
|
1094
|
-
[camera]
|
|
1095
|
-
);
|
|
1096
|
-
|
|
1097
849
|
// --- Assemble API ---
|
|
1098
850
|
const api = useMemo<MujocoSimAPI>(
|
|
1099
851
|
() => ({
|
|
@@ -1134,15 +886,8 @@ export function MujocoSimProvider({
|
|
|
1134
886
|
getKeyframeNames,
|
|
1135
887
|
getKeyframeCount,
|
|
1136
888
|
loadScene: loadSceneApi,
|
|
1137
|
-
setIkEnabled,
|
|
1138
|
-
moveTarget,
|
|
1139
|
-
syncTargetToSite,
|
|
1140
|
-
solveIK,
|
|
1141
|
-
getGizmoStats,
|
|
1142
889
|
getCanvasSnapshot,
|
|
1143
890
|
project2DTo3D,
|
|
1144
|
-
getCameraState,
|
|
1145
|
-
moveCameraTo,
|
|
1146
891
|
setBodyMass,
|
|
1147
892
|
setGeomFriction,
|
|
1148
893
|
setGeomSize,
|
|
@@ -1157,8 +902,7 @@ export function MujocoSimProvider({
|
|
|
1157
902
|
getSensorData, getContacts, getBodies, getJoints, getGeoms, getSites,
|
|
1158
903
|
getActuatorsApi, getSensors, getModelOption, setGravity, setTimestepApi,
|
|
1159
904
|
raycast, getKeyframeNames, getKeyframeCount, loadSceneApi,
|
|
1160
|
-
|
|
1161
|
-
getCanvasSnapshot, project2DTo3D, getCameraState, moveCameraTo,
|
|
905
|
+
getCanvasSnapshot, project2DTo3D,
|
|
1162
906
|
setBodyMass, setGeomFriction, setGeomSize,
|
|
1163
907
|
]
|
|
1164
908
|
);
|
|
@@ -1172,22 +916,13 @@ export function MujocoSimProvider({
|
|
|
1172
916
|
mjDataRef,
|
|
1173
917
|
mujocoRef,
|
|
1174
918
|
configRef,
|
|
1175
|
-
siteIdRef,
|
|
1176
|
-
gripperIdRef,
|
|
1177
|
-
ikEnabledRef,
|
|
1178
|
-
ikCalculatingRef,
|
|
1179
919
|
pausedRef,
|
|
1180
920
|
speedRef,
|
|
1181
921
|
substepsRef,
|
|
1182
|
-
ikTargetRef,
|
|
1183
|
-
genericIkRef,
|
|
1184
|
-
ikSolveFnRef,
|
|
1185
|
-
firstIkEnableRef,
|
|
1186
|
-
gizmoAnimRef,
|
|
1187
|
-
cameraAnimRef,
|
|
1188
922
|
onSelectionRef,
|
|
1189
923
|
beforeStepCallbacks,
|
|
1190
924
|
afterStepCallbacks,
|
|
925
|
+
resetCallbacks,
|
|
1191
926
|
status,
|
|
1192
927
|
}),
|
|
1193
928
|
[api, status]
|