mujoco-react 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import loadMujoco from 'mujoco-js';
2
2
  import { createContext, forwardRef, useEffect, useContext, useState, useRef, useCallback, useMemo } from 'react';
3
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { jsx, jsxs } 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
  }
@@ -417,28 +225,25 @@ async function loadScene(mujoco, config, onProgress) {
417
225
  onProgress?.("Loading model...");
418
226
  const mjModel = mujoco.MjModel.loadFromXML(`/working/${config.sceneFile}`);
419
227
  const mjData = new mujoco.MjData(mjModel);
420
- const siteId = findSiteByName(mjModel, config.tcpSiteName ?? "tcp");
421
- const gripperId = findActuatorByName(mjModel, config.gripperActuatorName ?? "gripper");
422
228
  if (config.homeJoints) {
423
- for (let i = 0; i < config.homeJoints.length; i++) {
229
+ const homeCount = Math.min(config.homeJoints.length, mjModel.nu);
230
+ for (let i = 0; i < homeCount; i++) {
424
231
  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
- }
232
+ const qposAdr = getActuatedScalarQposAdr(mjModel, i);
233
+ if (qposAdr !== -1) {
234
+ mjData.qpos[qposAdr] = config.homeJoints[i];
431
235
  }
432
236
  }
433
237
  }
434
238
  mujoco.mj_forward(mjModel, mjData);
435
- return { mjModel, mjData, siteId, gripperId };
239
+ return { mjModel, mjData };
436
240
  }
