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.
@@ -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 (spec 1.1)
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 firstIkEnableRef = useRef(true);
208
- const stepsToRunRef = useRef(0); // for single-step mode (spec 1.2)
175
+ const stepsToRunRef = useRef(0);
176
+ const loadGenRef = useRef(0);
209
177
 
210
- // Interpolation state (spec 11.1)
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 (spec 1.1)
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 (spec 1.1)
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((state) => {
280
+ useFrame(() => {
378
281
  const model = mjModelRef.current;
379
282
  const data = mjDataRef.current;
380
283
  if (!model || !data) return;
381
284
 
382
- // Gizmo animation
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
- // IK
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
- for (let i = 0; i < homeJoints.length; i++) {
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
- if (model.actuator_trnid[2 * i + 1] === 1) {
490
- const jointId = model.actuator_trnid[2 * i];
491
- if (jointId >= 0 && jointId < model.njnt) {
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
- if (ikTargetRef.current) {
503
- syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
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
- }, [syncGizmoToSite]);
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
- try {
735
- const c = (data.contact as { get(i: number): { geom1: number; geom2: number; pos: Float64Array; dist: number } }).get(i);
736
- contacts.push({
737
- geom1: c.geom1,
738
- geom1Name: getName(model, model.name_geomadr[c.geom1]),
739
- geom2: c.geom2,
740
- geom2Name: getName(model, model.name_geomadr[c.geom2]),
741
- pos: [c.pos[0], c.pos[1], c.pos[2]],
742
- depth: c.dist,
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; // mj_ray may not be available in all WASM builds
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
- if (ikTargetRef.current) {
945
- syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
721
+ // Notify composable plugins
722
+ for (const cb of resetCallbacks.current) {
723
+ cb();
946
724
  }
947
- }, [mujoco, syncGizmoToSite]);
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, syncGizmoToSite]);
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
- const ndc = new THREE.Vector2(x * 2 - 1, -(y * 2 - 1));
1024
- const raycaster = new THREE.Raycaster();
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 = raycaster.intersectObjects(objects);
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 (spec 10.3) ---
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
- setIkEnabled, moveTarget, syncTargetToSite, solveIK, getGizmoStats,
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]