mujoco-react 10.4.0 → 10.6.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.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ *
5
+ * Live on-screen camera viewports for MuJoCo scenes. Each view renders the
6
+ * shared scene from a named MuJoCo camera/site/body into a `gl.scissor` region
7
+ * tracking a DOM element — no GPU readback, no PNG encoding.
8
+ *
9
+ * While at least one viewport is mounted the canvas switches from R3F's
10
+ * automatic render to a managed render loop (main scene full-frame, then each
11
+ * viewport). This is incompatible with `EffectComposer`/postprocessing or other
12
+ * custom render loops; use the offscreen capture APIs in those setups instead.
13
+ */
14
+
15
+ import {
16
+ createContext,
17
+ type CSSProperties,
18
+ type MutableRefObject,
19
+ type ReactNode,
20
+ type RefObject,
21
+ useCallback,
22
+ useContext,
23
+ useEffect,
24
+ useMemo,
25
+ useRef,
26
+ useState,
27
+ } from 'react';
28
+ import { useFrame, useThree } from '@react-three/fiber';
29
+ import type * as THREE from 'three';
30
+ import { useMujoco } from '../core/MujocoSimProvider';
31
+ import {
32
+ createCaptureCamera,
33
+ prepareCaptureCamera,
34
+ } from '../rendering/cameraFrameCapture';
35
+ import type {
36
+ Bodies,
37
+ Cameras,
38
+ CameraFrameCaptureOptions,
39
+ Sites,
40
+ } from '../types';
41
+
42
+ /** Camera selection + pose options for a live viewport. */
43
+ export type CameraViewportOptions = Pick<
44
+ CameraFrameCaptureOptions,
45
+ | 'camera'
46
+ | 'cameraName'
47
+ | 'siteName'
48
+ | 'bodyName'
49
+ | 'position'
50
+ | 'quaternion'
51
+ | 'lookAt'
52
+ | 'up'
53
+ | 'positionOffset'
54
+ | 'quaternionOffset'
55
+ | 'fov'
56
+ | 'near'
57
+ | 'far'
58
+ | 'projectionMatrix'
59
+ | 'mujocoCameraCompatibility'
60
+ >;
61
+
62
+ interface CameraViewportDescriptor {
63
+ getElement: () => HTMLElement | null;
64
+ getOptions: () => CameraViewportOptions;
65
+ camera: THREE.Camera | null;
66
+ }
67
+
68
+ interface CameraViewportRegistry {
69
+ register: (descriptor: CameraViewportDescriptor) => () => void;
70
+ }
71
+
72
+ const CameraViewportRegistryContext = createContext<CameraViewportRegistry | null>(null);
73
+
74
+ let nextViewportId = 0;
75
+
76
+ const VIEWPORT_RENDER_PRIORITY = 1;
77
+
78
+ function CameraViewportRenderer({
79
+ viewportsRef,
80
+ }: {
81
+ viewportsRef: MutableRefObject<Map<number, CameraViewportDescriptor>>;
82
+ }) {
83
+ const gl = useThree((state) => state.gl);
84
+ const scene = useThree((state) => state.scene);
85
+ const mainCamera = useThree((state) => state.camera);
86
+ const mujoco = useMujoco();
87
+
88
+ useFrame(() => {
89
+ const drawWidth = gl.domElement.width;
90
+ const drawHeight = gl.domElement.height;
91
+
92
+ // We own the render now: draw the main scene full-frame first.
93
+ gl.setScissorTest(false);
94
+ gl.setViewport(0, 0, drawWidth, drawHeight);
95
+ gl.render(scene, mainCamera);
96
+
97
+ const api = mujoco.api;
98
+ if (!api || viewportsRef.current.size === 0) return;
99
+
100
+ const dpr = gl.getPixelRatio();
101
+ const canvasRect = gl.domElement.getBoundingClientRect();
102
+
103
+ for (const descriptor of viewportsRef.current.values()) {
104
+ const element = descriptor.getElement();
105
+ if (!element) continue;
106
+
107
+ const rect = element.getBoundingClientRect();
108
+ const isOffscreen =
109
+ rect.bottom < canvasRect.top ||
110
+ rect.top > canvasRect.bottom ||
111
+ rect.right < canvasRect.left ||
112
+ rect.left > canvasRect.right;
113
+ if (isOffscreen) continue;
114
+
115
+ const width = Math.floor(rect.width * dpr);
116
+ const height = Math.floor(rect.height * dpr);
117
+ if (width <= 0 || height <= 0) continue;
118
+
119
+ const left = Math.floor((rect.left - canvasRect.left) * dpr);
120
+ const bottom = Math.floor((canvasRect.bottom - rect.bottom) * dpr);
121
+
122
+ let resolved: CameraFrameCaptureOptions;
123
+ try {
124
+ resolved = api.resolveCameraCaptureOptions(descriptor.getOptions());
125
+ } catch {
126
+ continue;
127
+ }
128
+
129
+ if (!descriptor.camera) {
130
+ descriptor.camera = createCaptureCamera(resolved, mainCamera, width, height);
131
+ } else {
132
+ prepareCaptureCamera(descriptor.camera, resolved, mainCamera, width, height);
133
+ }
134
+
135
+ gl.setViewport(left, bottom, width, height);
136
+ gl.setScissor(left, bottom, width, height);
137
+ gl.setScissorTest(true);
138
+ gl.render(scene, descriptor.camera);
139
+ }
140
+
141
+ gl.setScissorTest(false);
142
+ gl.setViewport(0, 0, drawWidth, drawHeight);
143
+ }, VIEWPORT_RENDER_PRIORITY);
144
+
145
+ return null;
146
+ }
147
+
148
+ /**
149
+ * Provides the live-viewport registry and mounts the managed render loop only
150
+ * while at least one viewport is active. Mounted internally by the MuJoCo
151
+ * provider; you do not need to add it yourself.
152
+ */
153
+ export function CameraViewportProvider({ children }: { children?: ReactNode }) {
154
+ const viewportsRef = useRef<Map<number, CameraViewportDescriptor>>(new Map());
155
+ const [count, setCount] = useState(0);
156
+
157
+ const register = useCallback((descriptor: CameraViewportDescriptor) => {
158
+ const id = nextViewportId++;
159
+ viewportsRef.current.set(id, descriptor);
160
+ setCount((value) => value + 1);
161
+ return () => {
162
+ viewportsRef.current.delete(id);
163
+ setCount((value) => value - 1);
164
+ };
165
+ }, []);
166
+
167
+ const value = useMemo<CameraViewportRegistry>(() => ({ register }), [register]);
168
+
169
+ return (
170
+ <CameraViewportRegistryContext.Provider value={value}>
171
+ {children}
172
+ {count > 0 && <CameraViewportRenderer viewportsRef={viewportsRef} />}
173
+ </CameraViewportRegistryContext.Provider>
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Render the live MuJoCo scene from a named camera into the region covered by
179
+ * `elementRef`'s DOM element. Call this inside `<MujocoCanvas>` with a ref to a
180
+ * DOM element you position anywhere (it does not need to be in the R3F tree).
181
+ */
182
+ export function useCameraViewport<T extends HTMLElement = HTMLElement>(
183
+ elementRef: RefObject<T | null>,
184
+ options: CameraViewportOptions
185
+ ) {
186
+ const registry = useContext(CameraViewportRegistryContext);
187
+ if (!registry) {
188
+ throw new Error('useCameraViewport must be used inside <MujocoCanvas>.');
189
+ }
190
+
191
+ const optionsRef = useRef(options);
192
+ optionsRef.current = options;
193
+
194
+ useEffect(() => {
195
+ const descriptor: CameraViewportDescriptor = {
196
+ getElement: () => elementRef.current,
197
+ getOptions: () => optionsRef.current,
198
+ camera: null,
199
+ };
200
+ return registry.register(descriptor);
201
+ }, [registry, elementRef]);
202
+ }
203
+
204
+ export interface CameraViewProps extends CameraViewportOptions {
205
+ className?: string;
206
+ style?: CSSProperties;
207
+ }
208
+
209
+ /**
210
+ * Drop-in live camera pane. Renders an absolutely-positioned overlay `<div>`
211
+ * over the canvas showing the selected MuJoCo camera. Position it with
212
+ * `style`/`className` (the canvas's parent should be positioned).
213
+ */
214
+ export function CameraView({ className, style, ...options }: CameraViewProps) {
215
+ const gl = useThree((state) => state.gl);
216
+ const elementRef = useRef<HTMLDivElement | null>(null);
217
+ if (!elementRef.current && typeof document !== 'undefined') {
218
+ elementRef.current = document.createElement('div');
219
+ }
220
+
221
+ useEffect(() => {
222
+ const element = elementRef.current;
223
+ const parent = gl.domElement.parentElement;
224
+ if (!element || !parent) return;
225
+ element.style.position = 'absolute';
226
+ element.style.overflow = 'hidden';
227
+ parent.appendChild(element);
228
+ return () => {
229
+ parent.removeChild(element);
230
+ };
231
+ }, [gl]);
232
+
233
+ useEffect(() => {
234
+ const element = elementRef.current;
235
+ if (!element) return;
236
+ element.className = className ?? '';
237
+ if (style) Object.assign(element.style, style);
238
+ }, [className, style]);
239
+
240
+ useCameraViewport(elementRef, options);
241
+
242
+ return null;
243
+ }
244
+
245
+ export type { Bodies, Cameras, Sites };
@@ -13,9 +13,13 @@ export interface GenericIKOptions {
13
13
  epsilon: number;
14
14
  posWeight: number;
15
15
  rotWeight: number;
16
+ jointLimits?: ReadonlyArray<readonly [number, number] | null | undefined>;
16
17
  }
17
18
 
18
- const DEFAULTS: GenericIKOptions = {
19
+ type ResolvedGenericIKOptions = Required<Omit<GenericIKOptions, 'jointLimits'>> &
20
+ Pick<GenericIKOptions, 'jointLimits'>;
21
+
22
+ const DEFAULTS: Required<Omit<GenericIKOptions, 'jointLimits'>> = {
19
23
  maxIterations: 50,
20
24
  damping: 0.01,
21
25
  tolerance: 1e-3,
@@ -24,7 +28,7 @@ const DEFAULTS: GenericIKOptions = {
24
28
  rotWeight: 0.3,
25
29
  };
26
30
 
27
- function resolveOptions(opts?: Partial<GenericIKOptions>): GenericIKOptions {
31
+ function resolveOptions(opts?: Partial<GenericIKOptions>): ResolvedGenericIKOptions {
28
32
  return {
29
33
  maxIterations: opts?.maxIterations ?? DEFAULTS.maxIterations,
30
34
  damping: opts?.damping ?? DEFAULTS.damping,
@@ -32,6 +36,7 @@ function resolveOptions(opts?: Partial<GenericIKOptions>): GenericIKOptions {
32
36
  epsilon: opts?.epsilon ?? DEFAULTS.epsilon,
33
37
  posWeight: opts?.posWeight ?? DEFAULTS.posWeight,
34
38
  rotWeight: opts?.rotWeight ?? DEFAULTS.rotWeight,
39
+ jointLimits: opts?.jointLimits,
35
40
  };
36
41
  }
37
42
 
@@ -96,6 +101,14 @@ export class GenericIK {
96
101
 
97
102
  let bestQ: number[] | null = null;
98
103
  let bestErr = Infinity;
104
+ // Stop early once the error stops improving. The solver always returns the
105
+ // best configuration it has seen, so when it stalls or diverges (an
106
+ // unreachable target, e.g. a near-singular wrist orientation) further
107
+ // iterations can't beat bestQ — they only burn (1 + n) mj_forward calls
108
+ // each and feed jitter into ctrl. This is what bounds the cost of dragging
109
+ // the target into an unreachable region.
110
+ const patience = 4;
111
+ let noImprove = 0;
99
112
 
100
113
  if (n === 0) return null;
101
114
 
@@ -133,14 +146,20 @@ export class GenericIK {
133
146
  );
134
147
 
135
148
  // Track best solution
136
- if (errNorm < bestErr) {
149
+ if (errNorm < bestErr - 1e-9) {
137
150
  bestErr = errNorm;
138
151
  bestQ = Array.from(q);
152
+ noImprove = 0;
153
+ } else {
154
+ noImprove++;
139
155
  }
140
156
 
141
157
  // Converged
142
158
  if (errNorm < o.tolerance) break;
143
159
 
160
+ // Stalled or diverging — bestQ can no longer improve, so stop.
161
+ if (noImprove >= patience) break;
162
+
144
163
  // Compute Jacobian via finite differences
145
164
  for (let j = 0; j < n; j++) {
146
165
  const adr = qposAdr[j];
@@ -16,6 +16,7 @@ import {
16
16
  import * as THREE from 'three';
17
17
  import { MujocoData, MujocoModel, MujocoModule, getContact, withContacts } from '../types';
18
18
  import { SceneRenderer } from '../components/SceneRenderer';
19
+ import { CameraViewportProvider } from '../components/CameraView';
19
20
  import {
20
21
  ActuatedJointInfo,
21
22
  ActuatorInfo,
@@ -59,6 +60,7 @@ import {
59
60
  captureCameraFrame,
60
61
  captureCameraFrameBlob,
61
62
  createCameraFrameCaptureSession,
63
+ type CameraFrameCaptureTensorOptions,
62
64
  } from '../rendering/cameraFrameCapture';
63
65
  import {
64
66
  getCameraFrameCaptureSourceTarget,
@@ -1558,6 +1560,35 @@ export function MujocoSimProvider({
1558
1560
  [camera, gl, resolveCameraCaptureOptions, scene]
1559
1561
  );
1560
1562
 
1563
+ const createCameraFrameCaptureSessionApi = useCallback(
1564
+ (options: CameraFrameCaptureOptions = {}) =>
1565
+ createCameraFrameCaptureSession(
1566
+ gl,
1567
+ scene,
1568
+ camera,
1569
+ resolveCameraCaptureOptions(options)
1570
+ ),
1571
+ [camera, gl, resolveCameraCaptureOptions, scene]
1572
+ );
1573
+
1574
+ const captureCameraFrameTensorApi = useCallback(
1575
+ (options: CameraFrameCaptureTensorOptions = {}) => {
1576
+ const resolved: CameraFrameCaptureTensorOptions = {
1577
+ ...resolveCameraCaptureOptions(options),
1578
+ channels: options.channels,
1579
+ layout: options.layout,
1580
+ range: options.range,
1581
+ };
1582
+ const session = createCameraFrameCaptureSession(gl, scene, camera, resolved);
1583
+ try {
1584
+ return session.captureTensor(resolved);
1585
+ } finally {
1586
+ session.dispose();
1587
+ }
1588
+ },
1589
+ [camera, gl, resolveCameraCaptureOptions, scene]
1590
+ );
1591
+
1561
1592
  const recordCameraSequenceApi = useCallback(
1562
1593
  async (
1563
1594
  options: CameraFrameSequenceOptions
@@ -1882,6 +1913,9 @@ export function MujocoSimProvider({
1882
1913
  captureFrameBlob: captureFrameBlobApi,
1883
1914
  captureCameraFrame: captureCameraFrameApi,
1884
1915
  captureCameraFrameBlob: captureCameraFrameBlobApi,
1916
+ captureCameraFrameTensor: captureCameraFrameTensorApi,
1917
+ createCameraFrameCaptureSession: createCameraFrameCaptureSessionApi,
1918
+ resolveCameraCaptureOptions,
1885
1919
  recordCameraSequence: recordCameraSequenceApi,
1886
1920
  project2DTo3D,
1887
1921
  projectImagePointTo3D,
@@ -1903,6 +1937,8 @@ export function MujocoSimProvider({
1903
1937
  loadFromFilesApi, addBodyApi, removeBodyApi, recompileApi,
1904
1938
  getCanvas, getCanvasSnapshot, captureFrameApi, captureFrameBlobApi,
1905
1939
  captureCameraFrameApi, captureCameraFrameBlobApi,
1940
+ captureCameraFrameTensorApi, createCameraFrameCaptureSessionApi,
1941
+ resolveCameraCaptureOptions,
1906
1942
  recordCameraSequenceApi,
1907
1943
  project2DTo3D,
1908
1944
  projectImagePointTo3D,
@@ -1940,7 +1976,7 @@ export function MujocoSimProvider({
1940
1976
  return (
1941
1977
  <MujocoSimContext.Provider value={contextValue}>
1942
1978
  <SceneRenderer renderOptions={renderOptions} />
1943
- {children}
1979
+ <CameraViewportProvider>{children}</CameraViewportProvider>
1944
1980
  </MujocoSimContext.Provider>
1945
1981
  );
1946
1982
  }
@@ -496,9 +496,10 @@ function sceneObjectToXml(obj: SceneObject): string {
496
496
  const solref = obj.solref ? ` solref="${obj.solref}"` : '';
497
497
  const solimp = obj.solimp ? ` solimp="${obj.solimp}"` : '';
498
498
  const condim = obj.condim ? ` condim="${obj.condim}"` : '';
499
+ const contype = obj.contype ?? 1;
500
+ const conaffinity = obj.conaffinity ?? 1;
499
501
  const group = obj.group !== undefined ? ` group="${obj.group}"` : '';
500
- // Always set contype/conaffinity=1 so objects collide regardless of model defaults
501
- return `<body name="${obj.name}" pos="${pos}">${joint}<geom name="${geomName}" type="${obj.type}" size="${size}" rgba="${rgba}" contype="1" conaffinity="1"${mass}${friction}${solref}${solimp}${condim}${group}/></body>`;
502
+ return `<body name="${obj.name}" pos="${pos}">${joint}<geom name="${geomName}" type="${obj.type}" size="${size}" rgba="${rgba}" contype="${contype}" conaffinity="${conaffinity}"${mass}${friction}${solref}${solimp}${condim}${group}/></body>`;
502
503
  }
503
504
 
504
505
  /** Create virtual directory structure for a file path. */
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ *
5
+ * Stream a live MuJoCo camera into a DOM `<canvas>`. Each frame the scene is
6
+ * rendered offscreen from the selected camera and blitted into the canvas, so
7
+ * it composites normally in the DOM (works inside opaque panels) and does NOT
8
+ * take over the render loop. Prefer this over `useCameraViewport` for camera
9
+ * tiles embedded in HTML UI; use `useCameraViewport` for transparent overlays
10
+ * on a full-bleed canvas.
11
+ *
12
+ * Uses the async capture path so Gaussian-splat environments render through
13
+ * their dedicated capture renderer — streaming a splat scene at full rate does
14
+ * not disturb the main view's splat sort.
15
+ */
16
+
17
+ import { useEffect, useRef } from 'react';
18
+ import type { RefObject } from 'react';
19
+ import { useFrame } from '@react-three/fiber';
20
+ import { useMujoco } from '../core/MujocoSimProvider';
21
+ import type { CameraFrameCaptureSession } from '../rendering/cameraFrameCapture';
22
+ import type { CameraFrameCaptureOptions } from '../types';
23
+
24
+ export interface CameraStreamOptions extends CameraFrameCaptureOptions {
25
+ /**
26
+ * Optional cap on updates per second. Omit to stream as fast as captures
27
+ * complete (one capture is in flight at a time regardless).
28
+ */
29
+ fps?: number;
30
+ /** Pause updates without unmounting. */
31
+ paused?: boolean;
32
+ }
33
+
34
+ function streamSignature(options: CameraStreamOptions): string {
35
+ return JSON.stringify({
36
+ cameraName: options.cameraName,
37
+ siteName: options.siteName,
38
+ bodyName: options.bodyName,
39
+ width: options.width,
40
+ height: options.height,
41
+ renderIsolation: options.renderIsolation ?? false,
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Render the live scene from a MuJoCo camera/site/body into `canvasRef`'s
47
+ * `<canvas>` every frame (throttled to `fps`). Call inside `<MujocoCanvas>`;
48
+ * the canvas itself can live anywhere in the DOM.
49
+ */
50
+ export function useCameraStream(
51
+ canvasRef: RefObject<HTMLCanvasElement | null>,
52
+ options: CameraStreamOptions
53
+ ) {
54
+ const mujoco = useMujoco();
55
+ const sessionRef = useRef<CameraFrameCaptureSession | null>(null);
56
+ const signatureRef = useRef<string>('');
57
+ const optionsRef = useRef(options);
58
+ optionsRef.current = options;
59
+ const elapsedRef = useRef(0);
60
+ const inFlightRef = useRef(false);
61
+ const mountedRef = useRef(true);
62
+
63
+ useEffect(() => {
64
+ mountedRef.current = true;
65
+ return () => {
66
+ mountedRef.current = false;
67
+ sessionRef.current?.dispose();
68
+ sessionRef.current = null;
69
+ signatureRef.current = '';
70
+ };
71
+ }, []);
72
+
73
+ useFrame((_state, delta) => {
74
+ const api = mujoco.api;
75
+ if (!api || !canvasRef.current) return;
76
+
77
+ const opts = optionsRef.current;
78
+ if (opts.paused) return;
79
+
80
+ if (opts.fps && opts.fps > 0) {
81
+ elapsedRef.current += delta;
82
+ if (elapsedRef.current < 1 / opts.fps) return;
83
+ }
84
+ // One capture in flight at a time — naturally rate-limits to capture speed.
85
+ if (inFlightRef.current) return;
86
+ elapsedRef.current = 0;
87
+
88
+ const signature = streamSignature(opts);
89
+ if (!sessionRef.current || signatureRef.current !== signature) {
90
+ sessionRef.current?.dispose();
91
+ sessionRef.current = api.createCameraFrameCaptureSession(opts);
92
+ signatureRef.current = signature;
93
+ }
94
+ const session = sessionRef.current;
95
+
96
+ inFlightRef.current = true;
97
+ session
98
+ .captureAsync(api.resolveCameraCaptureOptions(opts))
99
+ .then((frame) => {
100
+ const canvas = canvasRef.current;
101
+ if (!mountedRef.current || !canvas) return;
102
+ const ctx = canvas.getContext('2d');
103
+ if (!ctx) return;
104
+ if (canvas.width !== frame.width || canvas.height !== frame.height) {
105
+ canvas.width = frame.width;
106
+ canvas.height = frame.height;
107
+ }
108
+ ctx.drawImage(frame.canvas, 0, 0);
109
+ })
110
+ .catch(() => {})
111
+ .finally(() => {
112
+ inFlightRef.current = false;
113
+ });
114
+ });
115
+ }
Binary file
@@ -10,11 +10,31 @@ import { createControllerHook } from '../core/createController';
10
10
  import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
11
11
  import { GenericIK } from '../core/GenericIK';
12
12
  import { createContiguousControlGroup, findSiteByName, resolveControlGroup } from '../core/SceneLoader';
13
- import type { ControlGroupInfo, IkConfig, IkContextValue, IKSolveFn, IkSolveInput, MujocoData } from '../types';
13
+ import type {
14
+ ControlGroupInfo,
15
+ IkConfig,
16
+ IkContextValue,
17
+ IKSolveFn,
18
+ IkSolveInput,
19
+ IkTargetPosition,
20
+ MujocoData,
21
+ } from '../types';
14
22
 
15
23
  // Preallocated temp for syncGizmoToSite
16
24
  const _syncMat4 = new THREE.Matrix4();
17
25
 
26
+ function toIkVector3(value: IkSolveInput['position']): THREE.Vector3 {
27
+ return 'x' in value
28
+ ? new THREE.Vector3(value.x, value.y, value.z)
29
+ : new THREE.Vector3(value[0], value[1], value[2]);
30
+ }
31
+
32
+ function toIkQuaternion(value: IkSolveInput['quaternion']): THREE.Quaternion {
33
+ return 'x' in value
34
+ ? new THREE.Quaternion(value.x, value.y, value.z, value.w)
35
+ : new THREE.Quaternion(value[0], value[1], value[2], value[3]);
36
+ }
37
+
18
38
  function syncGizmoToSite(data: MujocoData, siteId: number, target: THREE.Group) {
19
39
  if (siteId === -1) return;
20
40
  const sitePos = data.site_xpos.subarray(siteId * 3, siteId * 3 + 3);
@@ -86,17 +106,28 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
86
106
  const ikSolveFn = useCallback(
87
107
  ({ position, quaternion, currentQ, context }: IkSolveInput): number[] | null => {
88
108
  if (!config) return null;
89
- if (config.ikSolveFn) return config.ikSolveFn({ position, quaternion, currentQ, context });
109
+ const targetPosition = toIkVector3(position);
110
+ const targetQuaternion = toIkQuaternion(quaternion);
111
+ const normalizedInput: IkSolveInput = {
112
+ position: targetPosition,
113
+ quaternion: targetQuaternion,
114
+ currentQ,
115
+ context,
116
+ };
117
+ if (config.ikSolveFn) return config.ikSolveFn(normalizedInput);
90
118
  const model = mjModelRef.current;
91
119
  const data = mjDataRef.current;
92
120
  const controlGroup = controlGroupRef.current;
93
121
  if (!model || !data || !controlGroup || siteIdRef.current === -1) return null;
94
122
  return genericIkRef.current.solve(
95
123
  model, data, siteIdRef.current, controlGroup.qposAdr,
96
- position, quaternion, currentQ,
124
+ targetPosition, targetQuaternion, currentQ,
97
125
  {
98
126
  damping: config.damping,
99
127
  epsilon: config.epsilon,
128
+ jointLimits: config.jointLimits ?? controlGroup.joints.map((joint) => (
129
+ joint.limited ? joint.range : joint.ctrlRange
130
+ )),
100
131
  maxIterations: config.maxIterations,
101
132
  posWeight: config.posWeight,
102
133
  rotWeight: config.rotWeight,
@@ -215,12 +246,12 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
215
246
  );
216
247
 
217
248
  const moveTarget = useCallback(
218
- (pos: THREE.Vector3, duration = 0) => {
249
+ (pos: IkTargetPosition, duration = 0) => {
219
250
  if (!ikEnabledRef.current) setIkEnabled(true);
220
251
  const target = ikTargetRef.current;
221
252
  if (!target) return;
222
253
 
223
- const targetPos = pos.clone();
254
+ const targetPos = toIkVector3(pos);
224
255
  const targetRot = new THREE.Quaternion().setFromEuler(
225
256
  new THREE.Euler(Math.PI, 0, 0),
226
257
  );