437
241
  function scanDependencies(xmlString, currentFile, parser, downloaded, queue) {
438
242
  const xmlDoc = parser.parseFromString(xmlString, "text/xml");
439
243
  const compiler = xmlDoc.querySelector("compiler");
440
- const meshDir = compiler?.getAttribute("meshdir") || "";
441
- const textureDir = compiler?.getAttribute("texturedir") || "";
244
+ const assetDir = compiler?.getAttribute("assetdir") || "";
245
+ const meshDir = compiler?.getAttribute("meshdir") || assetDir;
246
+ const textureDir = compiler?.getAttribute("texturedir") || assetDir;
442
247
  const currentDir = currentFile.includes("/") ? currentFile.substring(0, currentFile.lastIndexOf("/") + 1) : "";
443
248
  xmlDoc.querySelectorAll("[file]").forEach((el) => {
444
249
  const fileAttr = el.getAttribute("file");
@@ -519,6 +324,8 @@ var _applyPoint = new Float64Array(3);
519
324
  var _rayPnt = new Float64Array(3);
520
325
  var _rayVec = new Float64Array(3);
521
326
  var _rayGeomId = new Int32Array(1);
327
+ var _projRaycaster = new THREE11.Raycaster();
328
+ var _projNdc = new THREE11.Vector2();
522
329
  var MujocoSimContext = createContext(null);
523
330
  function useMujocoSim() {
524
331
  const ctx = useContext(MujocoSimContext);
@@ -563,7 +370,6 @@ function MujocoSimProvider({
563
370
  substeps,
564
371
  paused,
565
372
  speed,
566
- interpolate,
567
373
  children
568
374
  }) {
569
375
  const { gl, camera } = useThree();
@@ -572,25 +378,18 @@ function MujocoSimProvider({
572
378
  const mjDataRef = useRef(null);
573
379
  const mujocoRef = useRef(mujoco);
574
380
  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
381
  const pausedRef = useRef(paused ?? false);
580
382
  const speedRef = useRef(speed ?? 1);
581
383
  const substepsRef = useRef(substeps ?? 1);
582
- const interpolateRef = useRef(interpolate ?? false);
583
- const firstIkEnableRef = useRef(true);
584
384
  const stepsToRunRef = useRef(0);
585
- useRef(null);
586
- useRef(null);
587
- useRef(0);
385
+ const loadGenRef = useRef(0);
588
386
  const onSelectionRef = useRef(onSelection);
589
387
  onSelectionRef.current = onSelection;
590
388
  const onStepRef = useRef(onStep);
591
389
  onStepRef.current = onStep;
592
390
  const beforeStepCallbacks = useRef(/* @__PURE__ */ new Set());
593
391
  const afterStepCallbacks = useRef(/* @__PURE__ */ new Set());
392
+ const resetCallbacks = useRef(/* @__PURE__ */ new Set());
594
393
  configRef.current = config;
595
394
  useEffect(() => {
596
395
  pausedRef.current = paused ?? false;
@@ -601,9 +400,6 @@ function MujocoSimProvider({
601
400
  useEffect(() => {
602
401
  substepsRef.current = substeps ?? 1;
603
402
  }, [substeps]);
604
- useEffect(() => {
605
- interpolateRef.current = interpolate ?? false;
606
- }, [interpolate]);
607
403
  useEffect(() => {
608
404
  if (!gravity) return;
609
405
  const model = mjModelRef.current;
@@ -618,74 +414,6 @@ function MujocoSimProvider({
618
414
  if (!model?.opt) return;
619
415
  model.opt.timestep = timestep;
620
416
  }, [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
417
  useEffect(() => {
690
418
  let disposed = false;
691
419
  (async () => {
@@ -698,8 +426,6 @@ function MujocoSimProvider({
698
426
  }
699
427
  mjModelRef.current = result.mjModel;
700
428
  mjDataRef.current = result.mjData;
701
- siteIdRef.current = result.siteId;
702
- gripperIdRef.current = result.gripperId;
703
429
  if (gravity && result.mjModel.opt?.gravity) {
704
430
  result.mjModel.opt.gravity[0] = gravity[0];
705
431
  result.mjModel.opt.gravity[1] = gravity[1];
@@ -708,9 +434,6 @@ function MujocoSimProvider({
708
434
  if (timestep !== void 0 && result.mjModel.opt) {
709
435
  result.mjModel.opt.timestep = timestep;
710
436
  }
711
- if (ikTargetRef.current) {
712
- syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
713
- }
714
437
  setStatus("ready");
715
438
  } catch (e) {
716
439
  if (!disposed) {
@@ -744,42 +467,10 @@ function MujocoSimProvider({
744
467
  }
745
468
  }
746
469
  }, [status]);
747
- useFrame((state) => {
470
+ useFrame((_state, delta) => {
748
471
  const model = mjModelRef.current;
749
472
  const data = mjDataRef.current;
750
473
  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
474
  const shouldStep = !pausedRef.current || stepsToRunRef.current > 0;
784
475
  if (!shouldStep) return;
785
476
  for (let i = 0; i < model.nv; i++) {
@@ -788,18 +479,6 @@ function MujocoSimProvider({
788
479
  for (const cb of beforeStepCallbacks.current) {
789
480
  cb(model, data);
790
481
  }
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
482
  const numSubsteps = substepsRef.current;
804
483
  if (stepsToRunRef.current > 0) {
805
484
  for (let s = 0; s < stepsToRunRef.current; s++) {
@@ -808,7 +487,8 @@ function MujocoSimProvider({
808
487
  stepsToRunRef.current = 0;
809
488
  } else {
810
489
  const startSimTime = data.time;
811
- const frameTime = 1 / 60 * speedRef.current;
490
+ const clampedDelta = Math.min(delta, 1 / 15);
491
+ const frameTime = clampedDelta * speedRef.current;
812
492
  while (data.time - startSimTime < frameTime) {
813
493
  for (let s = 0; s < numSubsteps; s++) {
814
494
  mujoco.mj_step(model, data);
@@ -824,72 +504,24 @@ function MujocoSimProvider({
824
504
  const model = mjModelRef.current;
825
505
  const data = mjDataRef.current;
826
506
  if (!model || !data) return;
827
- gizmoAnimRef.current.active = false;
828
507
  mujoco.mj_resetData(model, data);
829
508
  const homeJoints = configRef.current.homeJoints;
830
509
  if (homeJoints) {
831
- for (let i = 0; i < homeJoints.length; i++) {
510
+ const homeCount = Math.min(homeJoints.length, model.nu);
511
+ for (let i = 0; i < homeCount; i++) {
832
512
  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
- }
513
+ const qposAdr = getActuatedScalarQposAdr(model, i);
514
+ if (qposAdr !== -1) {
515
+ data.qpos[qposAdr] = homeJoints[i];
839
516
  }
840
517
  }
841
518
  }
842
519
  configRef.current.onReset?.(model, data);
843
520
  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;
521
+ for (const cb of resetCallbacks.current) {
522
+ cb();
856
523
  }
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
- );
524
+ }, [mujoco]);
893
525
  const setSpeed = useCallback((multiplier) => {
894
526
  speedRef.current = multiplier;
895
527
  }, []);
@@ -1051,19 +683,16 @@ function MujocoSimProvider({
1051
683
  const contacts = [];
1052
684
  const ncon = data.ncon;
1053
685
  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
- }
686
+ const c = getContact(data, i);
687
+ if (!c) break;
688
+ contacts.push({
689
+ geom1: c.geom1,
690
+ geom1Name: getName(model, model.name_geomadr[c.geom1]),
691
+ geom2: c.geom2,
692
+ geom2Name: getName(model, model.name_geomadr[c.geom2]),
693
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
694
+ depth: c.dist
695
+ });
1067
696
  }
1068
697
  return contacts;
1069
698
  }, []);
@@ -1202,7 +831,7 @@ function MujocoSimProvider({
1202
831
  const geomId = _rayGeomId[0];
1203
832
  const bodyId = geomId >= 0 ? model.geom_bodyid[geomId] : -1;
1204
833
  return {
1205
- point: new THREE.Vector3(
834
+ point: new THREE11.Vector3(
1206
835
  origin.x + dir.x * dist,
1207
836
  origin.y + dir.y * dist,
1208
837
  origin.z + dir.z * dist
@@ -1240,10 +869,10 @@ function MujocoSimProvider({
1240
869
  for (let i = 0; i < model.nv; i++) data.qvel[i] = model.key_qvel[qvelOffset + i];
1241
870
  }
1242
871
  mujoco.mj_forward(model, data);
1243
- if (ikTargetRef.current) {
1244
- syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
872
+ for (const cb of resetCallbacks.current) {
873
+ cb();
1245
874
  }
1246
- }, [mujoco, syncGizmoToSite]);
875
+ }, [mujoco]);
1247
876
  const getKeyframeNames = useCallback(() => {
1248
877
  const model = mjModelRef.current;
1249
878
  if (!model) return [];
@@ -1257,6 +886,7 @@ function MujocoSimProvider({
1257
886
  return mjModelRef.current?.nkey ?? 0;
1258
887
  }, []);
1259
888
  const loadSceneApi = useCallback(async (newConfig) => {
889
+ const gen = ++loadGenRef.current;
1260
890
  try {
1261
891
  mjModelRef.current?.delete();
1262
892
  mjDataRef.current?.delete();
@@ -1264,28 +894,21 @@ function MujocoSimProvider({
1264
894
  mjDataRef.current = null;
1265
895
  setStatus("loading");
1266
896
  const result = await loadScene(mujoco, newConfig);
897
+ if (gen !== loadGenRef.current) {
898
+ result.mjModel.delete();
899
+ result.mjData.delete();
900
+ return;
901
+ }
1267
902
  mjModelRef.current = result.mjModel;
1268
903
  mjDataRef.current = result.mjData;
1269
- siteIdRef.current = result.siteId;
1270
- gripperIdRef.current = result.gripperId;
1271
904
  configRef.current = newConfig;
1272
- if (ikTargetRef.current) {
1273
- syncGizmoToSite(result.mjData, result.siteId, ikTargetRef.current);
1274
- }
1275
905
  setStatus("ready");
1276
906
  } catch (e) {
907
+ if (gen !== loadGenRef.current) return;
1277
908
  setStatus("error");
1278
909
  throw e;
1279
910
  }
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
- }, []);
911
+ }, [mujoco]);
1289
912
  const getCanvasSnapshot = useCallback(
1290
913
  (width, height, mimeType = "image/jpeg") => {
1291
914
  if (width && height) {
@@ -1309,9 +932,8 @@ function MujocoSimProvider({
1309
932
  virtCam.lookAt(lookAt);
1310
933
  virtCam.updateMatrixWorld();
1311
934
  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);
935
+ _projNdc.set(x * 2 - 1, -(y * 2 - 1));
936
+ _projRaycaster.setFromCamera(_projNdc, virtCam);
1315
937
  const objects = [];
1316
938
  const scene = camera.parent;
1317
939
  if (scene) {
@@ -1319,7 +941,7 @@ function MujocoSimProvider({
1319
941
  if (c.isMesh) objects.push(c);
1320
942
  });
1321
943
  }
1322
- const hits = raycaster.intersectObjects(objects);
944
+ const hits = _projRaycaster.intersectObjects(objects);
1323
945
  if (hits.length > 0) {
1324
946
  const hitObj = hits[0].object;
1325
947
  const geomId = hitObj.userData.geomID !== void 0 ? hitObj.userData.geomID : -1;
@@ -1359,31 +981,6 @@ function MujocoSimProvider({
1359
981
  model.geom_size[id * 3 + 1] = size[1];
1360
982
  model.geom_size[id * 3 + 2] = size[2];
1361
983
  }, []);
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
984
  const api = useMemo(
1388
985
  () => ({
1389
986
  get status() {
@@ -1425,15 +1022,8 @@ function MujocoSimProvider({
1425
1022
  getKeyframeNames,
1426
1023
  getKeyframeCount,
1427
1024
  loadScene: loadSceneApi,
1428
- setIkEnabled,
1429
- moveTarget,
1430
- syncTargetToSite,
1431
- solveIK,
1432
- getGizmoStats,
1433
1025
  getCanvasSnapshot,
1434
1026
  project2DTo3D,
1435
- getCameraState,
1436
- moveCameraTo,
1437
1027
  setBodyMass,
1438
1028
  setGeomFriction,
1439
1029
  setGeomSize,
@@ -1478,110 +1068,597 @@ function MujocoSimProvider({
1478
1068
  getKeyframeNames,
1479
1069
  getKeyframeCount,
1480
1070
  loadSceneApi,
1481
- setIkEnabled,
1482
- moveTarget,
1483
- syncTargetToSite,
1484
- solveIK,
1485
- getGizmoStats,
1486
1071
  getCanvasSnapshot,
1487
1072
  project2DTo3D,
1488
- getCameraState,
1489
- moveCameraTo,
1490
1073
  setBodyMass,
1491
1074
  setGeomFriction,
1492
1075
  setGeomSize
1493
1076
  ]
1494
1077
  );
1495
- const apiRef = useRef(api);
1496
- apiRef.current = api;
1078
+ const apiRef = useRef(api);
1079
+ apiRef.current = api;
1080
+ const contextValue = useMemo(
1081
+ () => ({
1082
+ api,
1083
+ mjModelRef,
1084
+ mjDataRef,
1085
+ mujocoRef,
1086
+ configRef,
1087
+ pausedRef,
1088
+ speedRef,
1089
+ substepsRef,
1090
+ onSelectionRef,
1091
+ beforeStepCallbacks,
1092
+ afterStepCallbacks,
1093
+ resetCallbacks,
1094
+ status
1095
+ }),
1096
+ [api, status]
1097
+ );
1098
+ return /* @__PURE__ */ jsx(MujocoSimContext.Provider, { value: contextValue, children });
1099
+ }
1100
+ var MujocoCanvas = forwardRef(
1101
+ function MujocoCanvas2({
1102
+ config,
1103
+ onReady,
1104
+ onError,
1105
+ onStep,
1106
+ onSelection,
1107
+ // Declarative physics config
1108
+ gravity,
1109
+ timestep,
1110
+ substeps,
1111
+ paused,
1112
+ speed,
1113
+ children,
1114
+ ...canvasProps
1115
+ }, ref) {
1116
+ const { mujoco, status: wasmStatus, error: wasmError } = useMujoco();
1117
+ useEffect(() => {
1118
+ if (wasmStatus === "error" && onError) {
1119
+ onError(new Error(wasmError ?? "WASM load failed"));
1120
+ }
1121
+ }, [wasmStatus, wasmError, onError]);
1122
+ if (wasmStatus === "error" || wasmStatus === "loading" || !mujoco) {
1123
+ return null;
1124
+ }
1125
+ return /* @__PURE__ */ jsx(Canvas, { ...canvasProps, children: /* @__PURE__ */ jsx(
1126
+ MujocoSimProvider,
1127
+ {
1128
+ mujoco,
1129
+ config,
1130
+ apiRef: ref,
1131
+ onReady,
1132
+ onError,
1133
+ onStep,
1134
+ onSelection,
1135
+ gravity,
1136
+ timestep,
1137
+ substeps,
1138
+ paused,
1139
+ speed,
1140
+ children
1141
+ }
1142
+ ) });
1143
+ }
1144
+ );
1145
+ var MujocoPhysics = forwardRef(
1146
+ function MujocoPhysics2({ onError, children, ...props }, ref) {
1147
+ const { mujoco, status: wasmStatus, error: wasmError } = useMujoco();
1148
+ useEffect(() => {
1149
+ if (wasmStatus === "error" && onError) {
1150
+ onError(new Error(wasmError ?? "WASM load failed"));
1151
+ }
1152
+ }, [wasmStatus, wasmError, onError]);
1153
+ if (wasmStatus === "error" || wasmStatus === "loading" || !mujoco) {
1154
+ return null;
1155
+ }
1156
+ return /* @__PURE__ */ jsx(
1157
+ MujocoSimProvider,
1158
+ {
1159
+ mujoco,
1160
+ apiRef: ref,
1161
+ onError,
1162
+ ...props,
1163
+ children
1164
+ }
1165
+ );
1166
+ }
1167
+ );
1168
+ function shallowEqual(a, b) {
1169
+ const keysA = Object.keys(a);
1170
+ const keysB = Object.keys(b);
1171
+ if (keysA.length !== keysB.length) return false;
1172
+ for (const key of keysA) {
1173
+ if (a[key] !== b[key]) return false;
1174
+ }
1175
+ return true;
1176
+ }
1177
+ function createController(options, Impl) {
1178
+ function Controller({
1179
+ config,
1180
+ children
1181
+ }) {
1182
+ const configObj = config ?? {};
1183
+ const stableRef = useRef(configObj);
1184
+ if (!shallowEqual(stableRef.current, configObj)) {
1185
+ stableRef.current = configObj;
1186
+ }
1187
+ const stableConfig = stableRef.current;
1188
+ const mergedConfig = useMemo(
1189
+ () => ({ ...options.defaultConfig, ...stableConfig }),
1190
+ [stableConfig]
1191
+ );
1192
+ return /* @__PURE__ */ jsx(Impl, { config: mergedConfig, children });
1193
+ }
1194
+ Controller.displayName = options.name;
1195
+ Controller.controllerName = options.name;
1196
+ Controller.defaultConfig = options.defaultConfig ?? {};
1197
+ return Controller;
1198
+ }
1199
+ var IkContext = createContext(null);
1200
+ function useIk(options) {
1201
+ const ctx = useContext(IkContext);
1202
+ if (!ctx && !options?.optional) {
1203
+ throw new Error("useIk() must be used inside an <IkController>");
1204
+ }
1205
+ return ctx;
1206
+ }
1207
+
1208
+ // src/core/GenericIK.ts
1209
+ var DEFAULTS = {
1210
+ maxIterations: 50,
1211
+ damping: 0.01,
1212
+ tolerance: 1e-3,
1213
+ epsilon: 1e-6,
1214
+ posWeight: 1,
1215
+ rotWeight: 0.3
1216
+ };
1217
+ var GenericIK = class {
1218
+ mujoco;
1219
+ constructor(mujoco) {
1220
+ this.mujoco = mujoco;
1221
+ }
1222
+ /**
1223
+ * Solve IK for a target 6-DOF pose.
1224
+ * @param model MuJoCo model
1225
+ * @param data MuJoCo data (qpos will be temporarily modified, then restored)
1226
+ * @param siteId Index of the end-effector site to control
1227
+ * @param numJoints Number of arm joints (assumes qpos[0..numJoints-1])
1228
+ * @param targetPos Target position in world frame
1229
+ * @param targetQuat Target orientation in world frame
1230
+ * @param currentQ Current joint angles (length = numJoints)
1231
+ * @param opts Optional solver parameters
1232
+ * @returns Joint angles array, or null if solver diverged
1233
+ */
1234
+ solve(model, data, siteId, numJoints, targetPos, targetQuat, currentQ, opts) {
1235
+ const o = { ...DEFAULTS, ...opts };
1236
+ const n = numJoints;
1237
+ const savedQpos = new Float64Array(data.qpos.length);
1238
+ savedQpos.set(data.qpos);
1239
+ const R_target = quatToMat3(targetQuat);
1240
+ const q = new Float64Array(n);
1241
+ for (let i = 0; i < n; i++) q[i] = currentQ[i];
1242
+ const J = new Float64Array(6 * n);
1243
+ const JJt = new Float64Array(36);
1244
+ const rhs = new Float64Array(6);
1245
+ const x = new Float64Array(6);
1246
+ const dq = new Float64Array(n);
1247
+ const baseSitePos = new Float64Array(3);
1248
+ const baseSiteMat = new Float64Array(9);
1249
+ const pertSitePos = new Float64Array(3);
1250
+ const pertSiteMat = new Float64Array(9);
1251
+ let bestQ = null;
1252
+ let bestErr = Infinity;
1253
+ for (let iter = 0; iter < o.maxIterations; iter++) {
1254
+ for (let i = 0; i < n; i++) data.qpos[i] = q[i];
1255
+ this.mujoco.mj_forward(model, data);
1256
+ const sp = data.site_xpos;
1257
+ const sm = data.site_xmat;
1258
+ const off3 = siteId * 3;
1259
+ const off9 = siteId * 9;
1260
+ for (let i = 0; i < 3; i++) baseSitePos[i] = sp[off3 + i];
1261
+ for (let i = 0; i < 9; i++) baseSiteMat[i] = sm[off9 + i];
1262
+ const posErr0 = targetPos.x - baseSitePos[0];
1263
+ const posErr1 = targetPos.y - baseSitePos[1];
1264
+ const posErr2 = targetPos.z - baseSitePos[2];
1265
+ const rotErr = orientationError(baseSiteMat, R_target);
1266
+ const error = [
1267
+ posErr0 * o.posWeight,
1268
+ posErr1 * o.posWeight,
1269
+ posErr2 * o.posWeight,
1270
+ rotErr[0] * o.rotWeight,
1271
+ rotErr[1] * o.rotWeight,
1272
+ rotErr[2] * o.rotWeight
1273
+ ];
1274
+ const errNorm = Math.sqrt(
1275
+ error[0] * error[0] + error[1] * error[1] + error[2] * error[2] + error[3] * error[3] + error[4] * error[4] + error[5] * error[5]
1276
+ );
1277
+ if (errNorm < bestErr) {
1278
+ bestErr = errNorm;
1279
+ bestQ = Array.from(q);
1280
+ }
1281
+ if (errNorm < o.tolerance) break;
1282
+ for (let j = 0; j < n; j++) {
1283
+ const saved = data.qpos[j];
1284
+ data.qpos[j] = q[j] + o.epsilon;
1285
+ this.mujoco.mj_forward(model, data);
1286
+ for (let i = 0; i < 3; i++) pertSitePos[i] = sp[off3 + i];
1287
+ for (let i = 0; i < 9; i++) pertSiteMat[i] = sm[off9 + i];
1288
+ J[0 * n + j] = (pertSitePos[0] - baseSitePos[0]) / o.epsilon * o.posWeight;
1289
+ J[1 * n + j] = (pertSitePos[1] - baseSitePos[1]) / o.epsilon * o.posWeight;
1290
+ J[2 * n + j] = (pertSitePos[2] - baseSitePos[2]) / o.epsilon * o.posWeight;
1291
+ const dRot = angularDelta(baseSiteMat, pertSiteMat);
1292
+ J[3 * n + j] = dRot[0] / o.epsilon * o.rotWeight;
1293
+ J[4 * n + j] = dRot[1] / o.epsilon * o.rotWeight;
1294
+ J[5 * n + j] = dRot[2] / o.epsilon * o.rotWeight;
1295
+ data.qpos[j] = saved;
1296
+ }
1297
+ for (let i = 0; i < n; i++) data.qpos[i] = q[i];
1298
+ for (let r = 0; r < 6; r++) {
1299
+ for (let c = 0; c < 6; c++) {
1300
+ let sum = 0;
1301
+ for (let k = 0; k < n; k++) {
1302
+ sum += J[r * n + k] * J[c * n + k];
1303
+ }
1304
+ JJt[r * 6 + c] = sum + (r === c ? o.damping : 0);
1305
+ }
1306
+ }
1307
+ for (let i = 0; i < 6; i++) rhs[i] = error[i];
1308
+ solve6x6(JJt, rhs, x);
1309
+ for (let j = 0; j < n; j++) {
1310
+ let sum = 0;
1311
+ for (let r = 0; r < 6; r++) {
1312
+ sum += J[r * n + j] * x[r];
1313
+ }
1314
+ dq[j] = sum;
1315
+ }
1316
+ for (let i = 0; i < n; i++) q[i] += dq[i];
1317
+ }
1318
+ data.qpos.set(savedQpos);
1319
+ this.mujoco.mj_forward(model, data);
1320
+ return bestQ;
1321
+ }
1322
+ };
1323
+ function quatToMat3(q) {
1324
+ const m = new Float64Array(9);
1325
+ const x = q.x, y = q.y, z = q.z, w = q.w;
1326
+ const xx = x * x, yy = y * y, zz = z * z;
1327
+ const xy = x * y, xz = x * z, yz = y * z;
1328
+ const wx = w * x, wy = w * y, wz = w * z;
1329
+ m[0] = 1 - 2 * (yy + zz);
1330
+ m[1] = 2 * (xy - wz);
1331
+ m[2] = 2 * (xz + wy);
1332
+ m[3] = 2 * (xy + wz);
1333
+ m[4] = 1 - 2 * (xx + zz);
1334
+ m[5] = 2 * (yz - wx);
1335
+ m[6] = 2 * (xz - wy);
1336
+ m[7] = 2 * (yz + wx);
1337
+ m[8] = 1 - 2 * (xx + yy);
1338
+ return m;
1339
+ }
1340
+ function orientationError(R_cur, R_tgt) {
1341
+ const Re = new Float64Array(9);
1342
+ for (let i = 0; i < 3; i++) {
1343
+ for (let j = 0; j < 3; j++) {
1344
+ let s2 = 0;
1345
+ for (let k = 0; k < 3; k++) {
1346
+ s2 += R_tgt[i * 3 + k] * R_cur[j * 3 + k];
1347
+ }
1348
+ Re[i * 3 + j] = s2;
1349
+ }
1350
+ }
1351
+ const trace = Re[0] + Re[4] + Re[8];
1352
+ const cosAngle = Math.max(-1, Math.min(1, (trace - 1) * 0.5));
1353
+ const angle = Math.acos(cosAngle);
1354
+ if (angle < 1e-6) {
1355
+ return [0, 0, 0];
1356
+ }
1357
+ if (angle > Math.PI - 1e-6) {
1358
+ return [
1359
+ 0.5 * (Re[7] - Re[5]),
1360
+ 0.5 * (Re[2] - Re[6]),
1361
+ 0.5 * (Re[3] - Re[1])
1362
+ ];
1363
+ }
1364
+ const s = angle / (2 * Math.sin(angle));
1365
+ return [
1366
+ s * (Re[7] - Re[5]),
1367
+ s * (Re[2] - Re[6]),
1368
+ s * (Re[3] - Re[1])
1369
+ ];
1370
+ }
1371
+ function angularDelta(R_base, R_pert) {
1372
+ const dR = new Float64Array(9);
1373
+ for (let i = 0; i < 3; i++) {
1374
+ for (let j = 0; j < 3; j++) {
1375
+ let s = 0;
1376
+ for (let k = 0; k < 3; k++) {
1377
+ s += R_pert[i * 3 + k] * R_base[j * 3 + k];
1378
+ }
1379
+ dR[i * 3 + j] = s;
1380
+ }
1381
+ }
1382
+ return [
1383
+ 0.5 * (dR[7] - dR[5]),
1384
+ 0.5 * (dR[2] - dR[6]),
1385
+ 0.5 * (dR[3] - dR[1])
1386
+ ];
1387
+ }
1388
+ function solve6x6(A, b, x) {
1389
+ const N = 6;
1390
+ const a = new Float64Array(A);
1391
+ const r = new Float64Array(b);
1392
+ for (let col = 0; col < N; col++) {
1393
+ let maxVal = Math.abs(a[col * N + col]);
1394
+ let maxRow = col;
1395
+ for (let row = col + 1; row < N; row++) {
1396
+ const val = Math.abs(a[row * N + col]);
1397
+ if (val > maxVal) {
1398
+ maxVal = val;
1399
+ maxRow = row;
1400
+ }
1401
+ }
1402
+ if (maxRow !== col) {
1403
+ for (let k = 0; k < N; k++) {
1404
+ const tmp2 = a[col * N + k];
1405
+ a[col * N + k] = a[maxRow * N + k];
1406
+ a[maxRow * N + k] = tmp2;
1407
+ }
1408
+ const tmp = r[col];
1409
+ r[col] = r[maxRow];
1410
+ r[maxRow] = tmp;
1411
+ }
1412
+ const pivot = a[col * N + col];
1413
+ if (Math.abs(pivot) < 1e-12) {
1414
+ x.fill(0);
1415
+ return;
1416
+ }
1417
+ for (let row = col + 1; row < N; row++) {
1418
+ const factor = a[row * N + col] / pivot;
1419
+ for (let k = col; k < N; k++) {
1420
+ a[row * N + k] -= factor * a[col * N + k];
1421
+ }
1422
+ r[row] -= factor * r[col];
1423
+ }
1424
+ }
1425
+ for (let row = N - 1; row >= 0; row--) {
1426
+ let sum = r[row];
1427
+ for (let k = row + 1; k < N; k++) {
1428
+ sum -= a[row * N + k] * x[k];
1429
+ }
1430
+ x[row] = sum / a[row * N + row];
1431
+ }
1432
+ }
1433
+ var _syncMat4 = new THREE11.Matrix4();
1434
+ function syncGizmoToSite(data, siteId, target) {
1435
+ if (siteId === -1) return;
1436
+ const sitePos = data.site_xpos.subarray(siteId * 3, siteId * 3 + 3);
1437
+ const siteMat = data.site_xmat.subarray(siteId * 9, siteId * 9 + 9);
1438
+ target.position.set(sitePos[0], sitePos[1], sitePos[2]);
1439
+ _syncMat4.set(
1440
+ siteMat[0],
1441
+ siteMat[1],
1442
+ siteMat[2],
1443
+ 0,
1444
+ siteMat[3],
1445
+ siteMat[4],
1446
+ siteMat[5],
1447
+ 0,
1448
+ siteMat[6],
1449
+ siteMat[7],
1450
+ siteMat[8],
1451
+ 0,
1452
+ 0,
1453
+ 0,
1454
+ 0,
1455
+ 1
1456
+ );
1457
+ target.quaternion.setFromRotationMatrix(_syncMat4);
1458
+ }
1459
+ function IkControllerImpl({
1460
+ config,
1461
+ children
1462
+ }) {
1463
+ const { mjModelRef, mjDataRef, mujocoRef, configRef, resetCallbacks, status } = useMujocoSim();
1464
+ const ikEnabledRef = useRef(false);
1465
+ const ikCalculatingRef = useRef(false);
1466
+ const ikTargetRef = useRef(new THREE11.Group());
1467
+ const siteIdRef = useRef(-1);
1468
+ const genericIkRef = useRef(new GenericIK(mujocoRef.current));
1469
+ const firstIkEnableRef = useRef(true);
1470
+ const needsInitialSync = useRef(true);
1471
+ const gizmoAnimRef = useRef({
1472
+ active: false,
1473
+ startPos: new THREE11.Vector3(),
1474
+ endPos: new THREE11.Vector3(),
1475
+ startRot: new THREE11.Quaternion(),
1476
+ endRot: new THREE11.Quaternion(),
1477
+ startTime: 0,
1478
+ duration: 1e3
1479
+ });
1480
+ useEffect(() => {
1481
+ const model = mjModelRef.current;
1482
+ if (!model || status !== "ready") {
1483
+ siteIdRef.current = -1;
1484
+ return;
1485
+ }
1486
+ siteIdRef.current = findSiteByName(model, config.siteName);
1487
+ const data = mjDataRef.current;
1488
+ if (data && ikTargetRef.current) {
1489
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1490
+ }
1491
+ }, [config.siteName, status, mjModelRef, mjDataRef]);
1492
+ const ikSolveFn = useCallback(
1493
+ (pos, quat, currentQ) => {
1494
+ if (config.ikSolveFn) return config.ikSolveFn(pos, quat, currentQ);
1495
+ const model = mjModelRef.current;
1496
+ const data = mjDataRef.current;
1497
+ if (!model || !data || siteIdRef.current === -1) return null;
1498
+ return genericIkRef.current.solve(
1499
+ model,
1500
+ data,
1501
+ siteIdRef.current,
1502
+ config.numJoints,
1503
+ pos,
1504
+ quat,
1505
+ currentQ,
1506
+ {
1507
+ damping: config.damping,
1508
+ maxIterations: config.maxIterations
1509
+ }
1510
+ );
1511
+ },
1512
+ [config.ikSolveFn, config.numJoints, config.damping, config.maxIterations, mjModelRef, mjDataRef]
1513
+ );
1514
+ const ikSolveFnRef = useRef(ikSolveFn);
1515
+ ikSolveFnRef.current = ikSolveFn;
1516
+ useFrame(() => {
1517
+ if (needsInitialSync.current && siteIdRef.current !== -1) {
1518
+ const data = mjDataRef.current;
1519
+ if (data && ikTargetRef.current) {
1520
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1521
+ needsInitialSync.current = false;
1522
+ }
1523
+ }
1524
+ const ga = gizmoAnimRef.current;
1525
+ const target = ikTargetRef.current;
1526
+ if (!ga.active || !target) return;
1527
+ const now = performance.now();
1528
+ const elapsed = now - ga.startTime;
1529
+ const t = Math.min(elapsed / ga.duration, 1);
1530
+ const ease = 1 - Math.pow(1 - t, 3);
1531
+ target.position.lerpVectors(ga.startPos, ga.endPos, ease);
1532
+ target.quaternion.slerpQuaternions(ga.startRot, ga.endRot, ease);
1533
+ if (t >= 1) ga.active = false;
1534
+ });
1535
+ useBeforePhysicsStep((model, data) => {
1536
+ if (!ikEnabledRef.current) {
1537
+ ikCalculatingRef.current = false;
1538
+ return;
1539
+ }
1540
+ const target = ikTargetRef.current;
1541
+ if (!target) return;
1542
+ ikCalculatingRef.current = true;
1543
+ const numJoints = config.numJoints;
1544
+ const currentQ = [];
1545
+ for (let i = 0; i < numJoints; i++) currentQ.push(data.qpos[i]);
1546
+ const solution = ikSolveFnRef.current(target.position, target.quaternion, currentQ);
1547
+ if (solution) {
1548
+ for (let i = 0; i < numJoints; i++) data.ctrl[i] = solution[i];
1549
+ }
1550
+ });
1551
+ useEffect(() => {
1552
+ const cb = () => {
1553
+ const data = mjDataRef.current;
1554
+ if (data && ikTargetRef.current) {
1555
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1556
+ }
1557
+ gizmoAnimRef.current.active = false;
1558
+ firstIkEnableRef.current = true;
1559
+ ikEnabledRef.current = false;
1560
+ needsInitialSync.current = true;
1561
+ };
1562
+ resetCallbacks.current.add(cb);
1563
+ return () => {
1564
+ resetCallbacks.current.delete(cb);
1565
+ };
1566
+ }, [resetCallbacks, mjDataRef]);
1567
+ const setIkEnabled = useCallback(
1568
+ (enabled) => {
1569
+ ikEnabledRef.current = enabled;
1570
+ const data = mjDataRef.current;
1571
+ if (enabled && data && !gizmoAnimRef.current.active && ikTargetRef.current) {
1572
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
1573
+ firstIkEnableRef.current = false;
1574
+ }
1575
+ },
1576
+ [mjDataRef]
1577
+ );
1578
+ const syncTargetToSiteApi = useCallback(() => {
1579
+ const data = mjDataRef.current;
1580
+ const target = ikTargetRef.current;
1581
+ if (data && target) syncGizmoToSite(data, siteIdRef.current, target);
1582
+ }, [mjDataRef]);
1583
+ const solveIK = useCallback(
1584
+ (pos, quat, currentQ) => {
1585
+ return ikSolveFnRef.current(pos, quat, currentQ);
1586
+ },
1587
+ []
1588
+ );
1589
+ const moveTarget = useCallback(
1590
+ (pos, duration = 0) => {
1591
+ if (!ikEnabledRef.current) setIkEnabled(true);
1592
+ const target = ikTargetRef.current;
1593
+ if (!target) return;
1594
+ const targetPos = pos.clone();
1595
+ const targetRot = new THREE11.Quaternion().setFromEuler(
1596
+ new THREE11.Euler(Math.PI, 0, 0)
1597
+ );
1598
+ if (duration > 0) {
1599
+ const ga = gizmoAnimRef.current;
1600
+ ga.active = true;
1601
+ ga.startPos.copy(target.position);
1602
+ ga.endPos.copy(targetPos);
1603
+ ga.startRot.copy(target.quaternion);
1604
+ ga.endRot.copy(targetRot);
1605
+ ga.startTime = performance.now();
1606
+ ga.duration = duration;
1607
+ } else {
1608
+ gizmoAnimRef.current.active = false;
1609
+ target.position.copy(targetPos);
1610
+ target.quaternion.copy(targetRot);
1611
+ }
1612
+ },
1613
+ [setIkEnabled]
1614
+ );
1615
+ const getGizmoStats = useCallback(
1616
+ () => {
1617
+ const target = ikTargetRef.current;
1618
+ if (!ikCalculatingRef.current || !target) return null;
1619
+ return {
1620
+ pos: target.position.clone(),
1621
+ rot: new THREE11.Euler().setFromQuaternion(target.quaternion)
1622
+ };
1623
+ },
1624
+ []
1625
+ );
1497
1626
  const contextValue = useMemo(
1498
1627
  () => ({
1499
- api,
1500
- mjModelRef,
1501
- mjDataRef,
1502
- mujocoRef,
1503
- configRef,
1504
- siteIdRef,
1505
- gripperIdRef,
1506
1628
  ikEnabledRef,
1507
1629
  ikCalculatingRef,
1508
- pausedRef,
1509
- speedRef,
1510
- substepsRef,
1511
1630
  ikTargetRef,
1512
- genericIkRef,
1513
- ikSolveFnRef,
1514
- firstIkEnableRef,
1515
- gizmoAnimRef,
1516
- cameraAnimRef,
1517
- onSelectionRef,
1518
- beforeStepCallbacks,
1519
- afterStepCallbacks,
1520
- status
1631
+ siteIdRef,
1632
+ setIkEnabled,
1633
+ moveTarget,
1634
+ syncTargetToSite: syncTargetToSiteApi,
1635
+ solveIK,
1636
+ getGizmoStats
1521
1637
  }),
1522
- [api, status]
1638
+ [setIkEnabled, moveTarget, syncTargetToSiteApi, solveIK, getGizmoStats]
1523
1639
  );
1524
- return /* @__PURE__ */ jsx(MujocoSimContext.Provider, { value: contextValue, children });
1640
+ return /* @__PURE__ */ jsx(IkContext.Provider, { value: contextValue, children });
1525
1641
  }
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
- }
1642
+ var IkController = createController(
1643
+ {
1644
+ name: "IkController",
1645
+ defaultConfig: {
1646
+ damping: 0.01,
1647
+ maxIterations: 50
1648
+ }
1649
+ },
1650
+ IkControllerImpl
1574
1651
  );
1575
- var CapsuleGeometry = class extends THREE.BufferGeometry {
1652
+ var CapsuleGeometry = class extends THREE11.BufferGeometry {
1576
1653
  parameters;
1577
1654
  constructor(radius = 1, length = 1, capSegments = 4, radialSegments = 8) {
1578
1655
  super();
1579
1656
  this.type = "CapsuleGeometry";
1580
1657
  this.parameters = { radius, length, capSegments, radialSegments };
1581
- const path = new THREE.Path();
1658
+ const path = new THREE11.Path();
1582
1659
  path.absarc(0, -length / 2, radius, Math.PI * 1.5, 0, false);
1583
1660
  path.absarc(0, length / 2, radius, 0, Math.PI * 0.5, false);
1584
- const latheGeometry = new THREE.LatheGeometry(path.getPoints(capSegments), radialSegments);
1661
+ const latheGeometry = new THREE11.LatheGeometry(path.getPoints(capSegments), radialSegments);
1585
1662
  const self = this;
1586
1663
  self.setIndex(latheGeometry.getIndex());
1587
1664
  self.setAttribute("position", latheGeometry.getAttribute("position"));
@@ -1589,27 +1666,27 @@ var CapsuleGeometry = class extends THREE.BufferGeometry {
1589
1666
  self.setAttribute("uv", latheGeometry.getAttribute("uv"));
1590
1667
  }
1591
1668
  };
1592
- var Reflector = class extends THREE.Mesh {
1669
+ var Reflector = class extends THREE11.Mesh {
1593
1670
  isReflector = true;
1594
1671
  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();
1672
+ reflectorPlane = new THREE11.Plane();
1673
+ normal = new THREE11.Vector3();
1674
+ reflectorWorldPosition = new THREE11.Vector3();
1675
+ cameraWorldPosition = new THREE11.Vector3();
1676
+ rotationMatrix = new THREE11.Matrix4();
1677
+ lookAtPosition = new THREE11.Vector3(0, 0, -1);
1678
+ clipPlane = new THREE11.Vector4();
1679
+ view = new THREE11.Vector3();
1680
+ target = new THREE11.Vector3();
1681
+ q = new THREE11.Vector4();
1682
+ textureMatrix = new THREE11.Matrix4();
1606
1683
  virtualCamera;
1607
1684
  renderTarget;
1608
1685
  constructor(geometry, options = {}) {
1609
1686
  super(geometry);
1610
1687
  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);
1688
+ this.camera = new THREE11.PerspectiveCamera();
1689
+ const color = options.color !== void 0 ? new THREE11.Color(options.color) : new THREE11.Color(8355711);
1613
1690
  const textureWidth = options.textureWidth || 512;
1614
1691
  const textureHeight = options.textureHeight || 512;
1615
1692
  const clipBias = options.clipBias || 0;
@@ -1617,11 +1694,11 @@ var Reflector = class extends THREE.Mesh {
1617
1694
  const blendTexture = options.texture || void 0;
1618
1695
  const mixStrength = options.mixStrength !== void 0 ? options.mixStrength : 0.25;
1619
1696
  this.virtualCamera = this.camera;
1620
- this.renderTarget = new THREE.WebGLRenderTarget(textureWidth, textureHeight, {
1697
+ this.renderTarget = new THREE11.WebGLRenderTarget(textureWidth, textureHeight, {
1621
1698
  samples: multisample,
1622
- type: THREE.HalfFloatType
1699
+ type: THREE11.HalfFloatType
1623
1700
  });
1624
- this.material = new THREE.MeshPhysicalMaterial({
1701
+ this.material = new THREE11.MeshPhysicalMaterial({
1625
1702
  map: blendTexture,
1626
1703
  color,
1627
1704
  roughness: 0.5,
@@ -1747,7 +1824,7 @@ var GeomBuilder = class {
1747
1824
  const pos = mjModel.geom_pos.subarray(g * 3, g * 3 + 3);
1748
1825
  const quat = mjModel.geom_quat.subarray(g * 4, g * 4 + 4);
1749
1826
  const matId = mjModel.geom_matid[g];
1750
- const color = new THREE.Color(16777215);
1827
+ const color = new THREE11.Color(16777215);
1751
1828
  let opacity = 1;
1752
1829
  if (matId >= 0) {
1753
1830
  const rgba = mjModel.mat_rgba.subarray(matId * 4, matId * 4 + 4);
@@ -1762,16 +1839,16 @@ var GeomBuilder = class {
1762
1839
  let geo = null;
1763
1840
  const getVal = (v) => v?.value ?? v;
1764
1841
  if (type === getVal(MG.mjGEOM_PLANE)) {
1765
- geo = new THREE.PlaneGeometry(size[0] * 2 || 5, size[1] * 2 || 5);
1842
+ geo = new THREE11.PlaneGeometry(size[0] * 2 || 5, size[1] * 2 || 5);
1766
1843
  } else if (type === getVal(MG.mjGEOM_SPHERE)) {
1767
- geo = new THREE.SphereGeometry(size[0], 24, 24);
1844
+ geo = new THREE11.SphereGeometry(size[0], 24, 24);
1768
1845
  } else if (type === getVal(MG.mjGEOM_CAPSULE)) {
1769
1846
  geo = new CapsuleGeometry(size[0], size[1] * 2, 24, 12);
1770
1847
  geo.rotateX(Math.PI / 2);
1771
1848
  } else if (type === getVal(MG.mjGEOM_BOX)) {
1772
- geo = new THREE.BoxGeometry(size[0] * 2, size[1] * 2, size[2] * 2);
1849
+ geo = new THREE11.BoxGeometry(size[0] * 2, size[1] * 2, size[2] * 2);
1773
1850
  } else if (type === getVal(MG.mjGEOM_CYLINDER)) {
1774
- geo = new THREE.CylinderGeometry(size[0], size[0], size[1] * 2, 24);
1851
+ geo = new THREE11.CylinderGeometry(size[0], size[0], size[1] * 2, 24);
1775
1852
  geo.rotateX(Math.PI / 2);
1776
1853
  } else if (type === getVal(MG.mjGEOM_MESH)) {
1777
1854
  const mId = mjModel.geom_dataid[g];
@@ -1779,8 +1856,8 @@ var GeomBuilder = class {
1779
1856
  const vNum = mjModel.mesh_vertnum[mId];
1780
1857
  const fAdr = mjModel.mesh_faceadr[mId];
1781
1858
  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));
1859
+ geo = new THREE11.BufferGeometry();
1860
+ geo.setAttribute("position", new THREE11.Float32BufferAttribute(mjModel.mesh_vert.subarray(vAdr * 3, (vAdr + vNum) * 3), 3));
1784
1861
  geo.setIndex(Array.from(mjModel.mesh_face.subarray(fAdr * 3, (fAdr + fNum) * 3)));
1785
1862
  geo.computeVertexNormals();
1786
1863
  }
@@ -1795,7 +1872,7 @@ var GeomBuilder = class {
1795
1872
  mixStrength: 0.25
1796
1873
  });
1797
1874
  } else {
1798
- mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({
1875
+ mesh = new THREE11.Mesh(geo, new THREE11.MeshStandardMaterial({
1799
1876
  color,
1800
1877
  transparent: opacity < 1,
1801
1878
  opacity,
@@ -1814,16 +1891,17 @@ var GeomBuilder = class {
1814
1891
  return null;
1815
1892
  }
1816
1893
  };
1817
- function SceneRenderer() {
1894
+ function SceneRenderer(props) {
1818
1895
  const { mjModelRef, mjDataRef, mujocoRef, onSelectionRef, status } = useMujocoSim();
1819
1896
  const groupRef = useRef(null);
1820
1897
  const bodyRefs = useRef([]);
1821
1898
  const prevModelRef = useRef(null);
1822
1899
  const geomBuilder = useMemo(() => {
1900
+ if (status !== "ready") return null;
1823
1901
  return new GeomBuilder(mujocoRef.current);
1824
- }, [mujocoRef.current]);
1902
+ }, [status, mujocoRef]);
1825
1903
  useEffect(() => {
1826
- if (status !== "ready") return;
1904
+ if (status !== "ready" || !geomBuilder) return;
1827
1905
  const model = mjModelRef.current;
1828
1906
  const group = groupRef.current;
1829
1907
  if (!model || !group) return;
@@ -1834,7 +1912,7 @@ function SceneRenderer() {
1834
1912
  }
1835
1913
  const refs = [];
1836
1914
  for (let i = 0; i < model.nbody; i++) {
1837
- const bodyGroup = new THREE.Group();
1915
+ const bodyGroup = new THREE11.Group();
1838
1916
  bodyGroup.userData.bodyID = i;
1839
1917
  for (let g = 0; g < model.ngeom; g++) {
1840
1918
  if (model.geom_bodyid[g] === i) {
@@ -1870,38 +1948,34 @@ function SceneRenderer() {
1870
1948
  return /* @__PURE__ */ jsx(
1871
1949
  "group",
1872
1950
  {
1951
+ ...props,
1873
1952
  ref: groupRef,
1874
1953
  onDoubleClick: (e) => {
1954
+ if (typeof props.onDoubleClick === "function") props.onDoubleClick(e);
1875
1955
  e.stopPropagation();
1876
1956
  let obj = e.object;
1877
1957
  while (obj && obj.userData.bodyID === void 0 && obj.parent) {
1878
1958
  obj = obj.parent;
1879
1959
  }
1880
- if (obj && obj.userData.bodyID !== void 0 && obj.userData.bodyID > 0) {
1960
+ const bodyID = obj?.userData.bodyID;
1961
+ if (typeof bodyID === "number" && bodyID > 0) {
1881
1962
  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);
1963
+ if (model && bodyID < model.nbody && onSelectionRef.current) {
1964
+ const name = getName(model, model.name_bodyadr[bodyID]);
1965
+ onSelectionRef.current(bodyID, name);
1885
1966
  }
1886
1967
  }
1887
1968
  }
1888
1969
  }
1889
1970
  );
1890
1971
  }
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);
1972
+ var _mat4 = new THREE11.Matrix4();
1973
+ var _pos = new THREE11.Vector3();
1974
+ var _quat = new THREE11.Quaternion();
1975
+ var _scale = new THREE11.Vector3(1, 1, 1);
1895
1976
  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();
1977
+ const { mjModelRef, mjDataRef, status } = useMujocoSim();
1978
+ const { ikTargetRef, siteIdRef, ikEnabledRef, setIkEnabled } = useIk();
1905
1979
  const wrapperRef = useRef(null);
1906
1980
  const pivotRef = useRef(null);
1907
1981
  const draggingRef = useRef(false);
@@ -1909,19 +1983,15 @@ function IkGizmo({ siteName, scale = 0.18, onDrag }) {
1909
1983
  const { controls } = useThree();
1910
1984
  useEffect(() => {
1911
1985
  const model = mjModelRef.current;
1912
- if (!model || status !== "ready") {
1986
+ if (!model || status !== "ready" || !siteName) {
1913
1987
  localSiteIdRef.current = -1;
1914
1988
  return;
1915
1989
  }
1916
- if (siteName) {
1917
- localSiteIdRef.current = findSiteByName(model, siteName);
1918
- } else {
1919
- localSiteIdRef.current = siteIdRef.current;
1920
- }
1921
- }, [siteName, status, mjModelRef, siteIdRef]);
1990
+ localSiteIdRef.current = findSiteByName(model, siteName);
1991
+ }, [siteName, status, mjModelRef]);
1922
1992
  useFrame(() => {
1923
1993
  const data = mjDataRef.current;
1924
- const sid = localSiteIdRef.current;
1994
+ const sid = siteName ? localSiteIdRef.current : siteIdRef.current;
1925
1995
  if (!data || sid < 0 || !wrapperRef.current) return;
1926
1996
  if (!draggingRef.current) {
1927
1997
  const p = data.site_xpos;
@@ -1966,7 +2036,7 @@ function IkGizmo({ siteName, scale = 0.18, onDrag }) {
1966
2036
  onDragStart: () => {
1967
2037
  draggingRef.current = true;
1968
2038
  if (!onDrag) {
1969
- if (!ikEnabledRef.current) api.setIkEnabled(true);
2039
+ if (!ikEnabledRef.current) setIkEnabled(true);
1970
2040
  }
1971
2041
  if (controls) controls.enabled = false;
1972
2042
  },
@@ -1994,12 +2064,13 @@ function IkGizmo({ siteName, scale = 0.18, onDrag }) {
1994
2064
  }
1995
2065
  ) });
1996
2066
  }
1997
- var _dummy = new THREE.Object3D();
2067
+ var _dummy = new THREE11.Object3D();
1998
2068
  function ContactMarkers({
1999
2069
  maxContacts = 100,
2000
- radius = 5e-3,
2001
- color = "#4f46e5",
2002
- visible = true
2070
+ radius = 8e-3,
2071
+ color = "#22d3ee",
2072
+ visible = true,
2073
+ ...groupProps
2003
2074
  } = {}) {
2004
2075
  const { mjDataRef, status } = useMujocoSim();
2005
2076
  const meshRef = useRef(null);
@@ -2013,62 +2084,53 @@ function ContactMarkers({
2013
2084
  const ncon = data.ncon;
2014
2085
  const count = Math.min(ncon, maxContacts);
2015
2086
  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 {
2087
+ const c = getContact(data, i);
2088
+ if (!c) {
2023
2089
  mesh.count = i;
2024
2090
  mesh.instanceMatrix.needsUpdate = true;
2025
2091
  return;
2026
2092
  }
2093
+ _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
2094
+ _dummy.updateMatrix();
2095
+ mesh.setMatrixAt(i, _dummy.matrix);
2027
2096
  }
2028
2097
  mesh.count = count;
2029
2098
  mesh.instanceMatrix.needsUpdate = true;
2030
2099
  });
2031
2100
  if (status !== "ready") return null;
2032
- return /* @__PURE__ */ jsxs("instancedMesh", { ref: meshRef, args: [void 0, void 0, maxContacts], children: [
2101
+ return /* @__PURE__ */ jsx("group", { ...groupProps, children: /* @__PURE__ */ jsxs("instancedMesh", { ref: meshRef, args: [void 0, void 0, maxContacts], frustumCulled: false, renderOrder: 999, children: [
2033
2102
  /* @__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
- )
2043
- ] });
2103
+ /* @__PURE__ */ jsx("meshBasicMaterial", { color, depthTest: false })
2104
+ ] }) });
2044
2105
  }
2045
2106
  var _force = new Float64Array(3);
2046
2107
  var _torque = new Float64Array(3);
2047
2108
  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();
2109
+ var _bodyPos = new THREE11.Vector3();
2110
+ var _bodyQuat = new THREE11.Quaternion();
2111
+ var _worldHit = new THREE11.Vector3();
2112
+ var _raycaster = new THREE11.Raycaster();
2113
+ var _mouse = new THREE11.Vector2();
2053
2114
  function DragInteraction({
2054
2115
  stiffness = 250,
2055
- showArrow = true
2116
+ showArrow = true,
2117
+ ...groupProps
2056
2118
  }) {
2057
2119
  const { mjDataRef, mujocoRef, mjModelRef, status } = useMujocoSim();
2058
2120
  const { gl, camera, scene, controls } = useThree();
2059
2121
  const draggingRef = useRef(false);
2060
2122
  const bodyIdRef = useRef(-1);
2061
2123
  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());
2124
+ const localHitRef = useRef(new THREE11.Vector3());
2125
+ const grabWorldRef = useRef(new THREE11.Vector3());
2126
+ const mouseWorldRef = useRef(new THREE11.Vector3());
2065
2127
  const arrowRef = useRef(null);
2066
2128
  const groupRef = useRef(null);
2067
2129
  useEffect(() => {
2068
2130
  if (!showArrow || !groupRef.current) return;
2069
- const arrow = new THREE.ArrowHelper(
2070
- new THREE.Vector3(0, 1, 0),
2071
- new THREE.Vector3(),
2131
+ const arrow = new THREE11.ArrowHelper(
2132
+ new THREE11.Vector3(0, 1, 0),
2133
+ new THREE11.Vector3(),
2072
2134
  0.1,
2073
2135
  16729156
2074
2136
  );
@@ -2196,7 +2258,7 @@ function DragInteraction({
2196
2258
  if (!arrow) return;
2197
2259
  if (draggingRef.current && bodyIdRef.current > 0) {
2198
2260
  arrow.visible = true;
2199
- const dir = mouseWorldRef.current.clone().sub(grabWorldRef.current);
2261
+ const dir = _bodyPos.copy(mouseWorldRef.current).sub(grabWorldRef.current);
2200
2262
  const len = dir.length();
2201
2263
  if (len > 1e-3) {
2202
2264
  dir.normalize();
@@ -2209,9 +2271,9 @@ function DragInteraction({
2209
2271
  }
2210
2272
  });
2211
2273
  if (status !== "ready") return null;
2212
- return /* @__PURE__ */ jsx("group", { ref: groupRef });
2274
+ return /* @__PURE__ */ jsx("group", { ...groupProps, ref: groupRef });
2213
2275
  }
2214
- function SceneLights({ intensity = 1 }) {
2276
+ function useSceneLights(intensity = 1) {
2215
2277
  const { mjModelRef, status } = useMujocoSim();
2216
2278
  const { scene } = useThree();
2217
2279
  const lightsRef = useRef([]);
@@ -2239,7 +2301,7 @@ function SceneLights({ intensity = 1 }) {
2239
2301
  const dr = model.light_diffuse ? model.light_diffuse[3 * i] : 1;
2240
2302
  const dg = model.light_diffuse ? model.light_diffuse[3 * i + 1] : 1;
2241
2303
  const db = model.light_diffuse ? model.light_diffuse[3 * i + 2] : 1;
2242
- const color = new THREE.Color(dr, dg, db);
2304
+ const color = new THREE11.Color(dr, dg, db);
2243
2305
  const px = model.light_pos[3 * i];
2244
2306
  const py = model.light_pos[3 * i + 1];
2245
2307
  const pz = model.light_pos[3 * i + 2];
@@ -2247,7 +2309,7 @@ function SceneLights({ intensity = 1 }) {
2247
2309
  const dy = model.light_dir[3 * i + 1];
2248
2310
  const dz = model.light_dir[3 * i + 2];
2249
2311
  if (isDirectional) {
2250
- const light = new THREE.DirectionalLight(color, finalIntensity);
2312
+ const light = new THREE11.DirectionalLight(color, finalIntensity);
2251
2313
  light.position.set(px, py, pz);
2252
2314
  light.target.position.set(px + dx, py + dy, pz + dz);
2253
2315
  light.castShadow = castShadow;
@@ -2270,7 +2332,7 @@ function SceneLights({ intensity = 1 }) {
2270
2332
  const cutoff = model.light_cutoff ? model.light_cutoff[i] : 45;
2271
2333
  const exponent = model.light_exponent ? model.light_exponent[i] : 10;
2272
2334
  const angle = cutoff * Math.PI / 180;
2273
- const light = new THREE.SpotLight(color, finalIntensity, 0, angle, exponent / 128);
2335
+ const light = new THREE11.SpotLight(color, finalIntensity, 0, angle, exponent / 128);
2274
2336
  light.position.set(px, py, pz);
2275
2337
  light.target.position.set(px + dx, py + dy, pz + dz);
2276
2338
  light.castShadow = castShadow;
@@ -2300,6 +2362,11 @@ function SceneLights({ intensity = 1 }) {
2300
2362
  targetsRef.current = [];
2301
2363
  };
2302
2364
  }, [status, mjModelRef, scene, intensity]);
2365
+ }
2366
+
2367
+ // src/components/SceneLights.tsx
2368
+ function SceneLights({ intensity = 1 }) {
2369
+ useSceneLights(intensity);
2303
2370
  return null;
2304
2371
  }
2305
2372
  var JOINT_COLORS = {
@@ -2312,6 +2379,12 @@ var JOINT_COLORS = {
2312
2379
  3: 16776960
2313
2380
  // hinge - yellow
2314
2381
  };
2382
+ var _v3a = new THREE11.Vector3();
2383
+ new THREE11.Vector3();
2384
+ var _quat2 = new THREE11.Quaternion();
2385
+ var _contactPos = new THREE11.Vector3();
2386
+ var _contactNormal = new THREE11.Vector3();
2387
+ var MAX_CONTACT_ARROWS = 50;
2315
2388
  function Debug({
2316
2389
  showGeoms = false,
2317
2390
  showSites = false,
@@ -2319,7 +2392,8 @@ function Debug({
2319
2392
  showContacts = false,
2320
2393
  showCOM = false,
2321
2394
  showInertia = false,
2322
- showTendons = false
2395
+ showTendons = false,
2396
+ ...groupProps
2323
2397
  }) {
2324
2398
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
2325
2399
  const { scene } = useThree();
@@ -2338,21 +2412,21 @@ function Debug({
2338
2412
  let geometry = null;
2339
2413
  switch (type) {
2340
2414
  case 2:
2341
- geometry = new THREE.SphereGeometry(s[3 * i], 12, 8);
2415
+ geometry = new THREE11.SphereGeometry(s[3 * i], 12, 8);
2342
2416
  break;
2343
2417
  case 3:
2344
- geometry = new THREE.CapsuleGeometry(s[3 * i], s[3 * i + 1] * 2, 6, 8);
2418
+ geometry = new THREE11.CapsuleGeometry(s[3 * i], s[3 * i + 1] * 2, 6, 8);
2345
2419
  break;
2346
2420
  case 5:
2347
- geometry = new THREE.CylinderGeometry(s[3 * i], s[3 * i], s[3 * i + 1] * 2, 12);
2421
+ geometry = new THREE11.CylinderGeometry(s[3 * i], s[3 * i], s[3 * i + 1] * 2, 12);
2348
2422
  break;
2349
2423
  case 6:
2350
- geometry = new THREE.BoxGeometry(s[3 * i] * 2, s[3 * i + 1] * 2, s[3 * i + 2] * 2);
2424
+ geometry = new THREE11.BoxGeometry(s[3 * i] * 2, s[3 * i + 1] * 2, s[3 * i + 2] * 2);
2351
2425
  break;
2352
2426
  }
2353
2427
  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);
2428
+ const mat = new THREE11.MeshBasicMaterial({ color: 65280, wireframe: true, transparent: true, opacity: 0.3 });
2429
+ const mesh = new THREE11.Mesh(geometry, mat);
2356
2430
  mesh.userData.geomId = i;
2357
2431
  mesh.userData.bodyId = model.geom_bodyid[i];
2358
2432
  geoms.push(mesh);
@@ -2360,35 +2434,88 @@ function Debug({
2360
2434
  }
2361
2435
  }
2362
2436
  if (showSites) {
2437
+ const siteSize = model.site_size;
2363
2438
  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);
2439
+ let radius = 8e-3;
2440
+ if (siteSize) {
2441
+ radius = Math.max(siteSize[3 * i] * 0.5, 4e-3);
2442
+ } else {
2443
+ const bodyId = model.site_bodyid[i];
2444
+ let maxGeomSize = 0;
2445
+ for (let g = 0; g < model.ngeom; g++) {
2446
+ if (model.geom_bodyid[g] === bodyId) {
2447
+ maxGeomSize = Math.max(maxGeomSize, model.geom_size[3 * g]);
2448
+ }
2449
+ }
2450
+ if (maxGeomSize > 0) radius = maxGeomSize * 0.15;
2451
+ }
2452
+ const geometry = new THREE11.OctahedronGeometry(radius);
2453
+ const mat = new THREE11.MeshBasicMaterial({ color: 16711935, depthTest: false });
2454
+ const mesh = new THREE11.Mesh(geometry, mat);
2455
+ mesh.renderOrder = 999;
2456
+ mesh.frustumCulled = false;
2367
2457
  mesh.userData.siteId = i;
2458
+ const canvas = document.createElement("canvas");
2459
+ canvas.width = 256;
2460
+ canvas.height = 64;
2461
+ const ctx = canvas.getContext("2d");
2462
+ ctx.fillStyle = "#ff00ff";
2463
+ ctx.font = "bold 36px monospace";
2464
+ ctx.textAlign = "center";
2465
+ ctx.fillText(getName(model, model.name_siteadr[i]), 128, 42);
2466
+ const tex = new THREE11.CanvasTexture(canvas);
2467
+ const spriteMat = new THREE11.SpriteMaterial({ map: tex, depthTest: false, transparent: true });
2468
+ const sprite = new THREE11.Sprite(spriteMat);
2469
+ const labelScale = radius * 15;
2470
+ sprite.scale.set(labelScale, labelScale * 0.25, 1);
2471
+ sprite.position.y = radius * 2;
2472
+ sprite.renderOrder = 999;
2473
+ mesh.add(sprite);
2368
2474
  sites.push(mesh);
2369
2475
  }
2370
2476
  }
2371
2477
  if (showJoints) {
2478
+ const jntPos = model.jnt_pos;
2479
+ const jntAxis = model.jnt_axis;
2372
2480
  for (let i = 0; i < model.njnt; i++) {
2373
2481
  const type = model.jnt_type[i];
2374
2482
  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,
2483
+ const bodyId = model.jnt_bodyid[i];
2484
+ let maxGeomSize = 0;
2485
+ for (let g = 0; g < model.ngeom; g++) {
2486
+ if (model.geom_bodyid[g] === bodyId) {
2487
+ maxGeomSize = Math.max(maxGeomSize, model.geom_size[3 * g]);
2488
+ }
2489
+ }
2490
+ const arrowLen = Math.max(maxGeomSize * 0.8, 0.05);
2491
+ const arrow = new THREE11.ArrowHelper(
2492
+ new THREE11.Vector3(0, 0, 1),
2493
+ new THREE11.Vector3(),
2494
+ arrowLen,
2379
2495
  color,
2380
- 0.01,
2381
- 5e-3
2496
+ arrowLen * 0.25,
2497
+ arrowLen * 0.12
2382
2498
  );
2499
+ arrow.renderOrder = 999;
2500
+ arrow.frustumCulled = false;
2501
+ arrow.line.material = new THREE11.LineBasicMaterial({ color, depthTest: false });
2502
+ arrow.cone.material.depthTest = false;
2503
+ arrow.line.renderOrder = 999;
2504
+ arrow.line.frustumCulled = false;
2505
+ arrow.cone.renderOrder = 999;
2506
+ arrow.cone.frustumCulled = false;
2383
2507
  arrow.userData.jointId = i;
2508
+ arrow.userData.bodyId = bodyId;
2509
+ arrow.userData.hasJntPos = !!jntPos;
2510
+ arrow.userData.hasJntAxis = !!jntAxis;
2384
2511
  joints.push(arrow);
2385
2512
  }
2386
2513
  }
2387
2514
  if (showCOM) {
2388
2515
  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);
2516
+ const geometry = new THREE11.SphereGeometry(5e-3, 6, 6);
2517
+ const mat = new THREE11.MeshBasicMaterial({ color: 16711680 });
2518
+ const mesh = new THREE11.Mesh(geometry, mat);
2392
2519
  mesh.userData.bodyId = i;
2393
2520
  comMarkers.push(mesh);
2394
2521
  }
@@ -2416,6 +2543,8 @@ function Debug({
2416
2543
  const model = mjModelRef.current;
2417
2544
  const data = mjDataRef.current;
2418
2545
  if (!model || !data || !debugGeometry) return;
2546
+ const jntPos = model.jnt_pos;
2547
+ const jntAxis = model.jnt_axis;
2419
2548
  for (const mesh of debugGeometry.geoms) {
2420
2549
  const bid = mesh.userData.bodyId;
2421
2550
  const i3 = bid * 3;
@@ -2429,7 +2558,8 @@ function Debug({
2429
2558
  );
2430
2559
  const gid = mesh.userData.geomId;
2431
2560
  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));
2561
+ _v3a.set(gp[3 * gid], gp[3 * gid + 1], gp[3 * gid + 2]).applyQuaternion(mesh.quaternion);
2562
+ mesh.position.add(_v3a);
2433
2563
  }
2434
2564
  for (const mesh of debugGeometry.sites) {
2435
2565
  const sid = mesh.userData.siteId;
@@ -2439,6 +2569,28 @@ function Debug({
2439
2569
  data.site_xpos[3 * sid + 2]
2440
2570
  );
2441
2571
  }
2572
+ for (const obj of debugGeometry.joints) {
2573
+ const arrow = obj;
2574
+ const jid = arrow.userData.jointId;
2575
+ const bid = arrow.userData.bodyId;
2576
+ const i3 = bid * 3;
2577
+ const i4 = bid * 4;
2578
+ _quat2.set(
2579
+ data.xquat[i4 + 1],
2580
+ data.xquat[i4 + 2],
2581
+ data.xquat[i4 + 3],
2582
+ data.xquat[i4]
2583
+ );
2584
+ arrow.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]);
2585
+ if (jntPos) {
2586
+ _v3a.set(jntPos[3 * jid], jntPos[3 * jid + 1], jntPos[3 * jid + 2]).applyQuaternion(_quat2);
2587
+ arrow.position.add(_v3a);
2588
+ }
2589
+ if (jntAxis) {
2590
+ _v3a.set(jntAxis[3 * jid], jntAxis[3 * jid + 1], jntAxis[3 * jid + 2]).applyQuaternion(_quat2).normalize();
2591
+ arrow.setDirection(_v3a);
2592
+ }
2593
+ }
2442
2594
  for (const mesh of debugGeometry.comMarkers) {
2443
2595
  const bid = mesh.userData.bodyId;
2444
2596
  const i3 = bid * 3;
@@ -2446,66 +2598,138 @@ function Debug({
2446
2598
  }
2447
2599
  });
2448
2600
  const contactGroupRef = useRef(null);
2449
- const contactArrowsRef = useRef([]);
2601
+ const contactPoolRef = useRef([]);
2602
+ const contactPoolInitRef = useRef(false);
2603
+ useEffect(() => {
2604
+ const group = contactGroupRef.current;
2605
+ if (!group || contactPoolInitRef.current) return;
2606
+ contactPoolInitRef.current = true;
2607
+ const pool = [];
2608
+ for (let i = 0; i < MAX_CONTACT_ARROWS; i++) {
2609
+ const arrow = new THREE11.ArrowHelper(
2610
+ new THREE11.Vector3(0, 1, 0),
2611
+ new THREE11.Vector3(),
2612
+ 0.1,
2613
+ 16729156,
2614
+ 0.03,
2615
+ 0.015
2616
+ );
2617
+ arrow.visible = false;
2618
+ group.add(arrow);
2619
+ pool.push(arrow);
2620
+ }
2621
+ contactPoolRef.current = pool;
2622
+ return () => {
2623
+ for (const arrow of pool) {
2624
+ group.remove(arrow);
2625
+ arrow.dispose();
2626
+ }
2627
+ contactPoolRef.current = [];
2628
+ contactPoolInitRef.current = false;
2629
+ };
2630
+ }, [showContacts]);
2450
2631
  useFrame(() => {
2451
2632
  if (!showContacts) return;
2452
- const model = mjModelRef.current;
2453
2633
  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 = [];
2634
+ const pool = contactPoolRef.current;
2635
+ if (!data || pool.length === 0) return;
2461
2636
  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
- }
2637
+ let arrowIdx = 0;
2638
+ for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
2639
+ const c = getContact(data, i);
2640
+ if (!c) break;
2641
+ _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
2642
+ _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
2643
+ const force = Math.abs(c.dist) * 100;
2644
+ const length = Math.min(force * 0.01, 0.1);
2645
+ if (length > 1e-3 && arrowIdx < pool.length) {
2646
+ const arrow = pool[arrowIdx];
2647
+ arrow.position.copy(_contactPos);
2648
+ arrow.setDirection(_contactNormal);
2649
+ arrow.setLength(length, length * 0.3, length * 0.15);
2650
+ arrow.visible = true;
2651
+ arrowIdx++;
2652
+ }
2653
+ }
2654
+ for (let i = arrowIdx; i < pool.length; i++) {
2655
+ pool[i].visible = false;
2477
2656
  }
2478
2657
  });
2479
2658
  if (status !== "ready") return null;
2480
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2659
+ return /* @__PURE__ */ jsxs("group", { ...groupProps, children: [
2481
2660
  /* @__PURE__ */ jsx("group", { ref: groupRef }),
2482
2661
  showContacts && /* @__PURE__ */ jsx("group", { ref: contactGroupRef })
2483
2662
  ] });
2484
2663
  }
2485
- var DEFAULT_TENDON_COLOR = new THREE.Color(0.3, 0.3, 0.8);
2664
+ var DEFAULT_TENDON_COLOR = new THREE11.Color(0.3, 0.3, 0.8);
2486
2665
  var DEFAULT_TENDON_WIDTH = 2e-3;
2487
- function TendonRenderer() {
2666
+ new THREE11.Vector3();
2667
+ function TendonRenderer(props) {
2488
2668
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
2489
2669
  const groupRef = useRef(null);
2490
2670
  const meshesRef = useRef([]);
2491
- useFrame(() => {
2671
+ const curvesRef = useRef([]);
2672
+ const materialRef = useRef(null);
2673
+ useEffect(() => {
2492
2674
  const model = mjModelRef.current;
2493
2675
  const data = mjDataRef.current;
2494
2676
  const group = groupRef.current;
2495
2677
  if (!model || !data || !group) return;
2496
2678
  const ntendon = model.ntendon ?? 0;
2497
2679
  if (ntendon === 0) return;
2498
- for (const mesh of meshesRef.current) {
2499
- group.remove(mesh);
2500
- mesh.geometry.dispose();
2501
- mesh.material.dispose();
2680
+ const material = new THREE11.MeshStandardMaterial({
2681
+ color: DEFAULT_TENDON_COLOR,
2682
+ roughness: 0.6,
2683
+ metalness: 0.1
2684
+ });
2685
+ materialRef.current = material;
2686
+ const meshes = [];
2687
+ const curves = [];
2688
+ for (let t = 0; t < ntendon; t++) {
2689
+ const wrapNum = model.ten_wrapnum[t];
2690
+ if (wrapNum < 2) {
2691
+ meshes.push(null);
2692
+ curves.push(null);
2693
+ continue;
2694
+ }
2695
+ const points = Array.from({ length: wrapNum }, () => new THREE11.Vector3());
2696
+ const curve = new THREE11.CatmullRomCurve3(points, false);
2697
+ const segments = Math.max(wrapNum * 2, 4);
2698
+ const geometry = new THREE11.TubeGeometry(curve, segments, DEFAULT_TENDON_WIDTH, 6, false);
2699
+ const mesh = new THREE11.Mesh(geometry, material);
2700
+ mesh.frustumCulled = false;
2701
+ group.add(mesh);
2702
+ meshes.push(mesh);
2703
+ curves.push(curve);
2502
2704
  }
2503
- meshesRef.current = [];
2705
+ meshesRef.current = meshes;
2706
+ curvesRef.current = curves;
2707
+ return () => {
2708
+ for (const mesh of meshes) {
2709
+ if (!mesh) continue;
2710
+ group.remove(mesh);
2711
+ mesh.geometry.dispose();
2712
+ }
2713
+ material.dispose();
2714
+ meshesRef.current = [];
2715
+ curvesRef.current = [];
2716
+ materialRef.current = null;
2717
+ };
2718
+ }, [status, mjModelRef, mjDataRef]);
2719
+ useFrame(() => {
2720
+ const model = mjModelRef.current;
2721
+ const data = mjDataRef.current;
2722
+ if (!model || !data) return;
2723
+ const ntendon = model.ntendon ?? 0;
2724
+ const meshes = meshesRef.current;
2725
+ const curves = curvesRef.current;
2504
2726
  for (let t = 0; t < ntendon; t++) {
2727
+ const mesh = meshes[t];
2728
+ const curve = curves[t];
2729
+ if (!mesh || !curve) continue;
2505
2730
  const wrapAdr = model.ten_wrapadr[t];
2506
2731
  const wrapNum = model.ten_wrapnum[t];
2507
- if (wrapNum < 2) continue;
2508
- const points = [];
2732
+ let validCount = 0;
2509
2733
  for (let w = 0; w < wrapNum; w++) {
2510
2734
  const idx = (wrapAdr + w) * 3;
2511
2735
  if (data.wrap_xpos && idx + 2 < data.wrap_xpos.length) {
@@ -2513,33 +2737,38 @@ function TendonRenderer() {
2513
2737
  const y = data.wrap_xpos[idx + 1];
2514
2738
  const z = data.wrap_xpos[idx + 2];
2515
2739
  if (x !== 0 || y !== 0 || z !== 0) {
2516
- points.push(new THREE.Vector3(x, y, z));
2740
+ if (validCount < curve.points.length) {
2741
+ curve.points[validCount].set(x, y, z);
2742
+ }
2743
+ validCount++;
2517
2744
  }
2518
2745
  }
2519
2746
  }
2520
- if (points.length < 2) continue;
2521
- const curve = new THREE.CatmullRomCurve3(points, false);
2522
- const geometry = new THREE.TubeGeometry(
2747
+ if (validCount < 2) {
2748
+ mesh.visible = false;
2749
+ continue;
2750
+ }
2751
+ if (curve.points.length !== validCount) {
2752
+ curve.points.length = validCount;
2753
+ while (curve.points.length < validCount) {
2754
+ curve.points.push(new THREE11.Vector3());
2755
+ }
2756
+ }
2757
+ mesh.geometry.dispose();
2758
+ mesh.geometry = new THREE11.TubeGeometry(
2523
2759
  curve,
2524
- Math.max(points.length * 2, 4),
2760
+ Math.max(validCount * 2, 4),
2525
2761
  DEFAULT_TENDON_WIDTH,
2526
2762
  6,
2527
2763
  false
2528
2764
  );
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);
2765
+ mesh.visible = true;
2537
2766
  }
2538
2767
  });
2539
2768
  if (status !== "ready") return null;
2540
- return /* @__PURE__ */ jsx("group", { ref: groupRef });
2769
+ return /* @__PURE__ */ jsx("group", { ...props, ref: groupRef });
2541
2770
  }
2542
- function FlexRenderer() {
2771
+ function FlexRenderer(props) {
2543
2772
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
2544
2773
  const groupRef = useRef(null);
2545
2774
  const meshesRef = useRef([]);
@@ -2553,24 +2782,24 @@ function FlexRenderer() {
2553
2782
  const vertAdr = model.flex_vertadr[f];
2554
2783
  const vertNum = model.flex_vertnum[f];
2555
2784
  if (vertNum === 0) continue;
2556
- const geometry = new THREE.BufferGeometry();
2785
+ const geometry = new THREE11.BufferGeometry();
2557
2786
  const positions = new Float32Array(vertNum * 3);
2558
- geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
2787
+ geometry.setAttribute("position", new THREE11.BufferAttribute(positions, 3));
2559
2788
  geometry.computeVertexNormals();
2560
- let color = new THREE.Color(0.5, 0.5, 0.5);
2789
+ let color = new THREE11.Color(0.5, 0.5, 0.5);
2561
2790
  if (model.flex_rgba) {
2562
- color = new THREE.Color(
2791
+ color = new THREE11.Color(
2563
2792
  model.flex_rgba[4 * f],
2564
2793
  model.flex_rgba[4 * f + 1],
2565
2794
  model.flex_rgba[4 * f + 2]
2566
2795
  );
2567
2796
  }
2568
- const material = new THREE.MeshStandardMaterial({
2797
+ const material = new THREE11.MeshStandardMaterial({
2569
2798
  color,
2570
2799
  roughness: 0.7,
2571
- side: THREE.DoubleSide
2800
+ side: THREE11.DoubleSide
2572
2801
  });
2573
- const mesh = new THREE.Mesh(geometry, material);
2802
+ const mesh = new THREE11.Mesh(geometry, material);
2574
2803
  mesh.userData.flexId = f;
2575
2804
  mesh.userData.vertAdr = vertAdr;
2576
2805
  mesh.userData.vertNum = vertNum;
@@ -2603,24 +2832,47 @@ function FlexRenderer() {
2603
2832
  }
2604
2833
  });
2605
2834
  if (status !== "ready") return null;
2606
- return /* @__PURE__ */ jsx("group", { ref: groupRef });
2835
+ return /* @__PURE__ */ jsx("group", { ...props, ref: groupRef });
2836
+ }
2837
+ var geomNameCacheByModel = /* @__PURE__ */ new WeakMap();
2838
+ function getGeomNameCached(model, geomId) {
2839
+ let perModel = geomNameCacheByModel.get(model);
2840
+ if (!perModel) {
2841
+ perModel = /* @__PURE__ */ new Map();
2842
+ geomNameCacheByModel.set(model, perModel);
2843
+ }
2844
+ let name = perModel.get(geomId);
2845
+ if (name === void 0) {
2846
+ name = getName(model, model.name_geomadr[geomId]);
2847
+ perModel.set(geomId, name);
2848
+ }
2849
+ return name;
2607
2850
  }
2608
2851
  function useContacts(bodyName, callback) {
2609
- const { mjModelRef } = useMujocoSim();
2852
+ const { mjModelRef, status } = useMujocoSim();
2610
2853
  const contactsRef = useRef([]);
2611
2854
  const bodyIdRef = useRef(-1);
2855
+ const bodyResolvedRef = useRef(false);
2612
2856
  const callbackRef = useRef(callback);
2613
2857
  callbackRef.current = callback;
2614
2858
  useEffect(() => {
2615
2859
  if (!bodyName) {
2616
2860
  bodyIdRef.current = -1;
2861
+ bodyResolvedRef.current = true;
2617
2862
  return;
2618
2863
  }
2864
+ bodyResolvedRef.current = false;
2865
+ if (status !== "ready") return;
2619
2866
  const model = mjModelRef.current;
2620
2867
  if (!model) return;
2621
2868
  bodyIdRef.current = findBodyByName(model, bodyName);
2622
- }, [bodyName, mjModelRef]);
2869
+ bodyResolvedRef.current = true;
2870
+ }, [bodyName, status, mjModelRef]);
2623
2871
  useAfterPhysicsStep((model, data) => {
2872
+ if (bodyName && !bodyResolvedRef.current) {
2873
+ bodyIdRef.current = findBodyByName(model, bodyName);
2874
+ bodyResolvedRef.current = true;
2875
+ }
2624
2876
  const ncon = data.ncon;
2625
2877
  if (ncon === 0) {
2626
2878
  if (contactsRef.current.length > 0) contactsRef.current = [];
@@ -2630,24 +2882,21 @@ function useContacts(bodyName, callback) {
2630
2882
  const contacts = [];
2631
2883
  const filterBody = bodyIdRef.current;
2632
2884
  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
- }
2885
+ const c = getContact(data, i);
2886
+ if (!c) break;
2887
+ if (filterBody >= 0) {
2888
+ const b1 = model.geom_bodyid[c.geom1];
2889
+ const b2 = model.geom_bodyid[c.geom2];
2890
+ if (b1 !== filterBody && b2 !== filterBody) continue;
2891
+ }
2892
+ contacts.push({
2893
+ geom1: c.geom1,
2894
+ geom1Name: getGeomNameCached(model, c.geom1),
2895
+ geom2: c.geom2,
2896
+ geom2Name: getGeomNameCached(model, c.geom2),
2897
+ pos: [c.pos[0], c.pos[1], c.pos[2]],
2898
+ depth: c.dist
2899
+ });
2651
2900
  }
2652
2901
  contactsRef.current = contacts;
2653
2902
  callbackRef.current?.(contacts);
@@ -2781,28 +3030,28 @@ function TrajectoryPlayer({
2781
3030
  onFrame
2782
3031
  }) {
2783
3032
  const player = useTrajectoryPlayer(trajectory, { fps, loop });
3033
+ const onFrameRef = useRef(onFrame);
3034
+ onFrameRef.current = onFrame;
3035
+ const lastReportedFrameRef = useRef(-1);
2784
3036
  useEffect(() => {
2785
3037
  if (playing) {
2786
3038
  player.play();
2787
3039
  } else {
2788
3040
  player.pause();
2789
3041
  }
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);
3042
+ }, [playing, player]);
3043
+ useFrame(() => {
3044
+ if (!onFrameRef.current) return;
3045
+ const currentFrame = player.frame;
3046
+ if (currentFrame !== lastReportedFrameRef.current && player.playing) {
3047
+ lastReportedFrameRef.current = currentFrame;
3048
+ onFrameRef.current(currentFrame);
2797
3049
  }
2798
- }, [onFrame, fps]);
3050
+ });
2799
3051
  return null;
2800
3052
  }
2801
- function SelectionHighlight({
2802
- bodyId,
2803
- color = "#ff4444",
2804
- emissiveIntensity = 0.3
2805
- }) {
3053
+ function useSelectionHighlight(bodyId, options = {}) {
3054
+ const { color = "#ff4444", emissiveIntensity = 0.3 } = options;
2806
3055
  const { scene } = useThree();
2807
3056
  const prevMeshesRef = useRef([]);
2808
3057
  useEffect(() => {
@@ -2815,7 +3064,7 @@ function SelectionHighlight({
2815
3064
  }
2816
3065
  prevMeshesRef.current = [];
2817
3066
  if (bodyId === null || bodyId < 0) return;
2818
- const highlightColor = new THREE.Color(color);
3067
+ const highlightColor = new THREE11.Color(color);
2819
3068
  scene.traverse((obj) => {
2820
3069
  if (obj.userData.bodyID === bodyId && obj.isMesh) {
2821
3070
  const mesh = obj;
@@ -2842,6 +3091,15 @@ function SelectionHighlight({
2842
3091
  prevMeshesRef.current = [];
2843
3092
  };
2844
3093
  }, [bodyId, color, emissiveIntensity, scene]);
3094
+ }
3095
+
3096
+ // src/components/SelectionHighlight.tsx
3097
+ function SelectionHighlight({
3098
+ bodyId,
3099
+ color = "#ff4444",
3100
+ emissiveIntensity = 0.3
3101
+ }) {
3102
+ useSelectionHighlight(bodyId, { color, emissiveIntensity });
2845
3103
  return null;
2846
3104
  }
2847
3105
  function useActuators() {
@@ -2862,12 +3120,12 @@ function useActuators() {
2862
3120
  return actuators;
2863
3121
  }, [status, mjModelRef]);
2864
3122
  }
2865
- var _mat42 = new THREE.Matrix4();
3123
+ var _mat42 = new THREE11.Matrix4();
2866
3124
  function useSitePosition(siteName) {
2867
3125
  const { mjModelRef, mjDataRef, status } = useMujocoSim();
2868
3126
  const siteIdRef = useRef(-1);
2869
- const positionRef = useRef(new THREE.Vector3());
2870
- const quaternionRef = useRef(new THREE.Quaternion());
3127
+ const positionRef = useRef(new THREE11.Vector3());
3128
+ const quaternionRef = useRef(new THREE11.Quaternion());
2871
3129
  useEffect(() => {
2872
3130
  const model = mjModelRef.current;
2873
3131
  if (!model || status !== "ready") {
@@ -2996,6 +3254,8 @@ function useJointState(name) {
2996
3254
  const dofDimRef = useRef(1);
2997
3255
  const positionRef = useRef(0);
2998
3256
  const velocityRef = useRef(0);
3257
+ const posBufferRef = useRef(null);
3258
+ const velBufferRef = useRef(null);
2999
3259
  useEffect(() => {
3000
3260
  const model = mjModelRef.current;
3001
3261
  if (!model || status !== "ready") return;
@@ -3015,6 +3275,13 @@ function useJointState(name) {
3015
3275
  qposDimRef.current = 1;
3016
3276
  dofDimRef.current = 1;
3017
3277
  }
3278
+ if (qposDimRef.current > 1) {
3279
+ posBufferRef.current = new Float64Array(qposDimRef.current);
3280
+ velBufferRef.current = new Float64Array(dofDimRef.current);
3281
+ } else {
3282
+ posBufferRef.current = null;
3283
+ velBufferRef.current = null;
3284
+ }
3018
3285
  return;
3019
3286
  }
3020
3287
  }
@@ -3028,8 +3295,12 @@ function useJointState(name) {
3028
3295
  positionRef.current = data.qpos[qa];
3029
3296
  velocityRef.current = data.qvel[da];
3030
3297
  } 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));
3298
+ const posBuf = posBufferRef.current;
3299
+ const velBuf = velBufferRef.current;
3300
+ posBuf.set(data.qpos.subarray(qa, qa + qposDimRef.current));
3301
+ velBuf.set(data.qvel.subarray(da, da + dofDimRef.current));
3302
+ positionRef.current = posBuf;
3303
+ velocityRef.current = velBuf;
3033
3304
  }
3034
3305
  });
3035
3306
  return { position: positionRef, velocity: velocityRef };
@@ -3037,10 +3308,10 @@ function useJointState(name) {
3037
3308
  function useBodyState(name) {
3038
3309
  const { mjModelRef, status } = useMujocoSim();
3039
3310
  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());
3311
+ const position = useRef(new THREE11.Vector3());
3312
+ const quaternion = useRef(new THREE11.Quaternion());
3313
+ const linearVelocity = useRef(new THREE11.Vector3());
3314
+ const angularVelocity = useRef(new THREE11.Vector3());
3044
3315
  useEffect(() => {
3045
3316
  const model = mjModelRef.current;
3046
3317
  if (!model || status !== "ready") return;
@@ -3377,10 +3648,97 @@ function useCtrlNoise(config = {}) {
3377
3648
  }
3378
3649
  });
3379
3650
  }
3651
+ function useCameraAnimation() {
3652
+ const { camera } = useThree();
3653
+ const orbitTargetRef = useRef(new THREE11.Vector3(0, 0, 0));
3654
+ const cameraAnimRef = useRef({
3655
+ active: false,
3656
+ startPos: new THREE11.Vector3(),
3657
+ endPos: new THREE11.Vector3(),
3658
+ startRot: new THREE11.Quaternion(),
3659
+ endRot: new THREE11.Quaternion(),
3660
+ startTarget: new THREE11.Vector3(),
3661
+ endTarget: new THREE11.Vector3(),
3662
+ startTime: 0,
3663
+ duration: 0,
3664
+ resolve: null
3665
+ });
3666
+ useFrame((state) => {
3667
+ const ca = cameraAnimRef.current;
3668
+ if (!ca.active) return;
3669
+ const now = performance.now();
3670
+ const progress = Math.min((now - ca.startTime) / ca.duration, 1);
3671
+ const ease = progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2;
3672
+ camera.position.lerpVectors(ca.startPos, ca.endPos, ease);
3673
+ camera.quaternion.slerpQuaternions(ca.startRot, ca.endRot, ease);
3674
+ orbitTargetRef.current.lerpVectors(ca.startTarget, ca.endTarget, ease);
3675
+ const orbitControls = state.controls;
3676
+ if (orbitControls?.target) {
3677
+ orbitControls.target.copy(orbitTargetRef.current);
3678
+ }
3679
+ if (progress >= 1) {
3680
+ ca.active = false;
3681
+ camera.position.copy(ca.endPos);
3682
+ camera.quaternion.copy(ca.endRot);
3683
+ orbitTargetRef.current.copy(ca.endTarget);
3684
+ ca.resolve?.();
3685
+ ca.resolve = null;
3686
+ }
3687
+ });
3688
+ const getCameraState = useCallback(
3689
+ () => ({
3690
+ position: camera.position.clone(),
3691
+ target: orbitTargetRef.current.clone()
3692
+ }),
3693
+ [camera]
3694
+ );
3695
+ const moveCameraTo = useCallback(
3696
+ (position, target, durationMs) => {
3697
+ return new Promise((resolve) => {
3698
+ const ca = cameraAnimRef.current;
3699
+ ca.active = true;
3700
+ ca.startTime = performance.now();
3701
+ ca.duration = durationMs;
3702
+ ca.startPos.copy(camera.position);
3703
+ ca.startRot.copy(camera.quaternion);
3704
+ ca.startTarget.copy(orbitTargetRef.current);
3705
+ ca.endPos.copy(position);
3706
+ ca.endTarget.copy(target);
3707
+ const dummyCam = camera.clone();
3708
+ dummyCam.position.copy(position);
3709
+ dummyCam.lookAt(target);
3710
+ ca.endRot.copy(dummyCam.quaternion);
3711
+ ca.resolve = resolve;
3712
+ setTimeout(resolve, durationMs + 100);
3713
+ });
3714
+ },
3715
+ [camera]
3716
+ );
3717
+ return { getCameraState, moveCameraTo };
3718
+ }
3380
3719
  /**
3381
3720
  * @license
3382
3721
  * SPDX-License-Identifier: Apache-2.0
3383
3722
  */
3723
+ /**
3724
+ * @license
3725
+ * SPDX-License-Identifier: Apache-2.0
3726
+ *
3727
+ * createController — typed factory for BYOC (Bring Your Own Controller) plugins.
3728
+ */
3729
+ /**
3730
+ * @license
3731
+ * SPDX-License-Identifier: Apache-2.0
3732
+ *
3733
+ * IkContext — React context for the IK controller plugin.
3734
+ */
3735
+ /**
3736
+ * @license
3737
+ * SPDX-License-Identifier: Apache-2.0
3738
+ *
3739
+ * IkController — composable IK controller plugin.
3740
+ * Extracts all IK logic from MujocoSimProvider into an opt-in component.
3741
+ */
3384
3742
  /**
3385
3743
  * @license
3386
3744
  * SPDX-License-Identifier: Apache-2.0
@@ -3394,6 +3752,14 @@ function useCtrlNoise(config = {}) {
3394
3752
  * Fixed from original: reads data.ncon first, accesses contact via .get(i),
3395
3753
  * limits to maxContacts to avoid WASM heap OOM.
3396
3754
  */
3755
+ /**
3756
+ * @license
3757
+ * SPDX-License-Identifier: Apache-2.0
3758
+ *
3759
+ * useSceneLights — hook form of SceneLights (spec 6.3)
3760
+ *
3761
+ * Auto-creates Three.js lights from MJCF <light> elements.
3762
+ */
3397
3763
  /**
3398
3764
  * @license
3399
3765
  * SPDX-License-Identifier: Apache-2.0
@@ -3456,6 +3822,15 @@ function useCtrlNoise(config = {}) {
3456
3822
  *
3457
3823
  * TrajectoryPlayer — component form of trajectory playback (spec 13.2)
3458
3824
  */
3825
+ /**
3826
+ * @license
3827
+ * SPDX-License-Identifier: Apache-2.0
3828
+ *
3829
+ * useSelectionHighlight — hook form of SelectionHighlight (spec 6.5)
3830
+ *
3831
+ * Applies emissive highlight to all meshes belonging to a body.
3832
+ * Restores original emissive when bodyId changes or hook unmounts.
3833
+ */
3459
3834
  /**
3460
3835
  * @license
3461
3836
  * SPDX-License-Identifier: Apache-2.0
@@ -3522,7 +3897,13 @@ function useCtrlNoise(config = {}) {
3522
3897
  *
3523
3898
  * useCtrlNoise — control noise / perturbation hook (spec 3.2)
3524
3899
  */
3900
+ /**
3901
+ * @license
3902
+ * SPDX-License-Identifier: Apache-2.0
3903
+ *
3904
+ * useCameraAnimation — composable camera animation hook.
3905
+ */
3525
3906
 
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 };
3907
+ export { ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkController, IkGizmo, MujocoCanvas, MujocoPhysics, 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
3908
  //# sourceMappingURL=index.js.map
3528
3909
  //# sourceMappingURL=index.js.map