mujoco-react 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import loadMujoco from 'mujoco-js';
2
2
  import { createContext, forwardRef, useEffect, useContext, useState, useRef, useCallback, useMemo } from 'react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import { Canvas, useThree, useFrame } from '@react-three/fiber';
5
- import * as THREE from 'three';
5
+ import * as THREE11 from 'three';
6
6
  import { PivotControls } from '@react-three/drei';
7
7
 
8
8
  // src/core/MujocoProvider.tsx
@@ -14,14 +14,14 @@ var MujocoContext = createContext({
14
14
  function useMujoco() {
15
15
  return useContext(MujocoContext);
16
16
  }
17
- function MujocoProvider({ wasmUrl, children, onError }) {
17
+ function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
18
18
  const [status, setStatus] = useState("loading");
19
19
  const [error, setError] = useState(null);
20
20
  const moduleRef = useRef(null);
21
21
  const isMounted = useRef(true);
22
22
  useEffect(() => {
23
23
  isMounted.current = true;
24
- loadMujoco({
24
+ const wasmPromise = loadMujoco({
25
25
  ...wasmUrl ? { locateFile: (path) => path.endsWith(".wasm") ? wasmUrl : path } : {},
26
26
  printErr: (text) => {
27
27
  if (text.includes("Aborted") && isMounted.current) {
@@ -29,7 +29,11 @@ function MujocoProvider({ wasmUrl, children, onError }) {
29
29
  setStatus("error");
30
30
  }
31
31
  }
32
- }).then((inst) => {
32
+ });
33
+ const timeoutPromise = new Promise(
34
+ (_, reject) => setTimeout(() => reject(new Error(`WASM module load timed out after ${timeout}ms`)), timeout)
35
+ );
36
+ Promise.race([wasmPromise, timeoutPromise]).then((inst) => {
33
37
  if (isMounted.current) {
34
38
  moduleRef.current = inst;
35
39
  setStatus("ready");
@@ -45,7 +49,7 @@ function MujocoProvider({ wasmUrl, children, onError }) {
45
49
  return () => {
46
50
  isMounted.current = false;
47
51
  };
48
- }, [wasmUrl]);
52
+ }, [wasmUrl, timeout]);
49
53
  return /* @__PURE__ */ jsx(
50
54
  MujocoContext.Provider,
51
55
  {
@@ -55,229 +59,12 @@ function MujocoProvider({ wasmUrl, children, onError }) {
55
59
  );
56
60
  }
57
61
 
58
- // src/core/GenericIK.ts
59
- var DEFAULTS = {
60
- maxIterations: 50,
61
- damping: 0.01,
62
- tolerance: 1e-3,
63
- epsilon: 1e-6,
64
- posWeight: 1,
65
- rotWeight: 0.3
66
- };
67
- var GenericIK = class {
68
- mujoco;
69
- constructor(mujoco) {
70
- this.mujoco = mujoco;
71
- }
72
- /**
73
- * Solve IK for a target 6-DOF pose.
74
- * @param model MuJoCo model
75
- * @param data MuJoCo data (qpos will be temporarily modified, then restored)
76
- * @param siteId Index of the end-effector site to control
77
- * @param numJoints Number of arm joints (assumes qpos[0..numJoints-1])
78
- * @param targetPos Target position in world frame
79
- * @param targetQuat Target orientation in world frame
80
- * @param currentQ Current joint angles (length = numJoints)
81
- * @param opts Optional solver parameters
82
- * @returns Joint angles array, or null if solver diverged
83
- */
84
- solve(model, data, siteId, numJoints, targetPos, targetQuat, currentQ, opts) {
85
- const o = { ...DEFAULTS, ...opts };
86
- const n = numJoints;
87
- const savedQpos = new Float64Array(data.qpos.length);
88
- savedQpos.set(data.qpos);
89
- const R_target = quatToMat3(targetQuat);
90
- const q = new Float64Array(n);
91
- for (let i = 0; i < n; i++) q[i] = currentQ[i];
92
- const J = new Float64Array(6 * n);
93
- const JJt = new Float64Array(36);
94
- const rhs = new Float64Array(6);
95
- const x = new Float64Array(6);
96
- const dq = new Float64Array(n);
97
- const baseSitePos = new Float64Array(3);
98
- const baseSiteMat = new Float64Array(9);
99
- const pertSitePos = new Float64Array(3);
100
- const pertSiteMat = new Float64Array(9);
101
- let bestQ = null;
102
- let bestErr = Infinity;
103
- for (let iter = 0; iter < o.maxIterations; iter++) {
104
- for (let i = 0; i < n; i++) data.qpos[i] = q[i];
105
- this.mujoco.mj_forward(model, data);
106
- const sp = data.site_xpos;
107
- const sm = data.site_xmat;
108
- const off3 = siteId * 3;
109
- const off9 = siteId * 9;
110
- for (let i = 0; i < 3; i++) baseSitePos[i] = sp[off3 + i];
111
- for (let i = 0; i < 9; i++) baseSiteMat[i] = sm[off9 + i];
112
- const posErr0 = targetPos.x - baseSitePos[0];
113
- const posErr1 = targetPos.y - baseSitePos[1];
114
- const posErr2 = targetPos.z - baseSitePos[2];
115
- const rotErr = orientationError(baseSiteMat, R_target);
116
- const error = [
117
- posErr0 * o.posWeight,
118
- posErr1 * o.posWeight,
119
- posErr2 * o.posWeight,
120
- rotErr[0] * o.rotWeight,
121
- rotErr[1] * o.rotWeight,
122
- rotErr[2] * o.rotWeight
123
- ];
124
- const errNorm = Math.sqrt(
125
- error[0] * error[0] + error[1] * error[1] + error[2] * error[2] + error[3] * error[3] + error[4] * error[4] + error[5] * error[5]
126
- );
127
- if (errNorm < bestErr) {
128
- bestErr = errNorm;
129
- bestQ = Array.from(q);
130
- }
131
- if (errNorm < o.tolerance) break;
132
- for (let j = 0; j < n; j++) {
133
- const saved = data.qpos[j];
134
- data.qpos[j] = q[j] + o.epsilon;
135
- this.mujoco.mj_forward(model, data);
136
- for (let i = 0; i < 3; i++) pertSitePos[i] = sp[off3 + i];
137
- for (let i = 0; i < 9; i++) pertSiteMat[i] = sm[off9 + i];
138
- J[0 * n + j] = (pertSitePos[0] - baseSitePos[0]) / o.epsilon * o.posWeight;
139
- J[1 * n + j] = (pertSitePos[1] - baseSitePos[1]) / o.epsilon * o.posWeight;
140
- J[2 * n + j] = (pertSitePos[2] - baseSitePos[2]) / o.epsilon * o.posWeight;
141
- const dRot = angularDelta(baseSiteMat, pertSiteMat);
142
- J[3 * n + j] = dRot[0] / o.epsilon * o.rotWeight;
143
- J[4 * n + j] = dRot[1] / o.epsilon * o.rotWeight;
144
- J[5 * n + j] = dRot[2] / o.epsilon * o.rotWeight;
145
- data.qpos[j] = saved;
146
- }
147
- for (let i = 0; i < n; i++) data.qpos[i] = q[i];
148
- for (let r = 0; r < 6; r++) {
149
- for (let c = 0; c < 6; c++) {
150
- let sum = 0;
151
- for (let k = 0; k < n; k++) {
152
- sum += J[r * n + k] * J[c * n + k];
153
- }
154
- JJt[r * 6 + c] = sum + (r === c ? o.damping : 0);
155
- }
156
- }
157
- for (let i = 0; i < 6; i++) rhs[i] = error[i];
158
- solve6x6(JJt, rhs, x);
159
- for (let j = 0; j < n; j++) {
160
- let sum = 0;
161
- for (let r = 0; r < 6; r++) {
162
- sum += J[r * n + j] * x[r];
163
- }
164
- dq[j] = sum;
165
- }
166
- for (let i = 0; i < n; i++) q[i] += dq[i];
167
- }
168
- data.qpos.set(savedQpos);
169
- this.mujoco.mj_forward(model, data);
170
- return bestQ;
171
- }
172
- };
173
- function quatToMat3(q) {
174
- const m = new Float64Array(9);
175
- const x = q.x, y = q.y, z = q.z, w = q.w;
176
- const xx = x * x, yy = y * y, zz = z * z;
177
- const xy = x * y, xz = x * z, yz = y * z;
178
- const wx = w * x, wy = w * y, wz = w * z;
179
- m[0] = 1 - 2 * (yy + zz);
180
- m[1] = 2 * (xy - wz);
181
- m[2] = 2 * (xz + wy);
182
- m[3] = 2 * (xy + wz);
183
- m[4] = 1 - 2 * (xx + zz);
184
- m[5] = 2 * (yz - wx);
185
- m[6] = 2 * (xz - wy);
186
- m[7] = 2 * (yz + wx);
187
- m[8] = 1 - 2 * (xx + yy);
188
- return m;
189
- }
190
- function orientationError(R_cur, R_tgt) {
191
- const Re = new Float64Array(9);
192
- for (let i = 0; i < 3; i++) {
193
- for (let j = 0; j < 3; j++) {
194
- let s2 = 0;
195
- for (let k = 0; k < 3; k++) {
196
- s2 += R_tgt[i * 3 + k] * R_cur[j * 3 + k];
197
- }
198
- Re[i * 3 + j] = s2;
199
- }
200
- }
201
- const trace = Re[0] + Re[4] + Re[8];
202
- const cosAngle = Math.max(-1, Math.min(1, (trace - 1) * 0.5));
203
- const angle = Math.acos(cosAngle);
204
- if (angle < 1e-6) {
205
- return [0, 0, 0];
206
- }
207
- if (angle > Math.PI - 1e-6) {
208
- return [
209
- 0.5 * (Re[7] - Re[5]),
210
- 0.5 * (Re[2] - Re[6]),
211
- 0.5 * (Re[3] - Re[1])
212
- ];
213
- }
214
- const s = angle / (2 * Math.sin(angle));
215
- return [
216
- s * (Re[7] - Re[5]),
217
- s * (Re[2] - Re[6]),
218
- s * (Re[3] - Re[1])
219
- ];
220
- }
221
- function angularDelta(R_base, R_pert) {
222
- const dR = new Float64Array(9);
223
- for (let i = 0; i < 3; i++) {
224
- for (let j = 0; j < 3; j++) {
225
- let s = 0;
226
- for (let k = 0; k < 3; k++) {
227
- s += R_pert[i * 3 + k] * R_base[j * 3 + k];
228
- }
229
- dR[i * 3 + j] = s;
230
- }
231
- }
232
- return [
233
- 0.5 * (dR[7] - dR[5]),
234
- 0.5 * (dR[2] - dR[6]),
235
- 0.5 * (dR[3] - dR[1])
236
- ];
237
- }
238
- function solve6x6(A, b, x) {
239
- const N = 6;
240
- const a = new Float64Array(A);
241
- const r = new Float64Array(b);
242
- for (let col = 0; col < N; col++) {
243
- let maxVal = Math.abs(a[col * N + col]);
244
- let maxRow = col;
245
- for (let row = col + 1; row < N; row++) {
246
- const val = Math.abs(a[row * N + col]);
247
- if (val > maxVal) {
248
- maxVal = val;
249
- maxRow = row;
250
- }
251
- }
252
- if (maxRow !== col) {
253
- for (let k = 0; k < N; k++) {
254
- const tmp2 = a[col * N + k];
255
- a[col * N + k] = a[maxRow * N + k];
256
- a[maxRow * N + k] = tmp2;
257
- }
258
- const tmp = r[col];
259
- r[col] = r[maxRow];
260
- r[maxRow] = tmp;
261
- }
262
- const pivot = a[col * N + col];
263
- if (Math.abs(pivot) < 1e-12) {
264
- x.fill(0);
265
- return;
266
- }
267
- for (let row = col + 1; row < N; row++) {
268
- const factor = a[row * N + col] / pivot;
269
- for (let k = col; k < N; k++) {
270
- a[row * N + k] -= factor * a[col * N + k];
271
- }
272
- r[row] -= factor * r[col];
273
- }
274
- }
275
- for (let row = N - 1; row >= 0; row--) {
276
- let sum = r[row];
277
- for (let k = row + 1; k < N; k++) {
278
- sum -= a[row * N + k] * x[k];
279
- }
280
- x[row] = sum / a[row * N + row];
62
+ // src/types.ts
63
+ function getContact(data, i) {
64
+ try {
65
+ return data.contact.get(i);
66
+ } catch {
67
+ return void 0;
281
68
  }
282
69
  }
283
70
 
@@ -340,6 +127,16 @@ function findTendonByName(mjModel, name) {
340
127
  }
341
128
  return -1;
342
129
  }
130
+ function getActuatedScalarQposAdr(mjModel, actuatorId) {
131
+ if (actuatorId < 0 || actuatorId >= mjModel.nu) return -1;
132
+ const trnType = mjModel.actuator_trntype?.[actuatorId];
133
+ if (trnType !== void 0 && trnType !== 0 && trnType !== 1) return -1;
134
+ const jointId = mjModel.actuator_trnid[2 * actuatorId];
135
+ if (jointId < 0 || jointId >= mjModel.njnt) return -1;
136
+ const jntType = mjModel.jnt_type[jointId];
137
+ if (jntType !== 2 && jntType !== 3) return -1;
138
+ return mjModel.jnt_qposadr[jointId];
139
+ }
343
140
  function sceneObjectToXml(obj) {
344
141
  const joint = obj.freejoint ? "<freejoint/>" : "";
345
142
  const pos = obj.position.map((v) => v.toFixed(3)).join(" ");
@@ -390,7 +187,13 @@ async function loadScene(mujoco, config, onProgress) {
390
187
  for (const patch of config.xmlPatches ?? []) {
391
188
  if (fname.endsWith(patch.target) || fname === patch.target) {
392
189
  if (patch.replace) {
393
- text = text.replace(patch.replace[0], patch.replace[1]);
190
+ const [from, to] = patch.replace;
191
+ if (text.includes(from)) {
192
+ text = text.replace(from, to);
193
+ } else {
194
+ const preview = from.length > 80 ? `${from.slice(0, 80)}...` : from;
195
+ console.warn(`XML patch replace pattern not found in ${fname}: "${preview}"`);
196
+ }
394
197
  }
395
198
  if (patch.inject && patch.injectAfter) {
396
199
  const idx = text.indexOf(patch.injectAfter);
@@ -398,7 +201,12 @@ async function loadScene(mujoco, config, onProgress) {
398
201
  const tagEnd = text.indexOf(">", idx + patch.injectAfter.length);
399
202
  if (tagEnd !== -1) {
400
203
  text = text.slice(0, tagEnd + 1) + patch.inject + text.slice(tagEnd + 1);
204
+ } else {
205
+ console.warn(`XML patch inject failed in ${fname}: could not find tag end after "${patch.injectAfter}"`);
401
206
  }
207
+ } else {
208
+ const preview = patch.injectAfter.length > 80 ? `${patch.injectAfter.slice(0, 80)}...` : patch.injectAfter;
209
+ console.warn(`XML patch inject anchor not found in ${fname}: "${preview}"`);
402
210
  }
403
211
  }
404
212
  }
@@ -420,14 +228,12 @@ async function loadScene(mujoco, config, onProgress) {
420
228
  const siteId = findSiteByName(mjModel, config.tcpSiteName ?? "tcp");
421
229
  const gripperId = findActuatorByName(mjModel, config.gripperActuatorName ?? "gripper");
422
230
  if (config.homeJoints) {
423
- for (let i = 0; i < config.homeJoints.length; i++) {
231
+ const homeCount = Math.min(config.homeJoints.length, mjModel.nu);
232
+ for (let i = 0; i < homeCount; i++) {
424
233
  mjData.ctrl[i] = config.homeJoints[i];
425
- if (mjModel.actuator_trnid[2 * i + 1] === 1) {
426
- const jointId = mjModel.actuator_trnid[2 * i];
427
- if (jointId >= 0 && jointId < mjModel.njnt) {
428
- const qposAdr = mjModel.jnt_qposadr[jointId];
429
- mjData.qpos[qposAdr] = config.homeJoints[i];
430
- }
234
+ const qposAdr = getActuatedScalarQposAdr(mjModel, i);
235
+ if (qposAdr !== -1) {
236
+ mjData.qpos[qposAdr] = config.homeJoints[i];
431
237
  }
432
238
  }
433
239
  }
@@ -437,8 +243,9 @@ async function loadScene(mujoco, config, onProgress) {
437
243
  function scanDependencies(xmlString, currentFile, parser, downloaded, queue) {
438
244
  const xmlDoc = parser.parseFromString(xmlString, "text/xml");
439
245
  const compiler = xmlDoc.querySelector("compiler");
440
- const meshDir = compiler?.getAttribute("meshdir") || "";
441
- const textureDir = compiler?.getAttribute("texturedir") || "";
246
+ const assetDir = compiler?.getAttribute("assetdir") || "";
247
+ const meshDir = compiler?.getAttribute("meshdir") || assetDir;
248
+ const textureDir = compiler?.getAttribute("texturedir") || assetDir;
442
249
  const currentDir = currentFile.includes("/") ? currentFile.substring(0, currentFile.lastIndexOf("/") + 1) : "";
443
250
  xmlDoc.querySelectorAll("[file]").forEach((el) => {
444
251
  const fileAttr = el.getAttribute("file");
@@ -519,6 +326,8 @@ var _applyPoint = new Float64Array(3);
519
326
  var _rayPnt = new Float64Array(3);
520
327
  var _rayVec = new Float64Array(3);
521
328
  var _rayGeomId = new Int32Array(1);
329
+ var _projRaycaster = new THREE11.Raycaster();
330
+ var _projNdc = new THREE11.Vector2();
522
331
  var MujocoSimContext = createContext(null);
523
332
  function useMujocoSim() {
524
333
  const ctx = useContext(MujocoSimContext);
@@ -572,16 +381,12 @@ function MujocoSimProvider({
572
381
  const mjDataRef = useRef(null);
573
382
  const mujocoRef = useRef(mujoco);
574
383
  const configRef = useRef(config);
575
- const siteIdRef = useRef(-1);
576
- const gripperIdRef = useRef(-1);
577
- const ikEnabledRef = useRef(false);
578
- const ikCalculatingRef = useRef(false);
579
384
  const pausedRef = useRef(paused ?? false);
580
385
  const speedRef = useRef(speed ?? 1);
581
386
  const substepsRef = useRef(substeps ?? 1);
582
387
  const interpolateRef = useRef(interpolate ?? false);
583
- const firstIkEnableRef = useRef(true);
584
388
  const stepsToRunRef = useRef(0);
389
+ const loadGenRef = useRef(0);
585
390
  useRef(null);
586
391
  useRef(null);
587
392
  useRef(0);
@@ -591,6 +396,7 @@ function MujocoSimProvider({
591
396
  onStepRef.current = onStep;
592
397
  const beforeStepCallbacks = useRef(/* @__PURE__ */ new Set());
593
398
  const afterStepCallbacks = useRef(/* @__PURE__ */ new Set());
399
+ const resetCallbacks = useRef(/* @__PURE__ */ new Set());
594
400
  configRef.current = config;
595
401
  useEffect(() => {
596
402
  pausedRef.current = paused ?? false;
@@ -618,74 +424,6 @@ function MujocoSimProvider({
618
424
  if (!model?.opt) return;
619
425
  model.opt.timestep = timestep;
620
426
  }, [timestep]);
621
- const ikTargetRef = useRef(new THREE.Group());
622
- const genericIkRef = useRef(new GenericIK(mujoco));
623
- const gizmoAnimRef = useRef({
624
- active: false,
625
- startPos: new THREE.Vector3(),
626
- endPos: new THREE.Vector3(),
627
- startRot: new THREE.Quaternion(),
628
- endRot: new THREE.Quaternion(),
629
- startTime: 0,
630
- duration: 1e3
631
- });
632
- const cameraAnimRef = useRef({
633
- active: false,
634
- startPos: new THREE.Vector3(),
635
- endPos: new THREE.Vector3(),
636
- startRot: new THREE.Quaternion(),
637
- endRot: new THREE.Quaternion(),
638
- startTarget: new THREE.Vector3(),
639
- endTarget: new THREE.Vector3(),
640
- startTime: 0,
641
- duration: 0,
642
- resolve: null
643
- });
644
- const orbitTargetRef = useRef(new THREE.Vector3(0, 0, 0));
645
- const syncGizmoToSite = useCallback((data, siteId, target) => {
646
- if (siteId === -1) return;
647
- const sitePos = data.site_xpos.subarray(siteId * 3, siteId * 3 + 3);
648
- const siteMat = data.site_xmat.subarray(siteId * 9, siteId * 9 + 9);
649
- target.position.set(sitePos[0], sitePos[1], sitePos[2]);
650
- const m = new THREE.Matrix4().set(
651
- siteMat[0],
652
- siteMat[1],
653
- siteMat[2],
654
- 0,
655
- siteMat[3],
656
- siteMat[4],
657
- siteMat[5],
658
- 0,
659
- siteMat[6],
660
- siteMat[7],
661
- siteMat[8],
662
- 0,
663
- 0,
664
- 0,
665
- 0,
666
- 1
667
- );
668
- target.quaternion.setFromRotationMatrix(m);
669
- }, []);
670
- const ikSolveFn = useCallback(
671
- (pos, quat, currentQ) => {
672
- const model = mjModelRef.current;
673
- const data = mjDataRef.current;
674
- if (!model || !data || siteIdRef.current === -1) return null;
675
- return genericIkRef.current.solve(
676
- model,
677
- data,
678
- siteIdRef.current,
679
- configRef.current.numArmJoints ?? 7,
680
- pos,
681
- quat,
682
- currentQ
683
- );
684
- },
685
- []
686
- );
687
- const ikSolveFnRef = useRef(ikSolveFn);
688
- ikSolveFnRef.current = ikSolveFn;
689
427
  useEffect(() => {
690
428
  let disposed = false;
691
429
  (async () => {
@@ -698,8 +436,6 @@ function MujocoSimProvider({
698
436
  }
699
437
  mjModelRef.current = result.mjModel;
700
438
  mjDataRef.current = result.mjData;
701
- siteIdRef.current = result.siteId;
702
- gripperIdRef.current = result.gripperId;
703
439
  if (gravity && result.mjModel.opt?.gravity) {
704
440
  result.mjModel.opt.gravity[0] = gravity[0];
705
441
  result.mjModel.opt.gravity[1] = gravity[1];
@@ -708,9 +444,6 @@ function MujocoSimProvider({
708
444
  if (timestep !== void 0 && result.mjModel.opt) {
709
445
  result.mjModel.opt.timestep = timestep;
710
446
  }
711
- if (ikTargetRef.current) {
712
- syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
713
- }
714
447
  setStatus("ready");
715
448
  } catch (e) {
716
449
  if (!disposed) {
@@ -744,42 +477,10 @@ function MujocoSimProvider({
744
477
  }
745
478
  }
746
479
  }, [status]);
747
- useFrame((state) => {
480
+ useFrame(() => {
748
481
  const model = mjModelRef.current;
749
482
  const data = mjDataRef.current;
750
483
  if (!model || !data) return;
751
- const ga = gizmoAnimRef.current;
752
- const target = ikTargetRef.current;
753
- if (ga.active && target) {
754
- const now = performance.now();
755
- const elapsed = now - ga.startTime;
756
- const t = Math.min(elapsed / ga.duration, 1);
757
- const ease = 1 - Math.pow(1 - t, 3);
758
- target.position.lerpVectors(ga.startPos, ga.endPos, ease);
759
- target.quaternion.slerpQuaternions(ga.startRot, ga.endRot, ease);
760
- if (t >= 1) ga.active = false;
761
- }
762
- const ca = cameraAnimRef.current;
763
- if (ca.active) {
764
- const now = performance.now();
765
- const progress = Math.min((now - ca.startTime) / ca.duration, 1);
766
- const ease = progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2;
767
- camera.position.lerpVectors(ca.startPos, ca.endPos, ease);
768
- camera.quaternion.slerpQuaternions(ca.startRot, ca.endRot, ease);
769
- orbitTargetRef.current.lerpVectors(ca.startTarget, ca.endTarget, ease);
770
- const orbitControls = state.controls;
771
- if (orbitControls?.target) {
772
- orbitControls.target.copy(orbitTargetRef.current);
773
- }
774
- if (progress >= 1) {
775
- ca.active = false;
776
- camera.position.copy(ca.endPos);
777
- camera.quaternion.copy(ca.endRot);
778
- orbitTargetRef.current.copy(ca.endTarget);
779
- ca.resolve?.();
780
- ca.resolve = null;
781
- }
782
- }
783
484
  const shouldStep = !pausedRef.current || stepsToRunRef.current > 0;
784
485
  if (!shouldStep) return;
785
486
  for (let i = 0; i < model.nv; i++) {
@@ -788,18 +489,6 @@ function MujocoSimProvider({
788
489
  for (const cb of beforeStepCallbacks.current) {
789
490
  cb(model, data);
790
491
  }
791
- if (ikEnabledRef.current && target) {
792
- ikCalculatingRef.current = true;
793
- const numArm = configRef.current.numArmJoints ?? 7;
794
- const currentQ = [];
795
- for (let i = 0; i < numArm; i++) currentQ.push(data.qpos[i]);
796
- const solution = ikSolveFnRef.current(target.position, target.quaternion, currentQ);
797
- if (solution) {
798
- for (let i = 0; i < numArm; i++) data.ctrl[i] = solution[i];
799
- }
800
- } else {
801
- ikCalculatingRef.current = false;
802
- }
803
492
  const numSubsteps = substepsRef.current;
804
493
  if (stepsToRunRef.current > 0) {
805
494
  for (let s = 0; s < stepsToRunRef.current; s++) {
@@ -824,72 +513,24 @@ function MujocoSimProvider({
824
513
  const model = mjModelRef.current;
825
514
  const data = mjDataRef.current;
826
515
  if (!model || !data) return;
827
- gizmoAnimRef.current.active = false;
828
516
  mujoco.mj_resetData(model, data);
829
517
  const homeJoints = configRef.current.homeJoints;
830
518
  if (homeJoints) {
831
- for (let i = 0; i < homeJoints.length; i++) {
519
+ const homeCount = Math.min(homeJoints.length, model.nu);
520
+ for (let i = 0; i < homeCount; i++) {
832
521
  data.ctrl[i] = homeJoints[i];
833
- if (model.actuator_trnid[2 * i + 1] === 1) {
834
- const jointId = model.actuator_trnid[2 * i];
835
- if (jointId >= 0 && jointId < model.njnt) {
836
- const qposAdr = model.jnt_qposadr[jointId];
837
- data.qpos[qposAdr] = homeJoints[i];
838
- }
522
+ const qposAdr = getActuatedScalarQposAdr(model, i);
523
+ if (qposAdr !== -1) {
524
+ data.qpos[qposAdr] = homeJoints[i];
839
525
  }
840
526
  }
841
527
  }
842
528
  configRef.current.onReset?.(model, data);
843
529
  mujoco.mj_forward(model, data);
844
- if (ikTargetRef.current) {
845
- syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
846
- }
847
- firstIkEnableRef.current = true;
848
- ikEnabledRef.current = false;
849
- }, [mujoco, syncGizmoToSite]);
850
- const setIkEnabled = useCallback((enabled) => {
851
- ikEnabledRef.current = enabled;
852
- const data = mjDataRef.current;
853
- if (enabled && data && !gizmoAnimRef.current.active && ikTargetRef.current) {
854
- syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
855
- firstIkEnableRef.current = false;
530
+ for (const cb of resetCallbacks.current) {
531
+ cb();
856
532
  }
857
- }, [syncGizmoToSite]);
858
- const syncTargetToSite = useCallback(() => {
859
- const data = mjDataRef.current;
860
- const target = ikTargetRef.current;
861
- if (data && target) syncGizmoToSite(data, siteIdRef.current, target);
862
- }, [syncGizmoToSite]);
863
- const solveIK = useCallback(
864
- (pos, quat, currentQ) => {
865
- return ikSolveFnRef.current(pos, quat, currentQ);
866
- },
867
- []
868
- );
869
- const moveTarget = useCallback(
870
- (pos, duration = 0) => {
871
- if (!ikEnabledRef.current) setIkEnabled(true);
872
- const target = ikTargetRef.current;
873
- if (!target) return;
874
- const targetPos = pos.clone();
875
- const targetRot = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI, 0, 0));
876
- if (duration > 0) {
877
- const ga = gizmoAnimRef.current;
878
- ga.active = true;
879
- ga.startPos.copy(target.position);
880
- ga.endPos.copy(targetPos);
881
- ga.startRot.copy(target.quaternion);
882
- ga.endRot.copy(targetRot);
883
- ga.startTime = performance.now();
884
- ga.duration = duration;
885
- } else {
886
- gizmoAnimRef.current.active = false;
887
- target.position.copy(targetPos);
888
- target.quaternion.copy(targetRot);
889
- }
890
- },
891
- [setIkEnabled]
892
- );
533
+ }, [mujoco]);
893
534
  const setSpeed = useCallback((multiplier) => {
894
535
  speedRef.current = multiplier;
895
536
  }, []);
@@ -1051,19 +692,16 @@ function MujocoSimProvider({
1051
692
  const contacts = [];
1052
693
  const ncon = data.ncon;
1053
694
  for (let i = 0; i < ncon; i++) {
1054
- try {
1055
- const c = data.contact.get(i);
1056
- contacts.push({
1057
- geom1: c.geom1,
1058
- geom1Name: getName(model, model.name_geomadr[c.geom1]),
1059
- geom2: c.geom2,
1060
- geom2Name: getName(model, model.name_geomadr[c.geom2]),
1061
- pos: [c.pos[0], c.pos[1], c.pos[2]],
1062
- depth: c.dist
1063
- });
1064
- } catch {
1065
- break;
1066
- }
695
+ const c = getContact(data, i);
696
+ if (!c) break;
697
+ contacts.push({
698
+ geom1: c.geom1,
699
+ geom1Name: getName(model, model.name_geomadr[c.geom1]),
700
+ geom2: c.geom2,
701
+ geom2Name: getName(model, model.name_geomadr[c.geom2]),
702
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
703
+ depth: c.dist
704
+ });
1067
705
  }
1068
706
  return contacts;
1069
707
  }, []);
@@ -1202,7 +840,7 @@ function MujocoSimProvider({
1202
840
  const geomId = _rayGeomId[0];
1203
841
  const bodyId = geomId >= 0 ? model.geom_bodyid[geomId] : -1;
1204
842
  return {
1205
- point: new THREE.Vector3(
843
+ point: new THREE11.Vector3(
1206
844
  origin.x + dir.x * dist,
1207
845
  origin.y + dir.y * dist,
1208
846
  origin.z + dir.z * dist
@@ -1240,10 +878,10 @@ function MujocoSimProvider({
1240
878
  for (let i = 0; i < model.nv; i++) data.qvel[i] = model.key_qvel[qvelOffset + i];
1241
879
  }
1242
880
  mujoco.mj_forward(model, data);
1243
- if (ikTargetRef.current) {
1244
- syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
881
+ for (const cb of resetCallbacks.current) {
882
+ cb();
1245
883
  }
1246
- }, [mujoco, syncGizmoToSite]);
884
+ }, [mujoco]);
1247
885
  const getKeyframeNames = useCallback(() => {
1248
886
  const model = mjModelRef.current;
1249
887
  if (!model) return [];
@@ -1257,6 +895,7 @@ function MujocoSimProvider({
1257
895
  return mjModelRef.current?.nkey ?? 0;
1258
896
  }, []);
1259
897
  const loadSceneApi = useCallback(async (newConfig) => {
898
+ const gen = ++loadGenRef.current;
1260
899
  try {
1261
900
  mjModelRef.current?.delete();
1262
901
  mjDataRef.current?.delete();
@@ -1264,28 +903,21 @@ function MujocoSimProvider({
1264
903
  mjDataRef.current = null;
1265
904
  setStatus("loading");
1266
905
  const result = await loadScene(mujoco, newConfig);
906
+ if (gen !== loadGenRef.current) {
907
+ result.mjModel.delete();
908
+ result.mjData.delete();
909
+ return;
910
+ }
1267
911
  mjModelRef.current = result.mjModel;
1268
912
  mjDataRef.current = result.mjData;
1269
- siteIdRef.current = result.siteId;
1270
- gripperIdRef.current = result.gripperId;
1271
913
  configRef.current = newConfig;
1272
- if (ikTargetRef.current) {
1273
- syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
1274
- }
1275
914
  setStatus("ready");
1276
915
  } catch (e) {
916
+ if (gen !== loadGenRef.current) return;
1277
917
  setStatus("error");
1278
918
  throw e;
1279
919
  }
1280
- }, [mujoco, syncGizmoToSite]);
1281
- const getGizmoStats = useCallback(() => {
1282
- const target = ikTargetRef.current;
1283
- if (!ikCalculatingRef.current || !target) return null;
1284
- return {
1285
- pos: target.position.clone(),
1286
- rot: new THREE.Euler().setFromQuaternion(target.quaternion)
1287
- };
1288
- }, []);
920
+ }, [mujoco]);
1289
921
  const getCanvasSnapshot = useCallback(
1290
922
  (width, height, mimeType = "image/jpeg") => {
1291
923
  if (width && height) {
@@ -1309,9 +941,8 @@ function MujocoSimProvider({
1309
941
  virtCam.lookAt(lookAt);
1310
942
  virtCam.updateMatrixWorld();
1311
943
  virtCam.updateProjectionMatrix();
1312
- const ndc = new THREE.Vector2(x * 2 - 1, -(y * 2 - 1));
1313
- const raycaster = new THREE.Raycaster();
1314
- raycaster.setFromCamera(ndc, virtCam);
944
+ _projNdc.set(x * 2 - 1, -(y * 2 - 1));
945
+ _projRaycaster.setFromCamera(_projNdc, virtCam);
1315
946
  const objects = [];
1316
947
  const scene = camera.parent;
1317
948
  if (scene) {
@@ -1319,7 +950,7 @@ function MujocoSimProvider({
1319
950
  if (c.isMesh) objects.push(c);
1320
951
  });
1321
952
  }
1322
- const hits = raycaster.intersectObjects(objects);
953
+ const hits = _projRaycaster.intersectObjects(objects);
1323
954
  if (hits.length > 0) {
1324
955
  const hitObj = hits[0].object;
1325
956
  const geomId = hitObj.userData.geomID !== void 0 ? hitObj.userData.geomID : -1;
@@ -1359,31 +990,6 @@ function MujocoSimProvider({
1359
990
  model.geom_size[id * 3 + 1] = size[1];
1360
991
  model.geom_size[id * 3 + 2] = size[2];
1361
992
  }, []);
1362
- const getCameraState = useCallback(() => {
1363
- return { position: camera.position.clone(), target: orbitTargetRef.current.clone() };
1364
- }, [camera]);
1365
- const moveCameraTo = useCallback(
1366
- (position, target, durationMs) => {
1367
- return new Promise((resolve) => {
1368
- const ca = cameraAnimRef.current;
1369
- ca.active = true;
1370
- ca.startTime = performance.now();
1371
- ca.duration = durationMs;
1372
- ca.startPos.copy(camera.position);
1373
- ca.startRot.copy(camera.quaternion);
1374
- ca.startTarget.copy(orbitTargetRef.current);
1375
- ca.endPos.copy(position);
1376
- ca.endTarget.copy(target);
1377
- const dummyCam = camera.clone();
1378
- dummyCam.position.copy(position);
1379
- dummyCam.lookAt(target);
1380
- ca.endRot.copy(dummyCam.quaternion);
1381
- ca.resolve = resolve;
1382
- setTimeout(resolve, durationMs + 100);
1383
- });
1384
- },
1385
- [camera]
1386
- );
1387
993
  const api = useMemo(
1388
994
  () => ({
1389
995
  get status() {
@@ -1425,15 +1031,8 @@ function MujocoSimProvider({
1425
1031
  getKeyframeNames,
1426
1032
  getKeyframeCount,
1427
1033
  loadScene: loadSceneApi,
1428
- setIkEnabled,
1429
- moveTarget,
1430
- syncTargetToSite,
1431
- solveIK,
1432
- getGizmoStats,
1433
1034
  getCanvasSnapshot,
1434
1035
  project2DTo3D,
1435
- getCameraState,
1436
- moveCameraTo,
1437
1036
  setBodyMass,
1438
1037
  setGeomFriction,
1439
1038
  setGeomSize,
@@ -1478,110 +1077,578 @@ function MujocoSimProvider({
1478
1077
  getKeyframeNames,
1479
1078
  getKeyframeCount,
1480
1079
  loadSceneApi,
1481
- setIkEnabled,
1482
- moveTarget,
1483
- syncTargetToSite,
1484
- solveIK,
1485
- getGizmoStats,
1486
1080
  getCanvasSnapshot,
1487
1081
  project2DTo3D,
1488
- getCameraState,
1489
- moveCameraTo,
1490
1082
  setBodyMass,
1491
1083
  setGeomFriction,
1492
1084
  setGeomSize
1493
1085
  ]
1494
1086
  );
1495
- const apiRef = useRef(api);
1496
- apiRef.current = api;
1087
+ const apiRef = useRef(api);
1088
+ apiRef.current = api;
1089
+ const contextValue = useMemo(
1090
+ () => ({
1091
+ api,
1092
+ mjModelRef,
1093
+ mjDataRef,
1094
+ mujocoRef,
1095
+ configRef,
1096
+ pausedRef,
1097
+ speedRef,
1098
+ substepsRef,
1099
+ onSelectionRef,
1100
+ beforeStepCallbacks,
1101
+ afterStepCallbacks,
1102
+ resetCallbacks,
1103
+ status
1104
+ }),
1105
+ [api, status]
1106
+ );
1107
+ return /* @__PURE__ */ jsx(MujocoSimContext.Provider, { value: contextValue, children });
1108
+ }
1109
+ var MujocoCanvas = forwardRef(
1110
+ function MujocoCanvas2({
1111
+ config,
1112
+ onReady,
1113
+ onError,
1114
+ onStep,
1115
+ onSelection,
1116
+ // Declarative physics config (spec 1.1)
1117
+ gravity,
1118
+ timestep,
1119
+ substeps,
1120
+ paused,
1121
+ speed,
1122
+ interpolate,
1123
+ gravityCompensation,
1124
+ mjcfLights,
1125
+ children,
1126
+ ...canvasProps
1127
+ }, ref) {
1128
+ const { mujoco, status: wasmStatus, error: wasmError } = useMujoco();
1129
+ useEffect(() => {
1130
+ if (wasmStatus === "error" && onError) {
1131
+ onError(new Error(wasmError ?? "WASM load failed"));
1132
+ }
1133
+ }, [wasmStatus, wasmError, onError]);
1134
+ if (wasmStatus === "error" || wasmStatus === "loading" || !mujoco) {
1135
+ return null;
1136
+ }
1137
+ return /* @__PURE__ */ jsx(Canvas, { ...canvasProps, children: /* @__PURE__ */ jsx(
1138
+ MujocoSimProvider,
1139
+ {
1140
+ mujoco,
1141
+ config,
1142
+ apiRef: ref,
1143
+ onReady,
1144
+ onError,
1145
+ onStep,
1146
+ onSelection,
1147
+ gravity,
1148
+ timestep,
1149
+ substeps,
1150
+ paused,
1151
+ speed,
1152
+ interpolate,
1153
+ children
1154
+ }
1155
+ ) });
1156
+ }
1157
+ );
1158
+ function shallowEqual(a, b) {
1159
+ const keysA = Object.keys(a);
1160
+ const keysB = Object.keys(b);
1161
+ if (keysA.length !== keysB.length) return false;
1162
+ for (const key of keysA) {
1163
+ if (a[key] !== b[key]) return false;
1164
+ }
1165
+ return true;
1166
+ }
1167
+ function createController(options, Impl) {
1168
+ function Controller({
1169
+ config,
1170
+ children
1171
+ }) {
1172
+ const configObj = config ?? {};
1173
+ const stableRef = useRef(configObj);
1174
+ if (!shallowEqual(stableRef.current, configObj)) {
1175
+ stableRef.current = configObj;
1176
+ }
1177
+ const stableConfig = stableRef.current;
1178
+ const mergedConfig = useMemo(
1179
+ () => ({ ...options.defaultConfig, ...stableConfig }),
1180
+ [stableConfig]
1181
+ );
1182
+ return /* @__PURE__ */ jsx(Impl, { config: mergedConfig, children });
1183
+ }
1184
+ Controller.displayName = options.name;
1185
+ Controller.controllerName = options.name;
1186
+ Controller.defaultConfig = options.defaultConfig ?? {};
1187
+ return Controller;
1188
+ }
1189
+ var IkContext = createContext(null);
1190
+ function useIk(options) {
1191
+ const ctx = useContext(IkContext);
1192
+ if (!ctx && !options?.optional) {
1193
+ throw new Error("useIk() must be used inside an <IkController>");
1194
+ }
1195
+ return ctx;
1196
+ }
1197
+
1198
+ // src/core/GenericIK.ts
1199
+ var DEFAULTS = {
1200
+ maxIterations: 50,
1201
+ damping: 0.01,
1202
+ tolerance: 1e-3,
1203
+ epsilon: 1e-6,
1204
+ posWeight: 1,
1205
+ rotWeight: 0.3
1206
+ };
1207
+ var GenericIK = class {
1208
+ mujoco;
1209
+ constructor(mujoco) {
1210
+ this.mujoco = mujoco;
1211
+ }
1212
+ /**
1213
+ * Solve IK for a target 6-DOF pose.
1214
+ * @param model MuJoCo model
1215
+ * @param data MuJoCo data (qpos will be temporarily modified, then restored)
1216
+ * @param siteId Index of the end-effector site to control
1217
+ * @param numJoints Number of arm joints (assumes qpos[0..numJoints-1])
1218
+ * @param targetPos Target position in world frame
1219
+ * @param targetQuat Target orientation in world frame
1220
+ * @param currentQ Current joint angles (length = numJoints)
1221
+ * @param opts Optional solver parameters
1222
+ * @returns Joint angles array, or null if solver diverged
1223
+ */
1224
+ solve(model, data, siteId, numJoints, targetPos, targetQuat, currentQ, opts) {
1225
+ const o = { ...DEFAULTS, ...opts };
1226
+ const n = numJoints;
1227
+ const savedQpos = new Float64Array(data.qpos.length);
1228
+ savedQpos.set(data.qpos);
1229
+ const R_target = quatToMat3(targetQuat);
1230
+ const q = new Float64Array(n);
1231
+ for (let i = 0; i < n; i++) q[i] = currentQ[i];
1232
+ const J = new Float64Array(6 * n);
1233
+ const JJt = new Float64Array(36);
1234
+ const rhs = new Float64Array(6);
1235
+ const x = new Float64Array(6);
1236
+ const dq = new Float64Array(n);
1237
+ const baseSitePos = new Float64Array(3);
1238
+ const baseSiteMat = new Float64Array(9);
1239
+ const pertSitePos = new Float64Array(3);
1240
+ const pertSiteMat = new Float64Array(9);
1241
+ let bestQ = null;
1242
+ let bestErr = Infinity;
1243
+ for (let iter = 0; iter < o.maxIterations; iter++) {
1244
+ for (let i = 0; i < n; i++) data.qpos[i] = q[i];
1245
+ this.mujoco.mj_forward(model, data);
1246
+ const sp = data.site_xpos;
1247
+ const sm = data.site_xmat;
1248
+ const off3 = siteId * 3;
1249
+ const off9 = siteId * 9;
1250
+ for (let i = 0; i < 3; i++) baseSitePos[i] = sp[off3 + i];
1251
+ for (let i = 0; i < 9; i++) baseSiteMat[i] = sm[off9 + i];
1252
+ const posErr0 = targetPos.x - baseSitePos[0];
1253
+ const posErr1 = targetPos.y - baseSitePos[1];
1254
+ const posErr2 = targetPos.z - baseSitePos[2];
1255
+ const rotErr = orientationError(baseSiteMat, R_target);
1256
+ const error = [
1257
+ posErr0 * o.posWeight,
1258
+ posErr1 * o.posWeight,
1259
+ posErr2 * o.posWeight,
1260
+ rotErr[0] * o.rotWeight,
1261
+ rotErr[1] * o.rotWeight,
1262
+ rotErr[2] * o.rotWeight
1263
+ ];
1264
+ const errNorm = Math.sqrt(
1265
+ error[0] * error[0] + error[1] * error[1] + error[2] * error[2] + error[3] * error[3] + error[4] * error[4] + error[5] * error[5]
1266
+ );
1267
+ if (errNorm < bestErr) {
1268
+ bestErr = errNorm;
1269
+ bestQ = Array.from(q);
1270
+ }
1271
+ if (errNorm < o.tolerance) break;
1272
+ for (let j = 0; j < n; j++) {
1273
+ const saved = data.qpos[j];
1274
+ data.qpos[j] = q[j] + o.epsilon;
1275
+ this.mujoco.mj_forward(model, data);
1276
+ for (let i = 0; i < 3; i++) pertSitePos[i] = sp[off3 + i];
1277
+ for (let i = 0; i < 9; i++) pertSiteMat[i] = sm[off9 + i];
1278
+ J[0 * n + j] = (pertSitePos[0] - baseSitePos[0]) / o.epsilon * o.posWeight;
1279
+ J[1 * n + j] = (pertSitePos[1] - baseSitePos[1]) / o.epsilon * o.posWeight;
1280
+ J[2 * n + j] = (pertSitePos[2] - baseSitePos[2]) / o.epsilon * o.posWeight;
1281
+ const dRot = angularDelta(baseSiteMat, pertSiteMat);
1282
+ J[3 * n + j] = dRot[0] / o.epsilon * o.rotWeight;
1283
+ J[4 * n + j] = dRot[1] / o.epsilon * o.rotWeight;
1284
+ J[5 * n + j] = dRot[2] / o.epsilon * o.rotWeight;
1285
+ data.qpos[j] = saved;
1286
+ }
1287
+ for (let i = 0; i < n; i++) data.qpos[i] = q[i];
1288
+ for (let r = 0; r < 6; r++) {
1289
+ for (let c = 0; c < 6; c++) {
1290
+ let sum = 0;
1291
+ for (let k = 0; k < n; k++) {
1292
+ sum += J[r * n + k] * J[c * n + k];
1293
+ }
1294
+ JJt[r * 6 + c] = sum + (r === c ? o.damping : 0);
1295
+ }
1296
+ }
1297
+ for (let i = 0; i < 6; i++) rhs[i] = error[i];
1298
+ solve6x6(JJt, rhs, x);
1299
+ for (let j = 0; j < n; j++) {
1300
+ let sum = 0;
1301
+ for (let r = 0; r < 6; r++) {
1302
+ sum += J[r * n + j] * x[r];
1303
+ }
1304
+ dq[j] = sum;
1305
+ }
1306
+ for (let i = 0; i < n; i++) q[i] += dq[i];
1307
+ }
1308
+ data.qpos.set(savedQpos);
1309
+ this.mujoco.mj_forward(model, data);
1310
+ return bestQ;
1311
+ }
1312
+ };
1313
+ function quatToMat3(q) {
1314
+ const m = new Float64Array(9);
1315
+ const x = q.x, y = q.y, z = q.z, w = q.w;
1316
+ const xx = x * x, yy = y * y, zz = z * z;
1317
+ const xy = x * y, xz = x * z, yz = y * z;
1318
+ const wx = w * x, wy = w * y, wz = w * z;
1319
+ m[0] = 1 - 2 * (yy + zz);
1320
+ m[1] = 2 * (xy - wz);
1321
+ m[2] = 2 * (xz + wy);
1322
+ m[3] = 2 * (xy + wz);
1323
+ m[4] = 1 - 2 * (xx + zz);
1324
+ m[5] = 2 * (yz - wx);
1325
+ m[6] = 2 * (xz - wy);
1326
+ m[7] = 2 * (yz + wx);
1327
+ m[8] = 1 - 2 * (xx + yy);
1328
+ return m;
1329
+ }
1330
+ function orientationError(R_cur, R_tgt) {
1331
+ const Re = new Float64Array(9);
1332
+ for (let i = 0; i < 3; i++) {
1333
+ for (let j = 0; j < 3; j++) {
1334
+ let s2 = 0;
1335
+ for (let k = 0; k < 3; k++) {
1336
+ s2 += R_tgt[i * 3 + k] * R_cur[j * 3 + k];
1337
+ }
1338
+ Re[i * 3 + j] = s2;
1339
+ }
1340
+ }
1341
+ const trace = Re[0] + Re[4] + Re[8];
1342
+ const cosAngle = Math.max(-1, Math.min(1, (trace - 1) * 0.5));
1343
+ const angle = Math.acos(cosAngle);
1344
+ if (angle < 1e-6) {
1345
+ return [0, 0, 0];
1346
+ }
1347
+ if (angle > Math.PI - 1e-6) {
1348
+ return [
1349
+ 0.5 * (Re[7] - Re[5]),
1350
+ 0.5 * (Re[2] - Re[6]),
1351
+ 0.5 * (Re[3] - Re[1])
1352
+ ];
1353
+ }
1354
+ const s = angle / (2 * Math.sin(angle));
1355
+ return [
1356
+ s * (Re[7] - Re[5]),
1357
+ s * (Re[2] - Re[6]),
1358
+ s * (Re[3] - Re[1])
1359
+ ];
1360
+ }
1361
+ function angularDelta(R_base, R_pert) {
1362
+ const dR = new Float64Array(9);
1363
+ for (let i = 0; i < 3; i++) {
1364
+ for (let j = 0; j < 3; j++) {
1365
+ let s = 0;
1366
+ for (let k = 0; k < 3; k++) {
1367
+ s += R_pert[i * 3 + k] * R_base[j * 3 + k];
1368
+ }
1369
+ dR[i * 3 + j] = s;
1370
+ }
1371
+ }
1372
+ return [
1373
+ 0.5 * (dR[7] - dR[5]),
1374
+ 0.5 * (dR[2] - dR[6]),
1375
+ 0.5 * (dR[3] - dR[1])
1376
+ ];
1377
+ }
1378
+ function solve6x6(A, b, x) {
1379
+ const N = 6;
1380
+ const a = new Float64Array(A);
1381
+ const r = new Float64Array(b);
1382
+ for (let col = 0; col < N; col++) {
1383
+ let maxVal = Math.abs(a[col * N + col]);
1384
+ let maxRow = col;
1385
+ for (let row = col + 1; row < N; row++) {
1386
+ const val = Math.abs(a[row * N + col]);
1387
+ if (val > maxVal) {
1388
+ maxVal = val;
1389
+ maxRow = row;
1390
+ }
1391
+ }
1392
+ if (maxRow !== col) {
1393
+ for (let k = 0; k < N; k++) {
1394
+ const tmp2 = a[col * N + k];
1395
+ a[col * N + k] = a[maxRow * N + k];
1396
+ a[maxRow * N + k] = tmp2;
1397
+ }
1398
+ const tmp = r[col];
1399
+ r[col] = r[maxRow];
1400
+ r[maxRow] = tmp;
1401
+ }
1402
+ const pivot = a[col * N + col];
1403
+ if (Math.abs(pivot) < 1e-12) {
1404
+ x.fill(0);
1405
+ return;
1406
+ }
1407
+ for (let row = col + 1; row < N; row++) {
1408
+ const factor = a[row * N + col] / pivot;
1409
+ for (let k = col; k < N; k++) {
1410
+ a[row * N + k] -= factor * a[col * N + k];
1411
+ }
1412
+ r[row] -= factor * r[col];
1413
+ }
1414
+ }
1415
+ for (let row = N - 1; row >= 0; row--) {
1416
+ let sum = r[row];
1417
+ for (let k = row + 1; k < N; k++) {
1418
+ sum -= a[row * N + k] * x[k];
1419
+ }
1420
+ x[row] = sum / a[row * N + row];
1421
+ }
1422
+ }
1423
+ var _syncMat4 = new THREE11.Matrix4();
1424
+ function syncGizmoToSite(data, siteId, target) {
1425
+ if (siteId === -1) return;
1426
+ const sitePos = data.site_xpos.subarray(siteId * 3, siteId * 3 + 3);
1427
+ const siteMat = data.site_xmat.subarray(siteId * 9, siteId * 9 + 9);
1428
+ target.position.set(sitePos[0], sitePos[1], sitePos[2]);
1429
+ _syncMat4.set(
1430
+ siteMat[0],
1431
+ siteMat[1],
1432
+ siteMat[2],
1433
+ 0,
1434
+ siteMat[3],
1435
+ siteMat[4],
1436
+ siteMat[5],
1437
+ 0,
1438
+ siteMat[6],
1439
+ siteMat[7],
1440
+ siteMat[8],
1441
+ 0,
1442
+ 0,
1443
+ 0,
1444
+ 0,
1445
+ 1
1446
+ );
1447
+ target.quaternion.setFromRotationMatrix(_syncMat4);
1448
+ }
1449
+ function IkControllerImpl({
1450
+ config,
1451
+ children
1452
+ }) {
1453
+ const { mjModelRef, mjDataRef, mujocoRef, configRef, resetCallbacks, status } = useMujocoSim();
1454
+ const ikEnabledRef = useRef(false);
1455
+ const ikCalculatingRef = useRef(false);
1456
+ const ikTargetRef = useRef(new THREE11.Group());
1457
+ const siteIdRef = useRef(-1);
1458
+ const genericIkRef = useRef(new GenericIK(mujocoRef.current));
1459
+ const firstIkEnableRef = useRef(true);
1460
+ const needsInitialSync = useRef(true);
1461
+ const gizmoAnimRef = useRef({
1462
+ active: false,
1463
+ startPos: new THREE11.Vector3(),
1464
+ endPos: new THREE11.Vector3(),
1465
+ startRot: new THREE11.Quaternion(),
1466
+ endRot: new THREE11.Quaternion(),
1467
+ startTime: 0,
1468
+ duration: 1e3
1469
+ });
1470
+ useEffect(() => {
1471
+ const model = mjModelRef.current;
1472
+ if (!model || status !== "ready") {
1473
+ siteIdRef.current = -1;
1474
+ return;
1475
+ }
1476
+ siteIdRef.current = findSiteByName(model, config.siteName);
1477
+ const data = mjDataRef.current;
1478
+ if (data && ikTargetRef.current) {
1479
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1480
+ }
1481
+ }, [config.siteName, status, mjModelRef, mjDataRef]);
1482
+ const ikSolveFn = useCallback(
1483
+ (pos, quat, currentQ) => {
1484
+ if (config.ikSolveFn) return config.ikSolveFn(pos, quat, currentQ);
1485
+ const model = mjModelRef.current;
1486
+ const data = mjDataRef.current;
1487
+ if (!model || !data || siteIdRef.current === -1) return null;
1488
+ return genericIkRef.current.solve(
1489
+ model,
1490
+ data,
1491
+ siteIdRef.current,
1492
+ config.numJoints,
1493
+ pos,
1494
+ quat,
1495
+ currentQ,
1496
+ {
1497
+ damping: config.damping,
1498
+ maxIterations: config.maxIterations
1499
+ }
1500
+ );
1501
+ },
1502
+ [config.ikSolveFn, config.numJoints, config.damping, config.maxIterations, mjModelRef, mjDataRef]
1503
+ );
1504
+ const ikSolveFnRef = useRef(ikSolveFn);
1505
+ ikSolveFnRef.current = ikSolveFn;
1506
+ useFrame(() => {
1507
+ if (needsInitialSync.current && siteIdRef.current !== -1) {
1508
+ const data = mjDataRef.current;
1509
+ if (data && ikTargetRef.current) {
1510
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1511
+ needsInitialSync.current = false;
1512
+ }
1513
+ }
1514
+ const ga = gizmoAnimRef.current;
1515
+ const target = ikTargetRef.current;
1516
+ if (!ga.active || !target) return;
1517
+ const now = performance.now();
1518
+ const elapsed = now - ga.startTime;
1519
+ const t = Math.min(elapsed / ga.duration, 1);
1520
+ const ease = 1 - Math.pow(1 - t, 3);
1521
+ target.position.lerpVectors(ga.startPos, ga.endPos, ease);
1522
+ target.quaternion.slerpQuaternions(ga.startRot, ga.endRot, ease);
1523
+ if (t >= 1) ga.active = false;
1524
+ });
1525
+ useBeforePhysicsStep((model, data) => {
1526
+ if (!ikEnabledRef.current) {
1527
+ ikCalculatingRef.current = false;
1528
+ return;
1529
+ }
1530
+ const target = ikTargetRef.current;
1531
+ if (!target) return;
1532
+ ikCalculatingRef.current = true;
1533
+ const numJoints = config.numJoints;
1534
+ const currentQ = [];
1535
+ for (let i = 0; i < numJoints; i++) currentQ.push(data.qpos[i]);
1536
+ const solution = ikSolveFnRef.current(target.position, target.quaternion, currentQ);
1537
+ if (solution) {
1538
+ for (let i = 0; i < numJoints; i++) data.ctrl[i] = solution[i];
1539
+ }
1540
+ });
1541
+ useEffect(() => {
1542
+ const cb = () => {
1543
+ const data = mjDataRef.current;
1544
+ if (data && ikTargetRef.current) {
1545
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1546
+ }
1547
+ gizmoAnimRef.current.active = false;
1548
+ firstIkEnableRef.current = true;
1549
+ ikEnabledRef.current = false;
1550
+ needsInitialSync.current = true;
1551
+ };
1552
+ resetCallbacks.current.add(cb);
1553
+ return () => {
1554
+ resetCallbacks.current.delete(cb);
1555
+ };
1556
+ }, [resetCallbacks, mjDataRef]);
1557
+ const setIkEnabled = useCallback(
1558
+ (enabled) => {
1559
+ ikEnabledRef.current = enabled;
1560
+ const data = mjDataRef.current;
1561
+ if (enabled && data && !gizmoAnimRef.current.active && ikTargetRef.current) {
1562
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1563
+ firstIkEnableRef.current = false;
1564
+ }
1565
+ },
1566
+ [mjDataRef]
1567
+ );
1568
+ const syncTargetToSiteApi = useCallback(() => {
1569
+ const data = mjDataRef.current;
1570
+ const target = ikTargetRef.current;
1571
+ if (data && target) syncGizmoToSite(data, siteIdRef.current, target);
1572
+ }, [mjDataRef]);
1573
+ const solveIK = useCallback(
1574
+ (pos, quat, currentQ) => {
1575
+ return ikSolveFnRef.current(pos, quat, currentQ);
1576
+ },
1577
+ []
1578
+ );
1579
+ const moveTarget = useCallback(
1580
+ (pos, duration = 0) => {
1581
+ if (!ikEnabledRef.current) setIkEnabled(true);
1582
+ const target = ikTargetRef.current;
1583
+ if (!target) return;
1584
+ const targetPos = pos.clone();
1585
+ const targetRot = new THREE11.Quaternion().setFromEuler(
1586
+ new THREE11.Euler(Math.PI, 0, 0)
1587
+ );
1588
+ if (duration > 0) {
1589
+ const ga = gizmoAnimRef.current;
1590
+ ga.active = true;
1591
+ ga.startPos.copy(target.position);
1592
+ ga.endPos.copy(targetPos);
1593
+ ga.startRot.copy(target.quaternion);
1594
+ ga.endRot.copy(targetRot);
1595
+ ga.startTime = performance.now();
1596
+ ga.duration = duration;
1597
+ } else {
1598
+ gizmoAnimRef.current.active = false;
1599
+ target.position.copy(targetPos);
1600
+ target.quaternion.copy(targetRot);
1601
+ }
1602
+ },
1603
+ [setIkEnabled]
1604
+ );
1605
+ const getGizmoStats = useCallback(
1606
+ () => {
1607
+ const target = ikTargetRef.current;
1608
+ if (!ikCalculatingRef.current || !target) return null;
1609
+ return {
1610
+ pos: target.position.clone(),
1611
+ rot: new THREE11.Euler().setFromQuaternion(target.quaternion)
1612
+ };
1613
+ },
1614
+ []
1615
+ );
1497
1616
  const contextValue = useMemo(
1498
1617
  () => ({
1499
- api,
1500
- mjModelRef,
1501
- mjDataRef,
1502
- mujocoRef,
1503
- configRef,
1504
- siteIdRef,
1505
- gripperIdRef,
1506
1618
  ikEnabledRef,
1507
1619
  ikCalculatingRef,
1508
- pausedRef,
1509
- speedRef,
1510
- substepsRef,
1511
1620
  ikTargetRef,
1512
- genericIkRef,
1513
- ikSolveFnRef,
1514
- firstIkEnableRef,
1515
- gizmoAnimRef,
1516
- cameraAnimRef,
1517
- onSelectionRef,
1518
- beforeStepCallbacks,
1519
- afterStepCallbacks,
1520
- status
1621
+ siteIdRef,
1622
+ setIkEnabled,
1623
+ moveTarget,
1624
+ syncTargetToSite: syncTargetToSiteApi,
1625
+ solveIK,
1626
+ getGizmoStats
1521
1627
  }),
1522
- [api, status]
1628
+ [setIkEnabled, moveTarget, syncTargetToSiteApi, solveIK, getGizmoStats]
1523
1629
  );
1524
- return /* @__PURE__ */ jsx(MujocoSimContext.Provider, { value: contextValue, children });
1630
+ return /* @__PURE__ */ jsx(IkContext.Provider, { value: contextValue, children });
1525
1631
  }
1526
- var MujocoCanvas = forwardRef(
1527
- function MujocoCanvas2({
1528
- config,
1529
- onReady,
1530
- onError,
1531
- onStep,
1532
- onSelection,
1533
- // Declarative physics config (spec 1.1)
1534
- gravity,
1535
- timestep,
1536
- substeps,
1537
- paused,
1538
- speed,
1539
- interpolate,
1540
- gravityCompensation,
1541
- mjcfLights,
1542
- children,
1543
- ...canvasProps
1544
- }, ref) {
1545
- const { mujoco, status: wasmStatus, error: wasmError } = useMujoco();
1546
- useEffect(() => {
1547
- if (wasmStatus === "error" && onError) {
1548
- onError(new Error(wasmError ?? "WASM load failed"));
1549
- }
1550
- }, [wasmStatus, wasmError, onError]);
1551
- if (wasmStatus === "error" || wasmStatus === "loading" || !mujoco) {
1552
- return null;
1553
- }
1554
- return /* @__PURE__ */ jsx(Canvas, { ...canvasProps, children: /* @__PURE__ */ jsx(
1555
- MujocoSimProvider,
1556
- {
1557
- mujoco,
1558
- config,
1559
- apiRef: ref,
1560
- onReady,
1561
- onError,
1562
- onStep,
1563
- onSelection,
1564
- gravity,
1565
- timestep,
1566
- substeps,
1567
- paused,
1568
- speed,
1569
- interpolate,
1570
- children
1571
- }
1572
- ) });
1573
- }
1632
+ var IkController = createController(
1633
+ {
1634
+ name: "IkController",
1635
+ defaultConfig: {
1636
+ damping: 0.01,
1637
+ maxIterations: 50
1638
+ }
1639
+ },
1640
+ IkControllerImpl
1574
1641
  );
1575
- var CapsuleGeometry = class extends THREE.BufferGeometry {
1642
+ var CapsuleGeometry = class extends THREE11.BufferGeometry {
1576
1643
  parameters;
1577
1644
  constructor(radius = 1, length = 1, capSegments = 4, radialSegments = 8) {
1578
1645
  super();
1579
1646
  this.type = "CapsuleGeometry";
1580
1647
  this.parameters = { radius, length, capSegments, radialSegments };
1581
- const path = new THREE.Path();
1648
+ const path = new THREE11.Path();
1582
1649
  path.absarc(0, -length / 2, radius, Math.PI * 1.5, 0, false);
1583
1650
  path.absarc(0, length / 2, radius, 0, Math.PI * 0.5, false);
1584
- const latheGeometry = new THREE.LatheGeometry(path.getPoints(capSegments), radialSegments);
1651
+ const latheGeometry = new THREE11.LatheGeometry(path.getPoints(capSegments), radialSegments);
1585
1652
  const self = this;
1586
1653
  self.setIndex(latheGeometry.getIndex());
1587
1654
  self.setAttribute("position", latheGeometry.getAttribute("position"));
@@ -1589,27 +1656,27 @@ var CapsuleGeometry = class extends THREE.BufferGeometry {
1589
1656
  self.setAttribute("uv", latheGeometry.getAttribute("uv"));
1590
1657
  }
1591
1658
  };
1592
- var Reflector = class extends THREE.Mesh {
1659
+ var Reflector = class extends THREE11.Mesh {
1593
1660
  isReflector = true;
1594
1661
  camera;
1595
- reflectorPlane = new THREE.Plane();
1596
- normal = new THREE.Vector3();
1597
- reflectorWorldPosition = new THREE.Vector3();
1598
- cameraWorldPosition = new THREE.Vector3();
1599
- rotationMatrix = new THREE.Matrix4();
1600
- lookAtPosition = new THREE.Vector3(0, 0, -1);
1601
- clipPlane = new THREE.Vector4();
1602
- view = new THREE.Vector3();
1603
- target = new THREE.Vector3();
1604
- q = new THREE.Vector4();
1605
- textureMatrix = new THREE.Matrix4();
1662
+ reflectorPlane = new THREE11.Plane();
1663
+ normal = new THREE11.Vector3();
1664
+ reflectorWorldPosition = new THREE11.Vector3();
1665
+ cameraWorldPosition = new THREE11.Vector3();
1666
+ rotationMatrix = new THREE11.Matrix4();
1667
+ lookAtPosition = new THREE11.Vector3(0, 0, -1);
1668
+ clipPlane = new THREE11.Vector4();
1669
+ view = new THREE11.Vector3();
1670
+ target = new THREE11.Vector3();
1671
+ q = new THREE11.Vector4();
1672
+ textureMatrix = new THREE11.Matrix4();
1606
1673
  virtualCamera;
1607
1674
  renderTarget;
1608
1675
  constructor(geometry, options = {}) {
1609
1676
  super(geometry);
1610
1677
  this.type = "Reflector";
1611
- this.camera = new THREE.PerspectiveCamera();
1612
- const color = options.color !== void 0 ? new THREE.Color(options.color) : new THREE.Color(8355711);
1678
+ this.camera = new THREE11.PerspectiveCamera();
1679
+ const color = options.color !== void 0 ? new THREE11.Color(options.color) : new THREE11.Color(8355711);
1613
1680
  const textureWidth = options.textureWidth || 512;
1614
1681
  const textureHeight = options.textureHeight || 512;
1615
1682
  const clipBias = options.clipBias || 0;
@@ -1617,11 +1684,11 @@ var Reflector = class extends THREE.Mesh {
1617
1684
  const blendTexture = options.texture || void 0;
1618
1685
  const mixStrength = options.mixStrength !== void 0 ? options.mixStrength : 0.25;
1619
1686
  this.virtualCamera = this.camera;
1620
- this.renderTarget = new THREE.WebGLRenderTarget(textureWidth, textureHeight, {
1687
+ this.renderTarget = new THREE11.WebGLRenderTarget(textureWidth, textureHeight, {
1621
1688
  samples: multisample,
1622
- type: THREE.HalfFloatType
1689
+ type: THREE11.HalfFloatType
1623
1690
  });
1624
- this.material = new THREE.MeshPhysicalMaterial({
1691
+ this.material = new THREE11.MeshPhysicalMaterial({
1625
1692
  map: blendTexture,
1626
1693
  color,
1627
1694
  roughness: 0.5,
@@ -1747,7 +1814,7 @@ var GeomBuilder = class {
1747
1814
  const pos = mjModel.geom_pos.subarray(g * 3, g * 3 + 3);
1748
1815
  const quat = mjModel.geom_quat.subarray(g * 4, g * 4 + 4);
1749
1816
  const matId = mjModel.geom_matid[g];
1750
- const color = new THREE.Color(16777215);
1817
+ const color = new THREE11.Color(16777215);
1751
1818
  let opacity = 1;
1752
1819
  if (matId >= 0) {
1753
1820
  const rgba = mjModel.mat_rgba.subarray(matId * 4, matId * 4 + 4);
@@ -1762,16 +1829,16 @@ var GeomBuilder = class {
1762
1829
  let geo = null;
1763
1830
  const getVal = (v) => v?.value ?? v;
1764
1831
  if (type === getVal(MG.mjGEOM_PLANE)) {
1765
- geo = new THREE.PlaneGeometry(size[0] * 2 || 5, size[1] * 2 || 5);
1832
+ geo = new THREE11.PlaneGeometry(size[0] * 2 || 5, size[1] * 2 || 5);
1766
1833
  } else if (type === getVal(MG.mjGEOM_SPHERE)) {
1767
- geo = new THREE.SphereGeometry(size[0], 24, 24);
1834
+ geo = new THREE11.SphereGeometry(size[0], 24, 24);
1768
1835
  } else if (type === getVal(MG.mjGEOM_CAPSULE)) {
1769
1836
  geo = new CapsuleGeometry(size[0], size[1] * 2, 24, 12);
1770
1837
  geo.rotateX(Math.PI / 2);
1771
1838
  } else if (type === getVal(MG.mjGEOM_BOX)) {
1772
- geo = new THREE.BoxGeometry(size[0] * 2, size[1] * 2, size[2] * 2);
1839
+ geo = new THREE11.BoxGeometry(size[0] * 2, size[1] * 2, size[2] * 2);
1773
1840
  } else if (type === getVal(MG.mjGEOM_CYLINDER)) {
1774
- geo = new THREE.CylinderGeometry(size[0], size[0], size[1] * 2, 24);
1841
+ geo = new THREE11.CylinderGeometry(size[0], size[0], size[1] * 2, 24);
1775
1842
  geo.rotateX(Math.PI / 2);
1776
1843
  } else if (type === getVal(MG.mjGEOM_MESH)) {
1777
1844
  const mId = mjModel.geom_dataid[g];
@@ -1779,8 +1846,8 @@ var GeomBuilder = class {
1779
1846
  const vNum = mjModel.mesh_vertnum[mId];
1780
1847
  const fAdr = mjModel.mesh_faceadr[mId];
1781
1848
  const fNum = mjModel.mesh_facenum[mId];
1782
- geo = new THREE.BufferGeometry();
1783
- geo.setAttribute("position", new THREE.Float32BufferAttribute(mjModel.mesh_vert.subarray(vAdr * 3, (vAdr + vNum) * 3), 3));
1849
+ geo = new THREE11.BufferGeometry();
1850
+ geo.setAttribute("position", new THREE11.Float32BufferAttribute(mjModel.mesh_vert.subarray(vAdr * 3, (vAdr + vNum) * 3), 3));
1784
1851
  geo.setIndex(Array.from(mjModel.mesh_face.subarray(fAdr * 3, (fAdr + fNum) * 3)));
1785
1852
  geo.computeVertexNormals();
1786
1853
  }
@@ -1795,7 +1862,7 @@ var GeomBuilder = class {
1795
1862
  mixStrength: 0.25
1796
1863
  });
1797
1864
  } else {
1798
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({
1865
+ mesh = new THREE11.Mesh(geo, new THREE11.MeshStandardMaterial({
1799
1866
  color,
1800
1867
  transparent: opacity < 1,
1801
1868
  opacity,
@@ -1820,10 +1887,11 @@ function SceneRenderer() {
1820
1887
  const bodyRefs = useRef([]);
1821
1888
  const prevModelRef = useRef(null);
1822
1889
  const geomBuilder = useMemo(() => {
1890
+ if (status !== "ready") return null;
1823
1891
  return new GeomBuilder(mujocoRef.current);
1824
- }, [mujocoRef.current]);
1892
+ }, [status, mujocoRef]);
1825
1893
  useEffect(() => {
1826
- if (status !== "ready") return;
1894
+ if (status !== "ready" || !geomBuilder) return;
1827
1895
  const model = mjModelRef.current;
1828
1896
  const group = groupRef.current;
1829
1897
  if (!model || !group) return;
@@ -1834,7 +1902,7 @@ function SceneRenderer() {
1834
1902
  }
1835
1903
  const refs = [];
1836
1904
  for (let i = 0; i < model.nbody; i++) {
1837
- const bodyGroup = new THREE.Group();
1905
+ const bodyGroup = new THREE11.Group();
1838
1906
  bodyGroup.userData.bodyID = i;
1839
1907
  for (let g = 0; g < model.ngeom; g++) {
1840
1908
  if (model.geom_bodyid[g] === i) {
@@ -1877,31 +1945,25 @@ function SceneRenderer() {
1877
1945
  while (obj && obj.userData.bodyID === void 0 && obj.parent) {
1878
1946
  obj = obj.parent;
1879
1947
  }
1880
- if (obj && obj.userData.bodyID !== void 0 && obj.userData.bodyID > 0) {
1948
+ const bodyID = obj?.userData.bodyID;
1949
+ if (typeof bodyID === "number" && bodyID > 0) {
1881
1950
  const model = mjModelRef.current;
1882
- if (model && onSelectionRef.current) {
1883
- const name = getName(model, model.name_bodyadr[obj.userData.bodyID]);
1884
- onSelectionRef.current(obj.userData.bodyID, name);
1951
+ if (model && bodyID < model.nbody && onSelectionRef.current) {
1952
+ const name = getName(model, model.name_bodyadr[bodyID]);
1953
+ onSelectionRef.current(bodyID, name);
1885
1954
  }
1886
1955
  }
1887
1956
  }
1888
1957
  }
1889
1958
  );
1890
1959
  }
1891
- var _mat4 = new THREE.Matrix4();
1892
- var _pos = new THREE.Vector3();
1893
- var _quat = new THREE.Quaternion();
1894
- var _scale = new THREE.Vector3(1, 1, 1);
1960
+ var _mat4 = new THREE11.Matrix4();
1961
+ var _pos = new THREE11.Vector3();
1962
+ var _quat = new THREE11.Quaternion();
1963
+ var _scale = new THREE11.Vector3(1, 1, 1);
1895
1964
  function IkGizmo({ siteName, scale = 0.18, onDrag }) {
1896
- const {
1897
- ikTargetRef,
1898
- mjModelRef,
1899
- mjDataRef,
1900
- siteIdRef,
1901
- api,
1902
- ikEnabledRef,
1903
- status
1904
- } = useMujocoSim();
1965
+ const { mjModelRef, mjDataRef, status } = useMujocoSim();
1966
+ const { ikTargetRef, siteIdRef, ikEnabledRef, setIkEnabled } = useIk();
1905
1967
  const wrapperRef = useRef(null);
1906
1968
  const pivotRef = useRef(null);
1907
1969
  const draggingRef = useRef(false);
@@ -1909,19 +1971,15 @@ function IkGizmo({ siteName, scale = 0.18, onDrag }) {
1909
1971
  const { controls } = useThree();
1910
1972
  useEffect(() => {
1911
1973
  const model = mjModelRef.current;
1912
- if (!model || status !== "ready") {
1974
+ if (!model || status !== "ready" || !siteName) {
1913
1975
  localSiteIdRef.current = -1;
1914
1976
  return;
1915
1977
  }
1916
- if (siteName) {
1917
- localSiteIdRef.current = findSiteByName(model, siteName);
1918
- } else {
1919
- localSiteIdRef.current = siteIdRef.current;
1920
- }
1921
- }, [siteName, status, mjModelRef, siteIdRef]);
1978
+ localSiteIdRef.current = findSiteByName(model, siteName);
1979
+ }, [siteName, status, mjModelRef]);
1922
1980
  useFrame(() => {
1923
1981
  const data = mjDataRef.current;
1924
- const sid = localSiteIdRef.current;
1982
+ const sid = siteName ? localSiteIdRef.current : siteIdRef.current;
1925
1983
  if (!data || sid < 0 || !wrapperRef.current) return;
1926
1984
  if (!draggingRef.current) {
1927
1985
  const p = data.site_xpos;
@@ -1966,7 +2024,7 @@ function IkGizmo({ siteName, scale = 0.18, onDrag }) {
1966
2024
  onDragStart: () => {
1967
2025
  draggingRef.current = true;
1968
2026
  if (!onDrag) {
1969
- if (!ikEnabledRef.current) api.setIkEnabled(true);
2027
+ if (!ikEnabledRef.current) setIkEnabled(true);
1970
2028
  }
1971
2029
  if (controls) controls.enabled = false;
1972
2030
  },
@@ -1994,11 +2052,11 @@ function IkGizmo({ siteName, scale = 0.18, onDrag }) {
1994
2052
  }
1995
2053
  ) });
1996
2054
  }
1997
- var _dummy = new THREE.Object3D();
2055
+ var _dummy = new THREE11.Object3D();
1998
2056
  function ContactMarkers({
1999
2057
  maxContacts = 100,
2000
- radius = 5e-3,
2001
- color = "#4f46e5",
2058
+ radius = 8e-3,
2059
+ color = "#22d3ee",
2002
2060
  visible = true
2003
2061
  } = {}) {
2004
2062
  const { mjDataRef, status } = useMujocoSim();
@@ -2013,43 +2071,33 @@ function ContactMarkers({
2013
2071
  const ncon = data.ncon;
2014
2072
  const count = Math.min(ncon, maxContacts);
2015
2073
  for (let i = 0; i < count; i++) {
2016
- try {
2017
- const c = data.contact.get(i);
2018
- if (!c) break;
2019
- _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
2020
- _dummy.updateMatrix();
2021
- mesh.setMatrixAt(i, _dummy.matrix);
2022
- } catch {
2074
+ const c = getContact(data, i);
2075
+ if (!c) {
2023
2076
  mesh.count = i;
2024
2077
  mesh.instanceMatrix.needsUpdate = true;
2025
2078
  return;
2026
2079
  }
2080
+ _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
2081
+ _dummy.updateMatrix();
2082
+ mesh.setMatrixAt(i, _dummy.matrix);
2027
2083
  }
2028
2084
  mesh.count = count;
2029
2085
  mesh.instanceMatrix.needsUpdate = true;
2030
2086
  });
2031
2087
  if (status !== "ready") return null;
2032
- return /* @__PURE__ */ jsxs("instancedMesh", { ref: meshRef, args: [void 0, void 0, maxContacts], children: [
2088
+ return /* @__PURE__ */ jsxs("instancedMesh", { ref: meshRef, args: [void 0, void 0, maxContacts], frustumCulled: false, renderOrder: 999, children: [
2033
2089
  /* @__PURE__ */ jsx("sphereGeometry", { args: [radius, 8, 8] }),
2034
- /* @__PURE__ */ jsx(
2035
- "meshStandardMaterial",
2036
- {
2037
- color,
2038
- emissive: color,
2039
- emissiveIntensity: 0.3,
2040
- roughness: 0.5
2041
- }
2042
- )
2090
+ /* @__PURE__ */ jsx("meshBasicMaterial", { color, depthTest: false })
2043
2091
  ] });
2044
2092
  }
2045
2093
  var _force = new Float64Array(3);
2046
2094
  var _torque = new Float64Array(3);
2047
2095
  var _point = new Float64Array(3);
2048
- var _bodyPos = new THREE.Vector3();
2049
- var _bodyQuat = new THREE.Quaternion();
2050
- var _worldHit = new THREE.Vector3();
2051
- var _raycaster = new THREE.Raycaster();
2052
- var _mouse = new THREE.Vector2();
2096
+ var _bodyPos = new THREE11.Vector3();
2097
+ var _bodyQuat = new THREE11.Quaternion();
2098
+ var _worldHit = new THREE11.Vector3();
2099
+ var _raycaster = new THREE11.Raycaster();
2100
+ var _mouse = new THREE11.Vector2();
2053
2101
  function DragInteraction({
2054
2102
  stiffness = 250,
2055
2103
  showArrow = true
@@ -2059,16 +2107,16 @@ function DragInteraction({
2059
2107
  const draggingRef = useRef(false);
2060
2108
  const bodyIdRef = useRef(-1);
2061
2109
  const grabDistanceRef = useRef(0);
2062
- const localHitRef = useRef(new THREE.Vector3());
2063
- const grabWorldRef = useRef(new THREE.Vector3());
2064
- const mouseWorldRef = useRef(new THREE.Vector3());
2110
+ const localHitRef = useRef(new THREE11.Vector3());
2111
+ const grabWorldRef = useRef(new THREE11.Vector3());
2112
+ const mouseWorldRef = useRef(new THREE11.Vector3());
2065
2113
  const arrowRef = useRef(null);
2066
2114
  const groupRef = useRef(null);
2067
2115
  useEffect(() => {
2068
2116
  if (!showArrow || !groupRef.current) return;
2069
- const arrow = new THREE.ArrowHelper(
2070
- new THREE.Vector3(0, 1, 0),
2071
- new THREE.Vector3(),
2117
+ const arrow = new THREE11.ArrowHelper(
2118
+ new THREE11.Vector3(0, 1, 0),
2119
+ new THREE11.Vector3(),
2072
2120
  0.1,
2073
2121
  16729156
2074
2122
  );
@@ -2196,7 +2244,7 @@ function DragInteraction({
2196
2244
  if (!arrow) return;
2197
2245
  if (draggingRef.current && bodyIdRef.current > 0) {
2198
2246
  arrow.visible = true;
2199
- const dir = mouseWorldRef.current.clone().sub(grabWorldRef.current);
2247
+ const dir = _bodyPos.copy(mouseWorldRef.current).sub(grabWorldRef.current);
2200
2248
  const len = dir.length();
2201
2249
  if (len > 1e-3) {
2202
2250
  dir.normalize();
@@ -2211,7 +2259,7 @@ function DragInteraction({
2211
2259
  if (status !== "ready") return null;
2212
2260
  return /* @__PURE__ */ jsx("group", { ref: groupRef });
2213
2261
  }
2214
- function SceneLights({ intensity = 1 }) {
2262
+ function useSceneLights(intensity = 1) {
2215
2263
  const { mjModelRef, status } = useMujocoSim();
2216
2264
  const { scene } = useThree();
2217
2265
  const lightsRef = useRef([]);
@@ -2239,7 +2287,7 @@ function SceneLights({ intensity = 1 }) {
2239
2287
  const dr = model.light_diffuse ? model.light_diffuse[3 * i] : 1;
2240
2288
  const dg = model.light_diffuse ? model.light_diffuse[3 * i + 1] : 1;
2241
2289
  const db = model.light_diffuse ? model.light_diffuse[3 * i + 2] : 1;
2242
- const color = new THREE.Color(dr, dg, db);
2290
+ const color = new THREE11.Color(dr, dg, db);
2243
2291
  const px = model.light_pos[3 * i];
2244
2292
  const py = model.light_pos[3 * i + 1];
2245
2293
  const pz = model.light_pos[3 * i + 2];
@@ -2247,7 +2295,7 @@ function SceneLights({ intensity = 1 }) {
2247
2295
  const dy = model.light_dir[3 * i + 1];
2248
2296
  const dz = model.light_dir[3 * i + 2];
2249
2297
  if (isDirectional) {
2250
- const light = new THREE.DirectionalLight(color, finalIntensity);
2298
+ const light = new THREE11.DirectionalLight(color, finalIntensity);
2251
2299
  light.position.set(px, py, pz);
2252
2300
  light.target.position.set(px + dx, py + dy, pz + dz);
2253
2301
  light.castShadow = castShadow;
@@ -2270,7 +2318,7 @@ function SceneLights({ intensity = 1 }) {
2270
2318
  const cutoff = model.light_cutoff ? model.light_cutoff[i] : 45;
2271
2319
  const exponent = model.light_exponent ? model.light_exponent[i] : 10;
2272
2320
  const angle = cutoff * Math.PI / 180;
2273
- const light = new THREE.SpotLight(color, finalIntensity, 0, angle, exponent / 128);
2321
+ const light = new THREE11.SpotLight(color, finalIntensity, 0, angle, exponent / 128);
2274
2322
  light.position.set(px, py, pz);
2275
2323
  light.target.position.set(px + dx, py + dy, pz + dz);
2276
2324
  light.castShadow = castShadow;
@@ -2300,6 +2348,11 @@ function SceneLights({ intensity = 1 }) {
2300
2348
  targetsRef.current = [];
2301
2349
  };
2302
2350
  }, [status, mjModelRef, scene, intensity]);
2351
+ }
2352
+
2353
+ // src/components/SceneLights.tsx
2354
+ function SceneLights({ intensity = 1 }) {
2355
+ useSceneLights(intensity);
2303
2356
  return null;
2304
2357
  }
2305
2358
  var JOINT_COLORS = {
@@ -2312,6 +2365,12 @@ var JOINT_COLORS = {
2312
2365
  3: 16776960
2313
2366
  // hinge - yellow
2314
2367
  };
2368
+ var _v3a = new THREE11.Vector3();
2369
+ new THREE11.Vector3();
2370
+ var _quat2 = new THREE11.Quaternion();
2371
+ var _contactPos = new THREE11.Vector3();
2372
+ var _contactNormal = new THREE11.Vector3();
2373
+ var MAX_CONTACT_ARROWS = 50;
2315
2374
  function Debug({
2316
2375
  showGeoms = false,
2317
2376
  showSites = false,
@@ -2338,21 +2397,21 @@ function Debug({
2338
2397
  let geometry = null;
2339
2398
  switch (type) {
2340
2399
  case 2:
2341
- geometry = new THREE.SphereGeometry(s[3 * i], 12, 8);
2400
+ geometry = new THREE11.SphereGeometry(s[3 * i], 12, 8);
2342
2401
  break;
2343
2402
  case 3:
2344
- geometry = new THREE.CapsuleGeometry(s[3 * i], s[3 * i + 1] * 2, 6, 8);
2403
+ geometry = new THREE11.CapsuleGeometry(s[3 * i], s[3 * i + 1] * 2, 6, 8);
2345
2404
  break;
2346
2405
  case 5:
2347
- geometry = new THREE.CylinderGeometry(s[3 * i], s[3 * i], s[3 * i + 1] * 2, 12);
2406
+ geometry = new THREE11.CylinderGeometry(s[3 * i], s[3 * i], s[3 * i + 1] * 2, 12);
2348
2407
  break;
2349
2408
  case 6:
2350
- geometry = new THREE.BoxGeometry(s[3 * i] * 2, s[3 * i + 1] * 2, s[3 * i + 2] * 2);
2409
+ geometry = new THREE11.BoxGeometry(s[3 * i] * 2, s[3 * i + 1] * 2, s[3 * i + 2] * 2);
2351
2410
  break;
2352
2411
  }
2353
2412
  if (geometry) {
2354
- const mat = new THREE.MeshBasicMaterial({ color: 65280, wireframe: true, transparent: true, opacity: 0.3 });
2355
- const mesh = new THREE.Mesh(geometry, mat);
2413
+ const mat = new THREE11.MeshBasicMaterial({ color: 65280, wireframe: true, transparent: true, opacity: 0.3 });
2414
+ const mesh = new THREE11.Mesh(geometry, mat);
2356
2415
  mesh.userData.geomId = i;
2357
2416
  mesh.userData.bodyId = model.geom_bodyid[i];
2358
2417
  geoms.push(mesh);
@@ -2360,35 +2419,88 @@ function Debug({
2360
2419
  }
2361
2420
  }
2362
2421
  if (showSites) {
2422
+ const siteSize = model.site_size;
2363
2423
  for (let i = 0; i < model.nsite; i++) {
2364
- const geometry = new THREE.OctahedronGeometry(0.01);
2365
- const mat = new THREE.MeshBasicMaterial({ color: 16711935, transparent: true, opacity: 0.7 });
2366
- const mesh = new THREE.Mesh(geometry, mat);
2424
+ let radius = 8e-3;
2425
+ if (siteSize) {
2426
+ radius = Math.max(siteSize[3 * i] * 0.5, 4e-3);
2427
+ } else {
2428
+ const bodyId = model.site_bodyid[i];
2429
+ let maxGeomSize = 0;
2430
+ for (let g = 0; g < model.ngeom; g++) {
2431
+ if (model.geom_bodyid[g] === bodyId) {
2432
+ maxGeomSize = Math.max(maxGeomSize, model.geom_size[3 * g]);
2433
+ }
2434
+ }
2435
+ if (maxGeomSize > 0) radius = maxGeomSize * 0.15;
2436
+ }
2437
+ const geometry = new THREE11.OctahedronGeometry(radius);
2438
+ const mat = new THREE11.MeshBasicMaterial({ color: 16711935, depthTest: false });
2439
+ const mesh = new THREE11.Mesh(geometry, mat);
2440
+ mesh.renderOrder = 999;
2441
+ mesh.frustumCulled = false;
2367
2442
  mesh.userData.siteId = i;
2443
+ const canvas = document.createElement("canvas");
2444
+ canvas.width = 256;
2445
+ canvas.height = 64;
2446
+ const ctx = canvas.getContext("2d");
2447
+ ctx.fillStyle = "#ff00ff";
2448
+ ctx.font = "bold 36px monospace";
2449
+ ctx.textAlign = "center";
2450
+ ctx.fillText(getName(model, model.name_siteadr[i]), 128, 42);
2451
+ const tex = new THREE11.CanvasTexture(canvas);
2452
+ const spriteMat = new THREE11.SpriteMaterial({ map: tex, depthTest: false, transparent: true });
2453
+ const sprite = new THREE11.Sprite(spriteMat);
2454
+ const labelScale = radius * 15;
2455
+ sprite.scale.set(labelScale, labelScale * 0.25, 1);
2456
+ sprite.position.y = radius * 2;
2457
+ sprite.renderOrder = 999;
2458
+ mesh.add(sprite);
2368
2459
  sites.push(mesh);
2369
2460
  }
2370
2461
  }
2371
2462
  if (showJoints) {
2463
+ const jntPos = model.jnt_pos;
2464
+ const jntAxis = model.jnt_axis;
2372
2465
  for (let i = 0; i < model.njnt; i++) {
2373
2466
  const type = model.jnt_type[i];
2374
2467
  const color = JOINT_COLORS[type] ?? 16777215;
2375
- const arrow = new THREE.ArrowHelper(
2376
- new THREE.Vector3(0, 0, 1),
2377
- new THREE.Vector3(),
2378
- 0.05,
2468
+ const bodyId = model.jnt_bodyid[i];
2469
+ let maxGeomSize = 0;
2470
+ for (let g = 0; g < model.ngeom; g++) {
2471
+ if (model.geom_bodyid[g] === bodyId) {
2472
+ maxGeomSize = Math.max(maxGeomSize, model.geom_size[3 * g]);
2473
+ }
2474
+ }
2475
+ const arrowLen = Math.max(maxGeomSize * 0.8, 0.05);
2476
+ const arrow = new THREE11.ArrowHelper(
2477
+ new THREE11.Vector3(0, 0, 1),
2478
+ new THREE11.Vector3(),
2479
+ arrowLen,
2379
2480
  color,
2380
- 0.01,
2381
- 5e-3
2481
+ arrowLen * 0.25,
2482
+ arrowLen * 0.12
2382
2483
  );
2484
+ arrow.renderOrder = 999;
2485
+ arrow.frustumCulled = false;
2486
+ arrow.line.material = new THREE11.LineBasicMaterial({ color, depthTest: false });
2487
+ arrow.cone.material.depthTest = false;
2488
+ arrow.line.renderOrder = 999;
2489
+ arrow.line.frustumCulled = false;
2490
+ arrow.cone.renderOrder = 999;
2491
+ arrow.cone.frustumCulled = false;
2383
2492
  arrow.userData.jointId = i;
2493
+ arrow.userData.bodyId = bodyId;
2494
+ arrow.userData.hasJntPos = !!jntPos;
2495
+ arrow.userData.hasJntAxis = !!jntAxis;
2384
2496
  joints.push(arrow);
2385
2497
  }
2386
2498
  }
2387
2499
  if (showCOM) {
2388
2500
  for (let i = 1; i < model.nbody; i++) {
2389
- const geometry = new THREE.SphereGeometry(5e-3, 6, 6);
2390
- const mat = new THREE.MeshBasicMaterial({ color: 16711680 });
2391
- const mesh = new THREE.Mesh(geometry, mat);
2501
+ const geometry = new THREE11.SphereGeometry(5e-3, 6, 6);
2502
+ const mat = new THREE11.MeshBasicMaterial({ color: 16711680 });
2503
+ const mesh = new THREE11.Mesh(geometry, mat);
2392
2504
  mesh.userData.bodyId = i;
2393
2505
  comMarkers.push(mesh);
2394
2506
  }
@@ -2416,6 +2528,8 @@ function Debug({
2416
2528
  const model = mjModelRef.current;
2417
2529
  const data = mjDataRef.current;
2418
2530
  if (!model || !data || !debugGeometry) return;
2531
+ const jntPos = model.jnt_pos;
2532
+ const jntAxis = model.jnt_axis;
2419
2533
  for (const mesh of debugGeometry.geoms) {
2420
2534
  const bid = mesh.userData.bodyId;
2421
2535
  const i3 = bid * 3;
@@ -2429,7 +2543,8 @@ function Debug({
2429
2543
  );
2430
2544
  const gid = mesh.userData.geomId;
2431
2545
  const gp = model.geom_pos;
2432
- mesh.position.add(new THREE.Vector3(gp[3 * gid], gp[3 * gid + 1], gp[3 * gid + 2]).applyQuaternion(mesh.quaternion));
2546
+ _v3a.set(gp[3 * gid], gp[3 * gid + 1], gp[3 * gid + 2]).applyQuaternion(mesh.quaternion);
2547
+ mesh.position.add(_v3a);
2433
2548
  }
2434
2549
  for (const mesh of debugGeometry.sites) {
2435
2550
  const sid = mesh.userData.siteId;
@@ -2439,6 +2554,28 @@ function Debug({
2439
2554
  data.site_xpos[3 * sid + 2]
2440
2555
  );
2441
2556
  }
2557
+ for (const obj of debugGeometry.joints) {
2558
+ const arrow = obj;
2559
+ const jid = arrow.userData.jointId;
2560
+ const bid = arrow.userData.bodyId;
2561
+ const i3 = bid * 3;
2562
+ const i4 = bid * 4;
2563
+ _quat2.set(
2564
+ data.xquat[i4 + 1],
2565
+ data.xquat[i4 + 2],
2566
+ data.xquat[i4 + 3],
2567
+ data.xquat[i4]
2568
+ );
2569
+ arrow.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]);
2570
+ if (jntPos) {
2571
+ _v3a.set(jntPos[3 * jid], jntPos[3 * jid + 1], jntPos[3 * jid + 2]).applyQuaternion(_quat2);
2572
+ arrow.position.add(_v3a);
2573
+ }
2574
+ if (jntAxis) {
2575
+ _v3a.set(jntAxis[3 * jid], jntAxis[3 * jid + 1], jntAxis[3 * jid + 2]).applyQuaternion(_quat2).normalize();
2576
+ arrow.setDirection(_v3a);
2577
+ }
2578
+ }
2442
2579
  for (const mesh of debugGeometry.comMarkers) {
2443
2580
  const bid = mesh.userData.bodyId;
2444
2581
  const i3 = bid * 3;
@@ -2446,34 +2583,61 @@ function Debug({
2446
2583
  }
2447
2584
  });
2448
2585
  const contactGroupRef = useRef(null);
2449
- const contactArrowsRef = useRef([]);
2586
+ const contactPoolRef = useRef([]);
2587
+ const contactPoolInitRef = useRef(false);
2588
+ useEffect(() => {
2589
+ const group = contactGroupRef.current;
2590
+ if (!group || contactPoolInitRef.current) return;
2591
+ contactPoolInitRef.current = true;
2592
+ const pool = [];
2593
+ for (let i = 0; i < MAX_CONTACT_ARROWS; i++) {
2594
+ const arrow = new THREE11.ArrowHelper(
2595
+ new THREE11.Vector3(0, 1, 0),
2596
+ new THREE11.Vector3(),
2597
+ 0.1,
2598
+ 16729156,
2599
+ 0.03,
2600
+ 0.015
2601
+ );
2602
+ arrow.visible = false;
2603
+ group.add(arrow);
2604
+ pool.push(arrow);
2605
+ }
2606
+ contactPoolRef.current = pool;
2607
+ return () => {
2608
+ for (const arrow of pool) {
2609
+ group.remove(arrow);
2610
+ arrow.dispose();
2611
+ }
2612
+ contactPoolRef.current = [];
2613
+ contactPoolInitRef.current = false;
2614
+ };
2615
+ }, [showContacts]);
2450
2616
  useFrame(() => {
2451
2617
  if (!showContacts) return;
2452
- const model = mjModelRef.current;
2453
2618
  const data = mjDataRef.current;
2454
- const group = contactGroupRef.current;
2455
- if (!model || !data || !group) return;
2456
- for (const arrow of contactArrowsRef.current) {
2457
- group.remove(arrow);
2458
- arrow.dispose();
2459
- }
2460
- contactArrowsRef.current = [];
2619
+ const pool = contactPoolRef.current;
2620
+ if (!data || pool.length === 0) return;
2461
2621
  const ncon = data.ncon;
2462
- for (let i = 0; i < Math.min(ncon, 50); i++) {
2463
- try {
2464
- const c = data.contact.get(i);
2465
- const pos = new THREE.Vector3(c.pos[0], c.pos[1], c.pos[2]);
2466
- const normal = new THREE.Vector3(c.frame[0], c.frame[1], c.frame[2]);
2467
- const force = Math.abs(c.dist) * 100;
2468
- const length = Math.min(force * 0.01, 0.1);
2469
- if (length > 1e-3) {
2470
- const arrow = new THREE.ArrowHelper(normal, pos, length, 16729156, length * 0.3, length * 0.15);
2471
- group.add(arrow);
2472
- contactArrowsRef.current.push(arrow);
2473
- }
2474
- } catch {
2475
- break;
2476
- }
2622
+ let arrowIdx = 0;
2623
+ for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
2624
+ const c = getContact(data, i);
2625
+ if (!c) break;
2626
+ _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
2627
+ _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
2628
+ const force = Math.abs(c.dist) * 100;
2629
+ const length = Math.min(force * 0.01, 0.1);
2630
+ if (length > 1e-3 && arrowIdx < pool.length) {
2631
+ const arrow = pool[arrowIdx];
2632
+ arrow.position.copy(_contactPos);
2633
+ arrow.setDirection(_contactNormal);
2634
+ arrow.setLength(length, length * 0.3, length * 0.15);
2635
+ arrow.visible = true;
2636
+ arrowIdx++;
2637
+ }
2638
+ }
2639
+ for (let i = arrowIdx; i < pool.length; i++) {
2640
+ pool[i].visible = false;
2477
2641
  }
2478
2642
  });
2479
2643
  if (status !== "ready") return null;
@@ -2482,30 +2646,75 @@ function Debug({
2482
2646
  showContacts && /* @__PURE__ */ jsx("group", { ref: contactGroupRef })
2483
2647
  ] });
2484
2648
  }
2485
- var DEFAULT_TENDON_COLOR = new THREE.Color(0.3, 0.3, 0.8);
2649
+ var DEFAULT_TENDON_COLOR = new THREE11.Color(0.3, 0.3, 0.8);
2486
2650
  var DEFAULT_TENDON_WIDTH = 2e-3;
2651
+ new THREE11.Vector3();
2487
2652
  function TendonRenderer() {
2488
2653
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
2489
2654
  const groupRef = useRef(null);
2490
2655
  const meshesRef = useRef([]);
2491
- useFrame(() => {
2656
+ const curvesRef = useRef([]);
2657
+ const materialRef = useRef(null);
2658
+ useEffect(() => {
2492
2659
  const model = mjModelRef.current;
2493
2660
  const data = mjDataRef.current;
2494
2661
  const group = groupRef.current;
2495
2662
  if (!model || !data || !group) return;
2496
2663
  const ntendon = model.ntendon ?? 0;
2497
2664
  if (ntendon === 0) return;
2498
- for (const mesh of meshesRef.current) {
2499
- group.remove(mesh);
2500
- mesh.geometry.dispose();
2501
- mesh.material.dispose();
2665
+ const material = new THREE11.MeshStandardMaterial({
2666
+ color: DEFAULT_TENDON_COLOR,
2667
+ roughness: 0.6,
2668
+ metalness: 0.1
2669
+ });
2670
+ materialRef.current = material;
2671
+ const meshes = [];
2672
+ const curves = [];
2673
+ for (let t = 0; t < ntendon; t++) {
2674
+ const wrapNum = model.ten_wrapnum[t];
2675
+ if (wrapNum < 2) {
2676
+ meshes.push(null);
2677
+ curves.push(null);
2678
+ continue;
2679
+ }
2680
+ const points = Array.from({ length: wrapNum }, () => new THREE11.Vector3());
2681
+ const curve = new THREE11.CatmullRomCurve3(points, false);
2682
+ const segments = Math.max(wrapNum * 2, 4);
2683
+ const geometry = new THREE11.TubeGeometry(curve, segments, DEFAULT_TENDON_WIDTH, 6, false);
2684
+ const mesh = new THREE11.Mesh(geometry, material);
2685
+ mesh.frustumCulled = false;
2686
+ group.add(mesh);
2687
+ meshes.push(mesh);
2688
+ curves.push(curve);
2502
2689
  }
2503
- meshesRef.current = [];
2690
+ meshesRef.current = meshes;
2691
+ curvesRef.current = curves;
2692
+ return () => {
2693
+ for (const mesh of meshes) {
2694
+ if (!mesh) continue;
2695
+ group.remove(mesh);
2696
+ mesh.geometry.dispose();
2697
+ }
2698
+ material.dispose();
2699
+ meshesRef.current = [];
2700
+ curvesRef.current = [];
2701
+ materialRef.current = null;
2702
+ };
2703
+ }, [status, mjModelRef, mjDataRef]);
2704
+ useFrame(() => {
2705
+ const model = mjModelRef.current;
2706
+ const data = mjDataRef.current;
2707
+ if (!model || !data) return;
2708
+ const ntendon = model.ntendon ?? 0;
2709
+ const meshes = meshesRef.current;
2710
+ const curves = curvesRef.current;
2504
2711
  for (let t = 0; t < ntendon; t++) {
2712
+ const mesh = meshes[t];
2713
+ const curve = curves[t];
2714
+ if (!mesh || !curve) continue;
2505
2715
  const wrapAdr = model.ten_wrapadr[t];
2506
2716
  const wrapNum = model.ten_wrapnum[t];
2507
- if (wrapNum < 2) continue;
2508
- const points = [];
2717
+ let validCount = 0;
2509
2718
  for (let w = 0; w < wrapNum; w++) {
2510
2719
  const idx = (wrapAdr + w) * 3;
2511
2720
  if (data.wrap_xpos && idx + 2 < data.wrap_xpos.length) {
@@ -2513,27 +2722,32 @@ function TendonRenderer() {
2513
2722
  const y = data.wrap_xpos[idx + 1];
2514
2723
  const z = data.wrap_xpos[idx + 2];
2515
2724
  if (x !== 0 || y !== 0 || z !== 0) {
2516
- points.push(new THREE.Vector3(x, y, z));
2725
+ if (validCount < curve.points.length) {
2726
+ curve.points[validCount].set(x, y, z);
2727
+ }
2728
+ validCount++;
2517
2729
  }
2518
2730
  }
2519
2731
  }
2520
- if (points.length < 2) continue;
2521
- const curve = new THREE.CatmullRomCurve3(points, false);
2522
- const geometry = new THREE.TubeGeometry(
2732
+ if (validCount < 2) {
2733
+ mesh.visible = false;
2734
+ continue;
2735
+ }
2736
+ if (curve.points.length !== validCount) {
2737
+ curve.points.length = validCount;
2738
+ while (curve.points.length < validCount) {
2739
+ curve.points.push(new THREE11.Vector3());
2740
+ }
2741
+ }
2742
+ mesh.geometry.dispose();
2743
+ mesh.geometry = new THREE11.TubeGeometry(
2523
2744
  curve,
2524
- Math.max(points.length * 2, 4),
2745
+ Math.max(validCount * 2, 4),
2525
2746
  DEFAULT_TENDON_WIDTH,
2526
2747
  6,
2527
2748
  false
2528
2749
  );
2529
- const material = new THREE.MeshStandardMaterial({
2530
- color: DEFAULT_TENDON_COLOR,
2531
- roughness: 0.6,
2532
- metalness: 0.1
2533
- });
2534
- const mesh = new THREE.Mesh(geometry, material);
2535
- group.add(mesh);
2536
- meshesRef.current.push(mesh);
2750
+ mesh.visible = true;
2537
2751
  }
2538
2752
  });
2539
2753
  if (status !== "ready") return null;
@@ -2553,24 +2767,24 @@ function FlexRenderer() {
2553
2767
  const vertAdr = model.flex_vertadr[f];
2554
2768
  const vertNum = model.flex_vertnum[f];
2555
2769
  if (vertNum === 0) continue;
2556
- const geometry = new THREE.BufferGeometry();
2770
+ const geometry = new THREE11.BufferGeometry();
2557
2771
  const positions = new Float32Array(vertNum * 3);
2558
- geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
2772
+ geometry.setAttribute("position", new THREE11.BufferAttribute(positions, 3));
2559
2773
  geometry.computeVertexNormals();
2560
- let color = new THREE.Color(0.5, 0.5, 0.5);
2774
+ let color = new THREE11.Color(0.5, 0.5, 0.5);
2561
2775
  if (model.flex_rgba) {
2562
- color = new THREE.Color(
2776
+ color = new THREE11.Color(
2563
2777
  model.flex_rgba[4 * f],
2564
2778
  model.flex_rgba[4 * f + 1],
2565
2779
  model.flex_rgba[4 * f + 2]
2566
2780
  );
2567
2781
  }
2568
- const material = new THREE.MeshStandardMaterial({
2782
+ const material = new THREE11.MeshStandardMaterial({
2569
2783
  color,
2570
2784
  roughness: 0.7,
2571
- side: THREE.DoubleSide
2785
+ side: THREE11.DoubleSide
2572
2786
  });
2573
- const mesh = new THREE.Mesh(geometry, material);
2787
+ const mesh = new THREE11.Mesh(geometry, material);
2574
2788
  mesh.userData.flexId = f;
2575
2789
  mesh.userData.vertAdr = vertAdr;
2576
2790
  mesh.userData.vertNum = vertNum;
@@ -2605,22 +2819,45 @@ function FlexRenderer() {
2605
2819
  if (status !== "ready") return null;
2606
2820
  return /* @__PURE__ */ jsx("group", { ref: groupRef });
2607
2821
  }
2822
+ var geomNameCacheByModel = /* @__PURE__ */ new WeakMap();
2823
+ function getGeomNameCached(model, geomId) {
2824
+ let perModel = geomNameCacheByModel.get(model);
2825
+ if (!perModel) {
2826
+ perModel = /* @__PURE__ */ new Map();
2827
+ geomNameCacheByModel.set(model, perModel);
2828
+ }
2829
+ let name = perModel.get(geomId);
2830
+ if (name === void 0) {
2831
+ name = getName(model, model.name_geomadr[geomId]);
2832
+ perModel.set(geomId, name);
2833
+ }
2834
+ return name;
2835
+ }
2608
2836
  function useContacts(bodyName, callback) {
2609
- const { mjModelRef } = useMujocoSim();
2837
+ const { mjModelRef, status } = useMujocoSim();
2610
2838
  const contactsRef = useRef([]);
2611
2839
  const bodyIdRef = useRef(-1);
2840
+ const bodyResolvedRef = useRef(false);
2612
2841
  const callbackRef = useRef(callback);
2613
2842
  callbackRef.current = callback;
2614
2843
  useEffect(() => {
2615
2844
  if (!bodyName) {
2616
2845
  bodyIdRef.current = -1;
2846
+ bodyResolvedRef.current = true;
2617
2847
  return;
2618
2848
  }
2849
+ bodyResolvedRef.current = false;
2850
+ if (status !== "ready") return;
2619
2851
  const model = mjModelRef.current;
2620
2852
  if (!model) return;
2621
2853
  bodyIdRef.current = findBodyByName(model, bodyName);
2622
- }, [bodyName, mjModelRef]);
2854
+ bodyResolvedRef.current = true;
2855
+ }, [bodyName, status, mjModelRef]);
2623
2856
  useAfterPhysicsStep((model, data) => {
2857
+ if (bodyName && !bodyResolvedRef.current) {
2858
+ bodyIdRef.current = findBodyByName(model, bodyName);
2859
+ bodyResolvedRef.current = true;
2860
+ }
2624
2861
  const ncon = data.ncon;
2625
2862
  if (ncon === 0) {
2626
2863
  if (contactsRef.current.length > 0) contactsRef.current = [];
@@ -2630,24 +2867,21 @@ function useContacts(bodyName, callback) {
2630
2867
  const contacts = [];
2631
2868
  const filterBody = bodyIdRef.current;
2632
2869
  for (let i = 0; i < ncon; i++) {
2633
- try {
2634
- const c = data.contact.get(i);
2635
- if (filterBody >= 0) {
2636
- const b1 = model.geom_bodyid[c.geom1];
2637
- const b2 = model.geom_bodyid[c.geom2];
2638
- if (b1 !== filterBody && b2 !== filterBody) continue;
2639
- }
2640
- contacts.push({
2641
- geom1: c.geom1,
2642
- geom1Name: getName(model, model.name_geomadr[c.geom1]),
2643
- geom2: c.geom2,
2644
- geom2Name: getName(model, model.name_geomadr[c.geom2]),
2645
- pos: [c.pos[0], c.pos[1], c.pos[2]],
2646
- depth: c.dist
2647
- });
2648
- } catch {
2649
- break;
2650
- }
2870
+ const c = getContact(data, i);
2871
+ if (!c) break;
2872
+ if (filterBody >= 0) {
2873
+ const b1 = model.geom_bodyid[c.geom1];
2874
+ const b2 = model.geom_bodyid[c.geom2];
2875
+ if (b1 !== filterBody && b2 !== filterBody) continue;
2876
+ }
2877
+ contacts.push({
2878
+ geom1: c.geom1,
2879
+ geom1Name: getGeomNameCached(model, c.geom1),
2880
+ geom2: c.geom2,
2881
+ geom2Name: getGeomNameCached(model, c.geom2),
2882
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
2883
+ depth: c.dist
2884
+ });
2651
2885
  }
2652
2886
  contactsRef.current = contacts;
2653
2887
  callbackRef.current?.(contacts);
@@ -2781,28 +3015,28 @@ function TrajectoryPlayer({
2781
3015
  onFrame
2782
3016
  }) {
2783
3017
  const player = useTrajectoryPlayer(trajectory, { fps, loop });
3018
+ const onFrameRef = useRef(onFrame);
3019
+ onFrameRef.current = onFrame;
3020
+ const lastReportedFrameRef = useRef(-1);
2784
3021
  useEffect(() => {
2785
3022
  if (playing) {
2786
3023
  player.play();
2787
3024
  } else {
2788
3025
  player.pause();
2789
3026
  }
2790
- }, [playing]);
2791
- useEffect(() => {
2792
- if (onFrame) {
2793
- const interval = setInterval(() => {
2794
- if (player.playing) onFrame(player.frame);
2795
- }, 1e3 / fps);
2796
- return () => clearInterval(interval);
3027
+ }, [playing, player]);
3028
+ useFrame(() => {
3029
+ if (!onFrameRef.current) return;
3030
+ const currentFrame = player.frame;
3031
+ if (currentFrame !== lastReportedFrameRef.current && player.playing) {
3032
+ lastReportedFrameRef.current = currentFrame;
3033
+ onFrameRef.current(currentFrame);
2797
3034
  }
2798
- }, [onFrame, fps]);
3035
+ });
2799
3036
  return null;
2800
3037
  }
2801
- function SelectionHighlight({
2802
- bodyId,
2803
- color = "#ff4444",
2804
- emissiveIntensity = 0.3
2805
- }) {
3038
+ function useSelectionHighlight(bodyId, options = {}) {
3039
+ const { color = "#ff4444", emissiveIntensity = 0.3 } = options;
2806
3040
  const { scene } = useThree();
2807
3041
  const prevMeshesRef = useRef([]);
2808
3042
  useEffect(() => {
@@ -2815,7 +3049,7 @@ function SelectionHighlight({
2815
3049
  }
2816
3050
  prevMeshesRef.current = [];
2817
3051
  if (bodyId === null || bodyId < 0) return;
2818
- const highlightColor = new THREE.Color(color);
3052
+ const highlightColor = new THREE11.Color(color);
2819
3053
  scene.traverse((obj) => {
2820
3054
  if (obj.userData.bodyID === bodyId && obj.isMesh) {
2821
3055
  const mesh = obj;
@@ -2842,6 +3076,15 @@ function SelectionHighlight({
2842
3076
  prevMeshesRef.current = [];
2843
3077
  };
2844
3078
  }, [bodyId, color, emissiveIntensity, scene]);
3079
+ }
3080
+
3081
+ // src/components/SelectionHighlight.tsx
3082
+ function SelectionHighlight({
3083
+ bodyId,
3084
+ color = "#ff4444",
3085
+ emissiveIntensity = 0.3
3086
+ }) {
3087
+ useSelectionHighlight(bodyId, { color, emissiveIntensity });
2845
3088
  return null;
2846
3089
  }
2847
3090
  function useActuators() {
@@ -2862,12 +3105,12 @@ function useActuators() {
2862
3105
  return actuators;
2863
3106
  }, [status, mjModelRef]);
2864
3107
  }
2865
- var _mat42 = new THREE.Matrix4();
3108
+ var _mat42 = new THREE11.Matrix4();
2866
3109
  function useSitePosition(siteName) {
2867
3110
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
2868
3111
  const siteIdRef = useRef(-1);
2869
- const positionRef = useRef(new THREE.Vector3());
2870
- const quaternionRef = useRef(new THREE.Quaternion());
3112
+ const positionRef = useRef(new THREE11.Vector3());
3113
+ const quaternionRef = useRef(new THREE11.Quaternion());
2871
3114
  useEffect(() => {
2872
3115
  const model = mjModelRef.current;
2873
3116
  if (!model || status !== "ready") {
@@ -2996,6 +3239,8 @@ function useJointState(name) {
2996
3239
  const dofDimRef = useRef(1);
2997
3240
  const positionRef = useRef(0);
2998
3241
  const velocityRef = useRef(0);
3242
+ const posBufferRef = useRef(null);
3243
+ const velBufferRef = useRef(null);
2999
3244
  useEffect(() => {
3000
3245
  const model = mjModelRef.current;
3001
3246
  if (!model || status !== "ready") return;
@@ -3015,6 +3260,13 @@ function useJointState(name) {
3015
3260
  qposDimRef.current = 1;
3016
3261
  dofDimRef.current = 1;
3017
3262
  }
3263
+ if (qposDimRef.current > 1) {
3264
+ posBufferRef.current = new Float64Array(qposDimRef.current);
3265
+ velBufferRef.current = new Float64Array(dofDimRef.current);
3266
+ } else {
3267
+ posBufferRef.current = null;
3268
+ velBufferRef.current = null;
3269
+ }
3018
3270
  return;
3019
3271
  }
3020
3272
  }
@@ -3028,8 +3280,12 @@ function useJointState(name) {
3028
3280
  positionRef.current = data.qpos[qa];
3029
3281
  velocityRef.current = data.qvel[da];
3030
3282
  } else {
3031
- positionRef.current = new Float64Array(data.qpos.subarray(qa, qa + qposDimRef.current));
3032
- velocityRef.current = new Float64Array(data.qvel.subarray(da, da + dofDimRef.current));
3283
+ const posBuf = posBufferRef.current;
3284
+ const velBuf = velBufferRef.current;
3285
+ posBuf.set(data.qpos.subarray(qa, qa + qposDimRef.current));
3286
+ velBuf.set(data.qvel.subarray(da, da + dofDimRef.current));
3287
+ positionRef.current = posBuf;
3288
+ velocityRef.current = velBuf;
3033
3289
  }
3034
3290
  });
3035
3291
  return { position: positionRef, velocity: velocityRef };
@@ -3037,10 +3293,10 @@ function useJointState(name) {
3037
3293
  function useBodyState(name) {
3038
3294
  const { mjModelRef, status } = useMujocoSim();
3039
3295
  const bodyIdRef = useRef(-1);
3040
- const position = useRef(new THREE.Vector3());
3041
- const quaternion = useRef(new THREE.Quaternion());
3042
- const linearVelocity = useRef(new THREE.Vector3());
3043
- const angularVelocity = useRef(new THREE.Vector3());
3296
+ const position = useRef(new THREE11.Vector3());
3297
+ const quaternion = useRef(new THREE11.Quaternion());
3298
+ const linearVelocity = useRef(new THREE11.Vector3());
3299
+ const angularVelocity = useRef(new THREE11.Vector3());
3044
3300
  useEffect(() => {
3045
3301
  const model = mjModelRef.current;
3046
3302
  if (!model || status !== "ready") return;
@@ -3377,10 +3633,97 @@ function useCtrlNoise(config = {}) {
3377
3633
  }
3378
3634
  });
3379
3635
  }
3636
+ function useCameraAnimation() {
3637
+ const { camera } = useThree();
3638
+ const orbitTargetRef = useRef(new THREE11.Vector3(0, 0, 0));
3639
+ const cameraAnimRef = useRef({
3640
+ active: false,
3641
+ startPos: new THREE11.Vector3(),
3642
+ endPos: new THREE11.Vector3(),
3643
+ startRot: new THREE11.Quaternion(),
3644
+ endRot: new THREE11.Quaternion(),
3645
+ startTarget: new THREE11.Vector3(),
3646
+ endTarget: new THREE11.Vector3(),
3647
+ startTime: 0,
3648
+ duration: 0,
3649
+ resolve: null
3650
+ });
3651
+ useFrame((state) => {
3652
+ const ca = cameraAnimRef.current;
3653
+ if (!ca.active) return;
3654
+ const now = performance.now();
3655
+ const progress = Math.min((now - ca.startTime) / ca.duration, 1);
3656
+ const ease = progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2;
3657
+ camera.position.lerpVectors(ca.startPos, ca.endPos, ease);
3658
+ camera.quaternion.slerpQuaternions(ca.startRot, ca.endRot, ease);
3659
+ orbitTargetRef.current.lerpVectors(ca.startTarget, ca.endTarget, ease);
3660
+ const orbitControls = state.controls;
3661
+ if (orbitControls?.target) {
3662
+ orbitControls.target.copy(orbitTargetRef.current);
3663
+ }
3664
+ if (progress >= 1) {
3665
+ ca.active = false;
3666
+ camera.position.copy(ca.endPos);
3667
+ camera.quaternion.copy(ca.endRot);
3668
+ orbitTargetRef.current.copy(ca.endTarget);
3669
+ ca.resolve?.();
3670
+ ca.resolve = null;
3671
+ }
3672
+ });
3673
+ const getCameraState = useCallback(
3674
+ () => ({
3675
+ position: camera.position.clone(),
3676
+ target: orbitTargetRef.current.clone()
3677
+ }),
3678
+ [camera]
3679
+ );
3680
+ const moveCameraTo = useCallback(
3681
+ (position, target, durationMs) => {
3682
+ return new Promise((resolve) => {
3683
+ const ca = cameraAnimRef.current;
3684
+ ca.active = true;
3685
+ ca.startTime = performance.now();
3686
+ ca.duration = durationMs;
3687
+ ca.startPos.copy(camera.position);
3688
+ ca.startRot.copy(camera.quaternion);
3689
+ ca.startTarget.copy(orbitTargetRef.current);
3690
+ ca.endPos.copy(position);
3691
+ ca.endTarget.copy(target);
3692
+ const dummyCam = camera.clone();
3693
+ dummyCam.position.copy(position);
3694
+ dummyCam.lookAt(target);
3695
+ ca.endRot.copy(dummyCam.quaternion);
3696
+ ca.resolve = resolve;
3697
+ setTimeout(resolve, durationMs + 100);
3698
+ });
3699
+ },
3700
+ [camera]
3701
+ );
3702
+ return { getCameraState, moveCameraTo };
3703
+ }
3380
3704
  /**
3381
3705
  * @license
3382
3706
  * SPDX-License-Identifier: Apache-2.0
3383
3707
  */
3708
+ /**
3709
+ * @license
3710
+ * SPDX-License-Identifier: Apache-2.0
3711
+ *
3712
+ * createController — typed factory for BYOC (Bring Your Own Controller) plugins.
3713
+ */
3714
+ /**
3715
+ * @license
3716
+ * SPDX-License-Identifier: Apache-2.0
3717
+ *
3718
+ * IkContext — React context for the IK controller plugin.
3719
+ */
3720
+ /**
3721
+ * @license
3722
+ * SPDX-License-Identifier: Apache-2.0
3723
+ *
3724
+ * IkController — composable IK controller plugin.
3725
+ * Extracts all IK logic from MujocoSimProvider into an opt-in component.
3726
+ */
3384
3727
  /**
3385
3728
  * @license
3386
3729
  * SPDX-License-Identifier: Apache-2.0
@@ -3394,6 +3737,14 @@ function useCtrlNoise(config = {}) {
3394
3737
  * Fixed from original: reads data.ncon first, accesses contact via .get(i),
3395
3738
  * limits to maxContacts to avoid WASM heap OOM.
3396
3739
  */
3740
+ /**
3741
+ * @license
3742
+ * SPDX-License-Identifier: Apache-2.0
3743
+ *
3744
+ * useSceneLights — hook form of SceneLights (spec 6.3)
3745
+ *
3746
+ * Auto-creates Three.js lights from MJCF <light> elements.
3747
+ */
3397
3748
  /**
3398
3749
  * @license
3399
3750
  * SPDX-License-Identifier: Apache-2.0
@@ -3456,6 +3807,15 @@ function useCtrlNoise(config = {}) {
3456
3807
  *
3457
3808
  * TrajectoryPlayer — component form of trajectory playback (spec 13.2)
3458
3809
  */
3810
+ /**
3811
+ * @license
3812
+ * SPDX-License-Identifier: Apache-2.0
3813
+ *
3814
+ * useSelectionHighlight — hook form of SelectionHighlight (spec 6.5)
3815
+ *
3816
+ * Applies emissive highlight to all meshes belonging to a body.
3817
+ * Restores original emissive when bodyId changes or hook unmounts.
3818
+ */
3459
3819
  /**
3460
3820
  * @license
3461
3821
  * SPDX-License-Identifier: Apache-2.0
@@ -3522,7 +3882,13 @@ function useCtrlNoise(config = {}) {
3522
3882
  *
3523
3883
  * useCtrlNoise — control noise / perturbation hook (spec 3.2)
3524
3884
  */
3885
+ /**
3886
+ * @license
3887
+ * SPDX-License-Identifier: Apache-2.0
3888
+ *
3889
+ * useCameraAnimation — composable camera animation hook.
3890
+ */
3525
3891
 
3526
- export { ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, MujocoCanvas, MujocoProvider, MujocoSimProvider, SceneLights, SceneRenderer, SelectionHighlight, TendonRenderer, TrajectoryPlayer, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getName, loadScene, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyState, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useJointState, useKeyboardTeleop, useMujoco, useMujocoSim, usePolicy, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
3892
+ export { ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkController, IkGizmo, MujocoCanvas, MujocoProvider, MujocoSimProvider, SceneLights, SceneRenderer, SelectionHighlight, TendonRenderer, TrajectoryPlayer, createController, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getContact, getName, loadScene, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIk, useJointState, useKeyboardTeleop, useMujoco, useMujocoSim, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
3527
3893
  //# sourceMappingURL=index.js.map
3528
3894
  //# sourceMappingURL=index.js.map