mujoco-react 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,15 +12,16 @@ import { useRef } from 'react';
12
12
  import { useFrame } from '@react-three/fiber';
13
13
  import * as THREE from 'three';
14
14
  import { useMujocoSim } from '../core/MujocoSimProvider';
15
+ import { getContact } from '../types';
15
16
 
16
17
  const _dummy = new THREE.Object3D();
17
18
 
18
19
  interface ContactMarkersProps {
19
20
  /** Maximum contacts to render. Default: 100. */
20
21
  maxContacts?: number;
21
- /** Sphere radius. Default: 0.005. */
22
+ /** Sphere radius. Default: 0.008. */
22
23
  radius?: number;
23
- /** Color. Default: '#4f46e5'. */
24
+ /** Color. Default: '#22d3ee'. */
24
25
  color?: string;
25
26
  /** Show markers. Default: true. */
26
27
  visible?: boolean;
@@ -28,8 +29,8 @@ interface ContactMarkersProps {
28
29
 
29
30
  export function ContactMarkers({
30
31
  maxContacts = 100,
31
- radius = 0.005,
32
- color = '#4f46e5',
32
+ radius = 0.008,
33
+ color = '#22d3ee',
33
34
  visible = true,
34
35
  }: ContactMarkersProps = {}) {
35
36
  const { mjDataRef, status } = useMujocoSim();
@@ -47,18 +48,15 @@ export function ContactMarkers({
47
48
  const count = Math.min(ncon, maxContacts);
48
49
 
49
50
  for (let i = 0; i < count; i++) {
50
- try {
51
- const c = (data.contact as { get(i: number): { pos: Float64Array } | undefined }).get(i);
52
- if (!c) break;
53
- _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
54
- _dummy.updateMatrix();
55
- mesh.setMatrixAt(i, _dummy.matrix);
56
- } catch {
57
- // Contact access failed — stop here
51
+ const c = getContact(data, i);
52
+ if (!c) {
58
53
  mesh.count = i;
59
54
  mesh.instanceMatrix.needsUpdate = true;
60
55
  return;
61
56
  }
57
+ _dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
58
+ _dummy.updateMatrix();
59
+ mesh.setMatrixAt(i, _dummy.matrix);
62
60
  }
63
61
 
64
62
  mesh.count = count;
@@ -68,14 +66,9 @@ export function ContactMarkers({
68
66
  if (status !== 'ready') return null;
69
67
 
70
68
  return (
71
- <instancedMesh ref={meshRef} args={[undefined, undefined, maxContacts]}>
69
+ <instancedMesh ref={meshRef} args={[undefined, undefined, maxContacts]} frustumCulled={false} renderOrder={999}>
72
70
  <sphereGeometry args={[radius, 8, 8]} />
73
- <meshStandardMaterial
74
- color={color}
75
- emissive={color}
76
- emissiveIntensity={0.3}
77
- roughness={0.5}
78
- />
71
+ <meshBasicMaterial color={color} depthTest={false} />
79
72
  </instancedMesh>
80
73
  );
81
74
  }
@@ -10,6 +10,7 @@ import { useFrame, useThree } from '@react-three/fiber';
10
10
  import * as THREE from 'three';
11
11
  import { useMujocoSim } from '../core/MujocoSimProvider';
12
12
  import { getName } from '../core/SceneLoader';
13
+ import { getContact } from '../types';
13
14
  import type { DebugProps } from '../types';
14
15
 
15
16
  const JOINT_COLORS: Record<number, number> = {
@@ -19,6 +20,14 @@ const JOINT_COLORS: Record<number, number> = {
19
20
  3: 0xffff00, // hinge - yellow
20
21
  };
21
22
 
23
+ // Preallocated temps to avoid per-frame GC pressure
24
+ const _v3a = new THREE.Vector3();
25
+ const _v3b = new THREE.Vector3();
26
+ const _quat = new THREE.Quaternion();
27
+ const _contactPos = new THREE.Vector3();
28
+ const _contactNormal = new THREE.Vector3();
29
+ const MAX_CONTACT_ARROWS = 50;
30
+
22
31
  /**
23
32
  * Declarative debug visualization component.
24
33
  * Renders wireframe geoms, site markers, joint axes, contact forces, COM markers, etc.
@@ -78,27 +87,92 @@ export function Debug({
78
87
  }
79
88
  }
80
89
 
81
- // Site markers
90
+ // Site markers — scale based on site_size if available, else use geom_size of parent body
82
91
  if (showSites) {
92
+ const siteSize = (model as Record<string, unknown>).site_size as Float64Array | undefined;
83
93
  for (let i = 0; i < model.nsite; i++) {
84
- const geometry = new THREE.OctahedronGeometry(0.01);
85
- const mat = new THREE.MeshBasicMaterial({ color: 0xff00ff, transparent: true, opacity: 0.7 });
94
+ // Determine marker radius: use site_size[3*i] if available, else estimate from parent body's geoms
95
+ let radius = 0.008;
96
+ if (siteSize) {
97
+ radius = Math.max(siteSize[3 * i] * 0.5, 0.004);
98
+ } else {
99
+ // Estimate from parent body's geom sizes
100
+ const bodyId = model.site_bodyid[i];
101
+ let maxGeomSize = 0;
102
+ for (let g = 0; g < model.ngeom; g++) {
103
+ if (model.geom_bodyid[g] === bodyId) {
104
+ maxGeomSize = Math.max(maxGeomSize, model.geom_size[3 * g]);
105
+ }
106
+ }
107
+ if (maxGeomSize > 0) radius = maxGeomSize * 0.15;
108
+ }
109
+
110
+ const geometry = new THREE.OctahedronGeometry(radius);
111
+ const mat = new THREE.MeshBasicMaterial({ color: 0xff00ff, depthTest: false });
86
112
  const mesh = new THREE.Mesh(geometry, mat);
113
+ mesh.renderOrder = 999;
114
+ mesh.frustumCulled = false;
87
115
  mesh.userData.siteId = i;
116
+
117
+ // Label
118
+ const canvas = document.createElement('canvas');
119
+ canvas.width = 256;
120
+ canvas.height = 64;
121
+ const ctx = canvas.getContext('2d')!;
122
+ ctx.fillStyle = '#ff00ff';
123
+ ctx.font = 'bold 36px monospace';
124
+ ctx.textAlign = 'center';
125
+ ctx.fillText(getName(model, model.name_siteadr[i]), 128, 42);
126
+ const tex = new THREE.CanvasTexture(canvas);
127
+ const spriteMat = new THREE.SpriteMaterial({ map: tex, depthTest: false, transparent: true });
128
+ const sprite = new THREE.Sprite(spriteMat);
129
+ const labelScale = radius * 15;
130
+ sprite.scale.set(labelScale, labelScale * 0.25, 1);
131
+ sprite.position.y = radius * 2;
132
+ sprite.renderOrder = 999;
133
+ mesh.add(sprite);
134
+
88
135
  sites.push(mesh);
89
136
  }
90
137
  }
91
138
 
92
- // Joint axes
139
+ // Joint axes — scale arrow length based on parent body's geom sizes
93
140
  if (showJoints) {
141
+ // Safely check for jnt_pos/jnt_axis on the WASM model
142
+ const jntPos = (model as Record<string, unknown>).jnt_pos as Float64Array | undefined;
143
+ const jntAxis = (model as Record<string, unknown>).jnt_axis as Float64Array | undefined;
144
+
94
145
  for (let i = 0; i < model.njnt; i++) {
95
146
  const type = model.jnt_type[i];
96
147
  const color = JOINT_COLORS[type] ?? 0xffffff;
148
+
149
+ // Scale based on parent body geom size
150
+ const bodyId = model.jnt_bodyid[i];
151
+ let maxGeomSize = 0;
152
+ for (let g = 0; g < model.ngeom; g++) {
153
+ if (model.geom_bodyid[g] === bodyId) {
154
+ maxGeomSize = Math.max(maxGeomSize, model.geom_size[3 * g]);
155
+ }
156
+ }
157
+ const arrowLen = Math.max(maxGeomSize * 0.8, 0.05);
158
+
97
159
  const arrow = new THREE.ArrowHelper(
98
160
  new THREE.Vector3(0, 0, 1), new THREE.Vector3(),
99
- 0.05, color, 0.01, 0.005
161
+ arrowLen, color, arrowLen * 0.25, arrowLen * 0.12
100
162
  );
163
+ // Render on top so arrows show through geometry
164
+ arrow.renderOrder = 999;
165
+ arrow.frustumCulled = false;
166
+ arrow.line.material = new THREE.LineBasicMaterial({ color, depthTest: false });
167
+ (arrow.cone.material as THREE.MeshBasicMaterial).depthTest = false;
168
+ arrow.line.renderOrder = 999;
169
+ arrow.line.frustumCulled = false;
170
+ arrow.cone.renderOrder = 999;
171
+ arrow.cone.frustumCulled = false;
101
172
  arrow.userData.jointId = i;
173
+ arrow.userData.bodyId = bodyId;
174
+ arrow.userData.hasJntPos = !!jntPos;
175
+ arrow.userData.hasJntAxis = !!jntAxis;
102
176
  joints.push(arrow);
103
177
  }
104
178
  }
@@ -144,6 +218,10 @@ export function Debug({
144
218
  const data = mjDataRef.current;
145
219
  if (!model || !data || !debugGeometry) return;
146
220
 
221
+ // Safely grab optional arrays once
222
+ const jntPos = (model as Record<string, unknown>).jnt_pos as Float64Array | undefined;
223
+ const jntAxis = (model as Record<string, unknown>).jnt_axis as Float64Array | undefined;
224
+
147
225
  // Update geom wireframes
148
226
  for (const mesh of debugGeometry.geoms) {
149
227
  const bid = mesh.userData.bodyId;
@@ -157,8 +235,9 @@ export function Debug({
157
235
  // Apply local geom offset
158
236
  const gid = mesh.userData.geomId;
159
237
  const gp = model.geom_pos;
160
- mesh.position.add(new THREE.Vector3(gp[3 * gid], gp[3 * gid + 1], gp[3 * gid + 2])
161
- .applyQuaternion(mesh.quaternion));
238
+ _v3a.set(gp[3 * gid], gp[3 * gid + 1], gp[3 * gid + 2])
239
+ .applyQuaternion(mesh.quaternion);
240
+ mesh.position.add(_v3a);
162
241
  }
163
242
 
164
243
  // Update site markers
@@ -171,6 +250,35 @@ export function Debug({
171
250
  );
172
251
  }
173
252
 
253
+ // Update joint axes
254
+ for (const obj of debugGeometry.joints) {
255
+ const arrow = obj as THREE.ArrowHelper;
256
+ const jid = arrow.userData.jointId;
257
+ const bid = arrow.userData.bodyId;
258
+ const i3 = bid * 3;
259
+ const i4 = bid * 4;
260
+
261
+ _quat.set(
262
+ data.xquat[i4 + 1], data.xquat[i4 + 2],
263
+ data.xquat[i4 + 3], data.xquat[i4]
264
+ );
265
+
266
+ // Position: body origin + local joint anchor (if available)
267
+ arrow.position.set(data.xpos[i3], data.xpos[i3 + 1], data.xpos[i3 + 2]);
268
+ if (jntPos) {
269
+ _v3a.set(jntPos[3 * jid], jntPos[3 * jid + 1], jntPos[3 * jid + 2])
270
+ .applyQuaternion(_quat);
271
+ arrow.position.add(_v3a);
272
+ }
273
+
274
+ // Orient along joint axis in world frame (if available)
275
+ if (jntAxis) {
276
+ _v3a.set(jntAxis[3 * jid], jntAxis[3 * jid + 1], jntAxis[3 * jid + 2])
277
+ .applyQuaternion(_quat).normalize();
278
+ arrow.setDirection(_v3a);
279
+ }
280
+ }
281
+
174
282
  // Update COM markers
175
283
  for (const mesh of debugGeometry.comMarkers) {
176
284
  const bid = mesh.userData.bodyId;
@@ -179,41 +287,68 @@ export function Debug({
179
287
  }
180
288
  });
181
289
 
182
- // Contact force vectors
290
+ // Contact force vectors — pre-created pool to avoid per-frame allocation
183
291
  const contactGroupRef = useRef<THREE.Group>(null);
184
- const contactArrowsRef = useRef<THREE.ArrowHelper[]>([]);
292
+ const contactPoolRef = useRef<THREE.ArrowHelper[]>([]);
293
+ const contactPoolInitRef = useRef(false);
185
294
 
186
- useFrame(() => {
187
- if (!showContacts) return;
188
- const model = mjModelRef.current;
189
- const data = mjDataRef.current;
295
+ // Initialize arrow pool once
296
+ useEffect(() => {
190
297
  const group = contactGroupRef.current;
191
- if (!model || !data || !group) return;
298
+ if (!group || contactPoolInitRef.current) return;
299
+ contactPoolInitRef.current = true;
192
300
 
193
- // Remove old arrows
194
- for (const arrow of contactArrowsRef.current) {
195
- group.remove(arrow);
196
- arrow.dispose();
301
+ const pool: THREE.ArrowHelper[] = [];
302
+ for (let i = 0; i < MAX_CONTACT_ARROWS; i++) {
303
+ const arrow = new THREE.ArrowHelper(
304
+ new THREE.Vector3(0, 1, 0), new THREE.Vector3(), 0.1, 0xff4444, 0.03, 0.015
305
+ );
306
+ arrow.visible = false;
307
+ group.add(arrow);
308
+ pool.push(arrow);
197
309
  }
198
- contactArrowsRef.current = [];
310
+ contactPoolRef.current = pool;
311
+
312
+ return () => {
313
+ for (const arrow of pool) {
314
+ group.remove(arrow);
315
+ arrow.dispose();
316
+ }
317
+ contactPoolRef.current = [];
318
+ contactPoolInitRef.current = false;
319
+ };
320
+ }, [showContacts]);
321
+
322
+ useFrame(() => {
323
+ if (!showContacts) return;
324
+ const data = mjDataRef.current;
325
+ const pool = contactPoolRef.current;
326
+ if (!data || pool.length === 0) return;
199
327
 
200
328
  const ncon = data.ncon;
201
- for (let i = 0; i < Math.min(ncon, 50); i++) {
202
- try {
203
- const c = (data.contact as { get(i: number): { pos: Float64Array; frame: Float64Array; dist: number } }).get(i);
204
- const pos = new THREE.Vector3(c.pos[0], c.pos[1], c.pos[2]);
205
- const normal = new THREE.Vector3(c.frame[0], c.frame[1], c.frame[2]);
206
- const force = Math.abs(c.dist) * 100;
207
- const length = Math.min(force * 0.01, 0.1);
208
- if (length > 0.001) {
209
- const arrow = new THREE.ArrowHelper(normal, pos, length, 0xff4444, length * 0.3, length * 0.15);
210
- group.add(arrow);
211
- contactArrowsRef.current.push(arrow);
212
- }
213
- } catch {
214
- break;
329
+ let arrowIdx = 0;
330
+
331
+ for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
332
+ const c = getContact(data, i);
333
+ if (!c) break;
334
+ _contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
335
+ _contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
336
+ const force = Math.abs(c.dist) * 100;
337
+ const length = Math.min(force * 0.01, 0.1);
338
+ if (length > 0.001 && arrowIdx < pool.length) {
339
+ const arrow = pool[arrowIdx];
340
+ arrow.position.copy(_contactPos);
341
+ arrow.setDirection(_contactNormal);
342
+ arrow.setLength(length, length * 0.3, length * 0.15);
343
+ arrow.visible = true;
344
+ arrowIdx++;
215
345
  }
216
346
  }
347
+
348
+ // Hide unused arrows
349
+ for (let i = arrowIdx; i < pool.length; i++) {
350
+ pool[i].visible = false;
351
+ }
217
352
  });
218
353
 
219
354
  if (status !== 'ready') return null;
@@ -208,7 +208,7 @@ export function DragInteraction({
208
208
 
209
209
  if (draggingRef.current && bodyIdRef.current > 0) {
210
210
  arrow.visible = true;
211
- const dir = mouseWorldRef.current.clone().sub(grabWorldRef.current);
211
+ const dir = _bodyPos.copy(mouseWorldRef.current).sub(grabWorldRef.current);
212
212
  const len = dir.length();
213
213
  if (len > 0.001) {
214
214
  dir.normalize();
@@ -0,0 +1,262 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ *
5
+ * IkController — composable IK controller plugin.
6
+ * Extracts all IK logic from MujocoSimProvider into an opt-in component.
7
+ */
8
+
9
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
10
+ import { useFrame } from '@react-three/fiber';
11
+ import * as THREE from 'three';
12
+ import { createController } from '../core/createController';
13
+ import { IkContext, type IkContextValue } from '../core/IkContext';
14
+ import { useMujocoSim, useBeforePhysicsStep } from '../core/MujocoSimProvider';
15
+ import { GenericIK } from '../core/GenericIK';
16
+ import { findSiteByName } from '../core/SceneLoader';
17
+ import type { IkConfig, IKSolveFn, MujocoData } from '../types';
18
+
19
+ // Preallocated temp for syncGizmoToSite
20
+ const _syncMat4 = new THREE.Matrix4();
21
+
22
+ function syncGizmoToSite(data: MujocoData, siteId: number, target: THREE.Group) {
23
+ if (siteId === -1) return;
24
+ const sitePos = data.site_xpos.subarray(siteId * 3, siteId * 3 + 3);
25
+ const siteMat = data.site_xmat.subarray(siteId * 9, siteId * 9 + 9);
26
+ target.position.set(sitePos[0], sitePos[1], sitePos[2]);
27
+ _syncMat4.set(
28
+ siteMat[0], siteMat[1], siteMat[2], 0,
29
+ siteMat[3], siteMat[4], siteMat[5], 0,
30
+ siteMat[6], siteMat[7], siteMat[8], 0,
31
+ 0, 0, 0, 1,
32
+ );
33
+ target.quaternion.setFromRotationMatrix(_syncMat4);
34
+ }
35
+
36
+ function IkControllerImpl({
37
+ config,
38
+ children,
39
+ }: {
40
+ config: IkConfig;
41
+ children?: React.ReactNode;
42
+ }) {
43
+ const { mjModelRef, mjDataRef, mujocoRef, configRef, resetCallbacks, status } =
44
+ useMujocoSim();
45
+
46
+ // All IK state lives here, NOT in the provider
47
+ const ikEnabledRef = useRef(false);
48
+ const ikCalculatingRef = useRef(false);
49
+ const ikTargetRef = useRef<THREE.Group>(new THREE.Group());
50
+ const siteIdRef = useRef(-1);
51
+ const genericIkRef = useRef<GenericIK>(new GenericIK(mujocoRef.current));
52
+ const firstIkEnableRef = useRef(true);
53
+
54
+ const needsInitialSync = useRef(true);
55
+
56
+ const gizmoAnimRef = useRef({
57
+ active: false,
58
+ startPos: new THREE.Vector3(),
59
+ endPos: new THREE.Vector3(),
60
+ startRot: new THREE.Quaternion(),
61
+ endRot: new THREE.Quaternion(),
62
+ startTime: 0,
63
+ duration: 1000,
64
+ });
65
+
66
+ // Resolve site ID when model loads or config changes
67
+ useEffect(() => {
68
+ const model = mjModelRef.current;
69
+ if (!model || status !== 'ready') {
70
+ siteIdRef.current = -1;
71
+ return;
72
+ }
73
+ siteIdRef.current = findSiteByName(model, config.siteName);
74
+ const data = mjDataRef.current;
75
+ if (data && ikTargetRef.current) {
76
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
77
+ }
78
+ }, [config.siteName, status, mjModelRef, mjDataRef]);
79
+
80
+ // IK solve function — use custom solver if provided, otherwise built-in GenericIK
81
+ const ikSolveFn = useCallback(
82
+ (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]): number[] | null => {
83
+ if (config.ikSolveFn) return config.ikSolveFn(pos, quat, currentQ);
84
+ const model = mjModelRef.current;
85
+ const data = mjDataRef.current;
86
+ if (!model || !data || siteIdRef.current === -1) return null;
87
+ return genericIkRef.current.solve(
88
+ model,
89
+ data,
90
+ siteIdRef.current,
91
+ config.numJoints,
92
+ pos,
93
+ quat,
94
+ currentQ,
95
+ {
96
+ damping: config.damping,
97
+ maxIterations: config.maxIterations,
98
+ },
99
+ );
100
+ },
101
+ [config.ikSolveFn, config.numJoints, config.damping, config.maxIterations, mjModelRef, mjDataRef],
102
+ );
103
+ const ikSolveFnRef = useRef<IKSolveFn>(ikSolveFn);
104
+ ikSolveFnRef.current = ikSolveFn;
105
+
106
+ // Gizmo animation + one-time initial sync in useFrame
107
+ useFrame(() => {
108
+ // Ensure the gizmo is positioned at the site after the first physics step
109
+ if (needsInitialSync.current && siteIdRef.current !== -1) {
110
+ const data = mjDataRef.current;
111
+ if (data && ikTargetRef.current) {
112
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
113
+ needsInitialSync.current = false;
114
+ }
115
+ }
116
+
117
+ const ga = gizmoAnimRef.current;
118
+ const target = ikTargetRef.current;
119
+ if (!ga.active || !target) return;
120
+
121
+ const now = performance.now();
122
+ const elapsed = now - ga.startTime;
123
+ const t = Math.min(elapsed / ga.duration, 1.0);
124
+ const ease = 1 - Math.pow(1 - t, 3);
125
+ target.position.lerpVectors(ga.startPos, ga.endPos, ease);
126
+ target.quaternion.slerpQuaternions(ga.startRot, ga.endRot, ease);
127
+ if (t >= 1.0) ga.active = false;
128
+ });
129
+
130
+ // IK solve in physics loop
131
+ useBeforePhysicsStep((model, data) => {
132
+ if (!ikEnabledRef.current) {
133
+ ikCalculatingRef.current = false;
134
+ return;
135
+ }
136
+ const target = ikTargetRef.current;
137
+ if (!target) return;
138
+
139
+ ikCalculatingRef.current = true;
140
+ const numJoints = config.numJoints;
141
+ const currentQ: number[] = [];
142
+ for (let i = 0; i < numJoints; i++) currentQ.push(data.qpos[i]);
143
+ const solution = ikSolveFnRef.current(target.position, target.quaternion, currentQ);
144
+ if (solution) {
145
+ for (let i = 0; i < numJoints; i++) data.ctrl[i] = solution[i];
146
+ }
147
+ });
148
+
149
+ // Reset callback — sync gizmo and reset IK state
150
+ useEffect(() => {
151
+ const cb = () => {
152
+ const data = mjDataRef.current;
153
+ if (data && ikTargetRef.current) {
154
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
155
+ }
156
+ gizmoAnimRef.current.active = false;
157
+ firstIkEnableRef.current = true;
158
+ ikEnabledRef.current = false;
159
+ needsInitialSync.current = true;
160
+ };
161
+ resetCallbacks.current.add(cb);
162
+ return () => {
163
+ resetCallbacks.current.delete(cb);
164
+ };
165
+ }, [resetCallbacks, mjDataRef]);
166
+
167
+ // --- API methods ---
168
+
169
+ const setIkEnabled = useCallback(
170
+ (enabled: boolean) => {
171
+ ikEnabledRef.current = enabled;
172
+ const data = mjDataRef.current;
173
+ if (enabled && data && !gizmoAnimRef.current.active && ikTargetRef.current) {
174
+ syncGizmoToSite(data, siteIdRef.current, ikTargetRef.current);
175
+ firstIkEnableRef.current = false;
176
+ }
177
+ },
178
+ [mjDataRef],
179
+ );
180
+
181
+ const syncTargetToSiteApi = useCallback(() => {
182
+ const data = mjDataRef.current;
183
+ const target = ikTargetRef.current;
184
+ if (data && target) syncGizmoToSite(data, siteIdRef.current, target);
185
+ }, [mjDataRef]);
186
+
187
+ const solveIK = useCallback(
188
+ (pos: THREE.Vector3, quat: THREE.Quaternion, currentQ: number[]): number[] | null => {
189
+ return ikSolveFnRef.current(pos, quat, currentQ);
190
+ },
191
+ [],
192
+ );
193
+
194
+ const moveTarget = useCallback(
195
+ (pos: THREE.Vector3, duration = 0) => {
196
+ if (!ikEnabledRef.current) setIkEnabled(true);
197
+ const target = ikTargetRef.current;
198
+ if (!target) return;
199
+
200
+ const targetPos = pos.clone();
201
+ const targetRot = new THREE.Quaternion().setFromEuler(
202
+ new THREE.Euler(Math.PI, 0, 0),
203
+ );
204
+
205
+ if (duration > 0) {
206
+ const ga = gizmoAnimRef.current;
207
+ ga.active = true;
208
+ ga.startPos.copy(target.position);
209
+ ga.endPos.copy(targetPos);
210
+ ga.startRot.copy(target.quaternion);
211
+ ga.endRot.copy(targetRot);
212
+ ga.startTime = performance.now();
213
+ ga.duration = duration;
214
+ } else {
215
+ gizmoAnimRef.current.active = false;
216
+ target.position.copy(targetPos);
217
+ target.quaternion.copy(targetRot);
218
+ }
219
+ },
220
+ [setIkEnabled],
221
+ );
222
+
223
+ const getGizmoStats = useCallback(
224
+ (): { pos: THREE.Vector3; rot: THREE.Euler } | null => {
225
+ const target = ikTargetRef.current;
226
+ if (!ikCalculatingRef.current || !target) return null;
227
+ return {
228
+ pos: target.position.clone(),
229
+ rot: new THREE.Euler().setFromQuaternion(target.quaternion),
230
+ };
231
+ },
232
+ [],
233
+ );
234
+
235
+ const contextValue = useMemo<IkContextValue>(
236
+ () => ({
237
+ ikEnabledRef,
238
+ ikCalculatingRef,
239
+ ikTargetRef,
240
+ siteIdRef,
241
+ setIkEnabled,
242
+ moveTarget,
243
+ syncTargetToSite: syncTargetToSiteApi,
244
+ solveIK,
245
+ getGizmoStats,
246
+ }),
247
+ [setIkEnabled, moveTarget, syncTargetToSiteApi, solveIK, getGizmoStats],
248
+ );
249
+
250
+ return <IkContext.Provider value={contextValue}>{children}</IkContext.Provider>;
251
+ }
252
+
253
+ export const IkController = createController<IkConfig>(
254
+ {
255
+ name: 'IkController',
256
+ defaultConfig: {
257
+ damping: 0.01,
258
+ maxIterations: 50,
259
+ },
260
+ },
261
+ IkControllerImpl,
262
+ );