mujoco-react 9.4.0 → 9.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.
- package/README.md +22 -6
- package/dist/{chunk-VDSEPZYQ.js → chunk-4JHALVB2.js} +397 -4
- package/dist/chunk-4JHALVB2.js.map +1 -0
- package/dist/index.d.ts +26 -4
- package/dist/index.js +497 -431
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +27 -3
- package/dist/spark.js +156 -3
- package/dist/spark.js.map +1 -1
- package/dist/{types-BuJ4boaq.d.ts → types-C1rwH74Y.d.ts} +37 -1
- package/package.json +1 -1
- package/src/components/ContactMarkers.tsx +8 -1
- package/src/components/Debug.tsx +154 -3
- package/src/components/DragInteraction.tsx +2 -0
- package/src/components/IkGizmo.tsx +5 -1
- package/src/core/MujocoSimProvider.tsx +5 -5
- package/src/hooks/useIkController.ts +8 -1
- package/src/hooks/useKeyboardIkTarget.ts +170 -0
- package/src/index.ts +5 -0
- package/src/rendering/cameraFrameCapture.ts +259 -28
- package/src/rendering/cameraFrameSource.ts +10 -2
- package/src/spark.tsx +241 -1
- package/src/types.ts +51 -0
- package/dist/chunk-VDSEPZYQ.js.map +0 -1
package/src/components/Debug.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import type { ThreeElements } from '@react-three/fiber';
|
|
|
11
11
|
import * as THREE from 'three';
|
|
12
12
|
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
13
13
|
import { getName } from '../core/SceneLoader';
|
|
14
|
+
import { CAPTURE_EXCLUDE_KEY } from '../rendering/cameraFrameCapture';
|
|
14
15
|
import { getContact, withContacts } from '../types';
|
|
15
16
|
import type { DebugProps } from '../types';
|
|
16
17
|
|
|
@@ -25,9 +26,20 @@ const JOINT_COLORS: Record<number, number> = {
|
|
|
25
26
|
const _v3a = new THREE.Vector3();
|
|
26
27
|
const _v3b = new THREE.Vector3();
|
|
27
28
|
const _quat = new THREE.Quaternion();
|
|
29
|
+
const _cameraMatrix = new THREE.Matrix4();
|
|
28
30
|
const _contactPos = new THREE.Vector3();
|
|
29
31
|
const _contactNormal = new THREE.Vector3();
|
|
30
32
|
const MAX_CONTACT_ARROWS = 50;
|
|
33
|
+
const CAMERA_DEBUG_LENGTH = 0.12;
|
|
34
|
+
const CAMERA_DEBUG_FRUSTUM_DEPTH = 0.08;
|
|
35
|
+
|
|
36
|
+
type CameraDebugObject = THREE.Group & {
|
|
37
|
+
userData: {
|
|
38
|
+
cameraId: number;
|
|
39
|
+
frustum: THREE.LineSegments;
|
|
40
|
+
label?: THREE.Sprite;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
45
|
* Declarative debug visualization component.
|
|
@@ -37,6 +49,7 @@ export function Debug({
|
|
|
37
49
|
showGeoms = false,
|
|
38
50
|
showSites = false,
|
|
39
51
|
showJoints = false,
|
|
52
|
+
showCameras = false,
|
|
40
53
|
showContacts = false,
|
|
41
54
|
showCOM = false,
|
|
42
55
|
showInertia = false,
|
|
@@ -55,6 +68,7 @@ export function Debug({
|
|
|
55
68
|
const geoms: THREE.Object3D[] = [];
|
|
56
69
|
const sites: THREE.Object3D[] = [];
|
|
57
70
|
const joints: THREE.Object3D[] = [];
|
|
71
|
+
const cameras: CameraDebugObject[] = [];
|
|
58
72
|
const comMarkers: THREE.Object3D[] = [];
|
|
59
73
|
|
|
60
74
|
// Wireframe geoms
|
|
@@ -179,6 +193,83 @@ export function Debug({
|
|
|
179
193
|
}
|
|
180
194
|
}
|
|
181
195
|
|
|
196
|
+
if (showCameras && model.ncam && model.name_camadr) {
|
|
197
|
+
for (let i = 0; i < model.ncam; i++) {
|
|
198
|
+
const group = new THREE.Group() as CameraDebugObject;
|
|
199
|
+
group.userData.cameraId = i;
|
|
200
|
+
group.renderOrder = 999;
|
|
201
|
+
group.frustumCulled = false;
|
|
202
|
+
|
|
203
|
+
const marker = new THREE.Mesh(
|
|
204
|
+
new THREE.BoxGeometry(0.014, 0.009, 0.006),
|
|
205
|
+
new THREE.MeshBasicMaterial({ color: 0x38bdf8, depthTest: false })
|
|
206
|
+
);
|
|
207
|
+
marker.renderOrder = 999;
|
|
208
|
+
marker.frustumCulled = false;
|
|
209
|
+
group.add(marker);
|
|
210
|
+
|
|
211
|
+
const forward = new THREE.ArrowHelper(
|
|
212
|
+
new THREE.Vector3(0, 0, -1),
|
|
213
|
+
new THREE.Vector3(),
|
|
214
|
+
CAMERA_DEBUG_LENGTH,
|
|
215
|
+
0x38bdf8,
|
|
216
|
+
CAMERA_DEBUG_LENGTH * 0.24,
|
|
217
|
+
CAMERA_DEBUG_LENGTH * 0.11
|
|
218
|
+
);
|
|
219
|
+
forward.renderOrder = 999;
|
|
220
|
+
forward.frustumCulled = false;
|
|
221
|
+
forward.line.material = new THREE.LineBasicMaterial({
|
|
222
|
+
color: 0x38bdf8,
|
|
223
|
+
depthTest: false,
|
|
224
|
+
});
|
|
225
|
+
(forward.cone.material as THREE.MeshBasicMaterial).depthTest = false;
|
|
226
|
+
group.add(forward);
|
|
227
|
+
|
|
228
|
+
const frustumGeometry = new THREE.BufferGeometry();
|
|
229
|
+
frustumGeometry.setAttribute(
|
|
230
|
+
'position',
|
|
231
|
+
new THREE.Float32BufferAttribute(new Float32Array(8 * 2 * 3), 3)
|
|
232
|
+
);
|
|
233
|
+
const frustum = new THREE.LineSegments(
|
|
234
|
+
frustumGeometry,
|
|
235
|
+
new THREE.LineBasicMaterial({
|
|
236
|
+
color: 0x38bdf8,
|
|
237
|
+
transparent: true,
|
|
238
|
+
opacity: 0.8,
|
|
239
|
+
depthTest: false,
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
frustum.renderOrder = 999;
|
|
243
|
+
frustum.frustumCulled = false;
|
|
244
|
+
group.userData.frustum = frustum;
|
|
245
|
+
group.add(frustum);
|
|
246
|
+
|
|
247
|
+
const canvas = document.createElement('canvas');
|
|
248
|
+
canvas.width = 256;
|
|
249
|
+
canvas.height = 64;
|
|
250
|
+
const ctx = canvas.getContext('2d')!;
|
|
251
|
+
ctx.fillStyle = '#38bdf8';
|
|
252
|
+
ctx.font = 'bold 32px monospace';
|
|
253
|
+
ctx.textAlign = 'center';
|
|
254
|
+
ctx.fillText(getName(model, model.name_camadr[i]), 128, 42);
|
|
255
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
256
|
+
const sprite = new THREE.Sprite(
|
|
257
|
+
new THREE.SpriteMaterial({
|
|
258
|
+
map: texture,
|
|
259
|
+
depthTest: false,
|
|
260
|
+
transparent: true,
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
sprite.position.set(0, 0.014, 0.01);
|
|
264
|
+
sprite.scale.set(0.04, 0.01, 1);
|
|
265
|
+
sprite.renderOrder = 999;
|
|
266
|
+
group.userData.label = sprite;
|
|
267
|
+
group.add(sprite);
|
|
268
|
+
|
|
269
|
+
cameras.push(group);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
182
273
|
// COM markers
|
|
183
274
|
if (showCOM) {
|
|
184
275
|
for (let i = 1; i < model.nbody; i++) {
|
|
@@ -190,8 +281,8 @@ export function Debug({
|
|
|
190
281
|
}
|
|
191
282
|
}
|
|
192
283
|
|
|
193
|
-
return { geoms, sites, joints, comMarkers };
|
|
194
|
-
}, [status, mjModelRef, showGeoms, showSites, showJoints, showCOM]);
|
|
284
|
+
return { geoms, sites, joints, cameras, comMarkers };
|
|
285
|
+
}, [status, mjModelRef, showGeoms, showSites, showJoints, showCameras, showCOM]);
|
|
195
286
|
|
|
196
287
|
// Add/remove debug objects from scene
|
|
197
288
|
useEffect(() => {
|
|
@@ -202,6 +293,7 @@ export function Debug({
|
|
|
202
293
|
...debugGeometry.geoms,
|
|
203
294
|
...debugGeometry.sites,
|
|
204
295
|
...debugGeometry.joints,
|
|
296
|
+
...debugGeometry.cameras,
|
|
205
297
|
...debugGeometry.comMarkers,
|
|
206
298
|
];
|
|
207
299
|
for (const obj of allObjects) group.add(obj);
|
|
@@ -281,6 +373,59 @@ export function Debug({
|
|
|
281
373
|
}
|
|
282
374
|
}
|
|
283
375
|
|
|
376
|
+
const camXpos = data.cam_xpos;
|
|
377
|
+
const camXmat = data.cam_xmat;
|
|
378
|
+
if (camXpos && camXmat) {
|
|
379
|
+
for (const group of debugGeometry.cameras) {
|
|
380
|
+
const cameraId = group.userData.cameraId;
|
|
381
|
+
const i3 = cameraId * 3;
|
|
382
|
+
const i9 = cameraId * 9;
|
|
383
|
+
group.position.set(
|
|
384
|
+
camXpos[i3],
|
|
385
|
+
camXpos[i3 + 1],
|
|
386
|
+
camXpos[i3 + 2]
|
|
387
|
+
);
|
|
388
|
+
_cameraMatrix.set(
|
|
389
|
+
camXmat[i9], camXmat[i9 + 1], camXmat[i9 + 2], 0,
|
|
390
|
+
camXmat[i9 + 3], camXmat[i9 + 4], camXmat[i9 + 5], 0,
|
|
391
|
+
camXmat[i9 + 6], camXmat[i9 + 7], camXmat[i9 + 8], 0,
|
|
392
|
+
0, 0, 0, 1
|
|
393
|
+
);
|
|
394
|
+
group.quaternion.setFromRotationMatrix(_cameraMatrix);
|
|
395
|
+
|
|
396
|
+
const fovy = model.cam_fovy?.[cameraId] ?? 45;
|
|
397
|
+
const halfHeight = Math.tan(THREE.MathUtils.degToRad(fovy) / 2) *
|
|
398
|
+
CAMERA_DEBUG_FRUSTUM_DEPTH;
|
|
399
|
+
const halfWidth = halfHeight * 4 / 3;
|
|
400
|
+
const positions = group.userData.frustum.geometry.attributes.position;
|
|
401
|
+
const array = positions.array as Float32Array;
|
|
402
|
+
const points = [
|
|
403
|
+
[0, 0, 0],
|
|
404
|
+
[-halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
405
|
+
[0, 0, 0],
|
|
406
|
+
[halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
407
|
+
[0, 0, 0],
|
|
408
|
+
[halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
409
|
+
[0, 0, 0],
|
|
410
|
+
[-halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
411
|
+
[-halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
412
|
+
[halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
413
|
+
[halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
414
|
+
[halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
415
|
+
[halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
416
|
+
[-halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
417
|
+
[-halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
418
|
+
[-halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
|
|
419
|
+
];
|
|
420
|
+
for (let i = 0; i < points.length; i += 1) {
|
|
421
|
+
array[i * 3] = points[i][0];
|
|
422
|
+
array[i * 3 + 1] = points[i][1];
|
|
423
|
+
array[i * 3 + 2] = points[i][2];
|
|
424
|
+
}
|
|
425
|
+
positions.needsUpdate = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
284
429
|
// Update COM markers
|
|
285
430
|
for (const mesh of debugGeometry.comMarkers) {
|
|
286
431
|
const bid = mesh.userData.bodyId;
|
|
@@ -358,7 +503,13 @@ export function Debug({
|
|
|
358
503
|
if (status !== 'ready') return null;
|
|
359
504
|
|
|
360
505
|
return (
|
|
361
|
-
<group
|
|
506
|
+
<group
|
|
507
|
+
{...groupProps}
|
|
508
|
+
userData={{
|
|
509
|
+
...groupProps.userData,
|
|
510
|
+
[CAPTURE_EXCLUDE_KEY]: true,
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
362
513
|
<group ref={groupRef} />
|
|
363
514
|
{showContacts && <group ref={contactGroupRef} />}
|
|
364
515
|
</group>
|
|
@@ -8,6 +8,7 @@ import type { ThreeElements } from '@react-three/fiber';
|
|
|
8
8
|
import { useEffect, useRef } from 'react';
|
|
9
9
|
import * as THREE from 'three';
|
|
10
10
|
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
11
|
+
import { CAPTURE_EXCLUDE_KEY } from '../rendering/cameraFrameCapture';
|
|
11
12
|
import type { DragInteractionProps } from '../types';
|
|
12
13
|
|
|
13
14
|
// Preallocated temps to avoid GC pressure
|
|
@@ -60,6 +61,7 @@ export function DragInteraction({
|
|
|
60
61
|
0.1,
|
|
61
62
|
0xff4444,
|
|
62
63
|
);
|
|
64
|
+
arrow.userData[CAPTURE_EXCLUDE_KEY] = true;
|
|
63
65
|
arrow.visible = false;
|
|
64
66
|
// Make arrow semi-transparent
|
|
65
67
|
(arrow.line.material as THREE.LineBasicMaterial).transparent = true;
|
|
@@ -9,6 +9,7 @@ import { useEffect, useRef } from 'react';
|
|
|
9
9
|
import * as THREE from 'three';
|
|
10
10
|
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
11
11
|
import { findSiteByName } from '../core/SceneLoader';
|
|
12
|
+
import { CAPTURE_EXCLUDE_KEY } from '../rendering/cameraFrameCapture';
|
|
12
13
|
import type { IkGizmoProps } from '../types';
|
|
13
14
|
|
|
14
15
|
// Preallocated temps to avoid GC pressure in useFrame
|
|
@@ -83,7 +84,10 @@ export function IkGizmo({ controller, siteName, scale = 0.18, onDrag }: IkGizmoP
|
|
|
83
84
|
if (status !== 'ready') return null;
|
|
84
85
|
|
|
85
86
|
return (
|
|
86
|
-
<group
|
|
87
|
+
<group
|
|
88
|
+
ref={wrapperRef}
|
|
89
|
+
userData={{ [CAPTURE_EXCLUDE_KEY]: true }}
|
|
90
|
+
>
|
|
87
91
|
<PivotControls
|
|
88
92
|
ref={pivotRef}
|
|
89
93
|
autoTransform
|
|
@@ -148,12 +148,12 @@ function vector3FromArray(values: ArrayLike<number>, offset: number): [number, n
|
|
|
148
148
|
return [values[offset], values[offset + 1], values[offset + 2]];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
function
|
|
151
|
+
function quaternionFromMujocoQuat(values: ArrayLike<number>, offset: number): [number, number, number, number] {
|
|
152
152
|
return [
|
|
153
|
-
values[offset],
|
|
154
153
|
values[offset + 1],
|
|
155
154
|
values[offset + 2],
|
|
156
155
|
values[offset + 3],
|
|
156
|
+
values[offset],
|
|
157
157
|
];
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -993,7 +993,7 @@ export function MujocoSimProvider({
|
|
|
993
993
|
? vector3FromArray(model.cam_pos, posOffset)
|
|
994
994
|
: null,
|
|
995
995
|
quaternion: model.cam_quat
|
|
996
|
-
?
|
|
996
|
+
? quaternionFromMujocoQuat(model.cam_quat, quatOffset)
|
|
997
997
|
: null,
|
|
998
998
|
});
|
|
999
999
|
}
|
|
@@ -1024,7 +1024,7 @@ export function MujocoSimProvider({
|
|
|
1024
1024
|
const quaternion = data.cam_xmat
|
|
1025
1025
|
? quaternionFromXmat(data.cam_xmat, cameraId * 9)
|
|
1026
1026
|
: model.cam_quat
|
|
1027
|
-
?
|
|
1027
|
+
? quaternionFromMujocoQuat(model.cam_quat, cameraId * 4)
|
|
1028
1028
|
: undefined;
|
|
1029
1029
|
|
|
1030
1030
|
if (!position || !quaternion) {
|
|
@@ -1442,7 +1442,7 @@ export function MujocoSimProvider({
|
|
|
1442
1442
|
const cameraFrames: Record<string, CameraFrameCaptureResult> = {};
|
|
1443
1443
|
for (const { key, captureOptions, mountedSource, session } of captureSessions) {
|
|
1444
1444
|
const resolvedCaptureOptions = resolveCameraCaptureOptions(captureOptions);
|
|
1445
|
-
const cameraFrame = session.
|
|
1445
|
+
const cameraFrame = await session.captureDataUrlAsync({
|
|
1446
1446
|
...resolvedCaptureOptions,
|
|
1447
1447
|
source: mountedSource ?? resolvedCaptureOptions.source,
|
|
1448
1448
|
});
|
|
@@ -94,7 +94,14 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
|
|
|
94
94
|
return genericIkRef.current.solve(
|
|
95
95
|
model, data, siteIdRef.current, controlGroup.qposAdr,
|
|
96
96
|
position, quaternion, currentQ,
|
|
97
|
-
{
|
|
97
|
+
{
|
|
98
|
+
damping: config.damping,
|
|
99
|
+
epsilon: config.epsilon,
|
|
100
|
+
maxIterations: config.maxIterations,
|
|
101
|
+
posWeight: config.posWeight,
|
|
102
|
+
rotWeight: config.rotWeight,
|
|
103
|
+
tolerance: config.tolerance,
|
|
104
|
+
},
|
|
98
105
|
);
|
|
99
106
|
},
|
|
100
107
|
[config, mjModelRef, mjDataRef],
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useFrame } from '@react-three/fiber';
|
|
7
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
8
|
+
import * as THREE from 'three';
|
|
9
|
+
import type { KeyboardIkTargetAction, KeyboardIkTargetBinding, KeyboardIkTargetConfig } from '../types';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TRANSLATE_SPEED = 0.25;
|
|
12
|
+
const DEFAULT_ROTATE_SPEED = 1.0;
|
|
13
|
+
|
|
14
|
+
const _translation = new THREE.Vector3();
|
|
15
|
+
const _axis = new THREE.Vector3();
|
|
16
|
+
const _quat = new THREE.Quaternion();
|
|
17
|
+
|
|
18
|
+
function actionSign(action: KeyboardIkTargetAction): 1 | -1 {
|
|
19
|
+
return action.endsWith('+') ? 1 : -1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function actionBase(action: KeyboardIkTargetAction) {
|
|
23
|
+
return action.slice(0, -1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function applyRotation(
|
|
27
|
+
target: THREE.Object3D,
|
|
28
|
+
action: KeyboardIkTargetAction,
|
|
29
|
+
amount: number,
|
|
30
|
+
frame: 'world' | 'target',
|
|
31
|
+
) {
|
|
32
|
+
const base = actionBase(action);
|
|
33
|
+
if (base === 'pitch') {
|
|
34
|
+
_axis.set(1, 0, 0);
|
|
35
|
+
} else if (base === 'yaw') {
|
|
36
|
+
_axis.set(0, 1, 0);
|
|
37
|
+
} else if (base === 'roll') {
|
|
38
|
+
_axis.set(0, 0, 1);
|
|
39
|
+
} else {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_quat.setFromAxisAngle(_axis, amount);
|
|
44
|
+
if (frame === 'target') {
|
|
45
|
+
target.quaternion.multiply(_quat);
|
|
46
|
+
} else {
|
|
47
|
+
target.quaternion.premultiply(_quat);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function addTranslation(action: KeyboardIkTargetAction, amount: number) {
|
|
52
|
+
switch (action) {
|
|
53
|
+
case 'x+':
|
|
54
|
+
_translation.x += amount;
|
|
55
|
+
break;
|
|
56
|
+
case 'x-':
|
|
57
|
+
_translation.x -= amount;
|
|
58
|
+
break;
|
|
59
|
+
case 'y+':
|
|
60
|
+
_translation.y += amount;
|
|
61
|
+
break;
|
|
62
|
+
case 'y-':
|
|
63
|
+
_translation.y -= amount;
|
|
64
|
+
break;
|
|
65
|
+
case 'z+':
|
|
66
|
+
_translation.z += amount;
|
|
67
|
+
break;
|
|
68
|
+
case 'z-':
|
|
69
|
+
_translation.z -= amount;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Moves an existing IK target from keyboard input.
|
|
76
|
+
*
|
|
77
|
+
* This hook is intentionally robot-agnostic: it only edits the target owned by
|
|
78
|
+
* `useIkController`, then lets the normal IK solver write robot controls.
|
|
79
|
+
*/
|
|
80
|
+
export function useKeyboardIkTarget(config: KeyboardIkTargetConfig | null) {
|
|
81
|
+
const pressedRef = useRef(new Set<string>());
|
|
82
|
+
const wasActiveRef = useRef(false);
|
|
83
|
+
const configRef = useRef(config);
|
|
84
|
+
configRef.current = config;
|
|
85
|
+
|
|
86
|
+
const boundCodes = useMemo(() => {
|
|
87
|
+
return new Set((config?.bindings ?? []).map((binding) => binding.code));
|
|
88
|
+
}, [config?.bindings]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
92
|
+
const current = configRef.current;
|
|
93
|
+
if (!current || current.enabled === false || !boundCodes.has(event.code)) return;
|
|
94
|
+
if (current.preventDefault !== false) event.preventDefault();
|
|
95
|
+
pressedRef.current.add(event.code);
|
|
96
|
+
};
|
|
97
|
+
const onKeyUp = (event: KeyboardEvent) => {
|
|
98
|
+
const current = configRef.current;
|
|
99
|
+
if (boundCodes.has(event.code) && current?.preventDefault !== false) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
}
|
|
102
|
+
pressedRef.current.delete(event.code);
|
|
103
|
+
};
|
|
104
|
+
const onBlur = () => {
|
|
105
|
+
pressedRef.current.clear();
|
|
106
|
+
wasActiveRef.current = false;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
window.addEventListener('keydown', onKeyDown);
|
|
110
|
+
window.addEventListener('keyup', onKeyUp);
|
|
111
|
+
window.addEventListener('blur', onBlur);
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
115
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
116
|
+
window.removeEventListener('blur', onBlur);
|
|
117
|
+
};
|
|
118
|
+
}, [boundCodes]);
|
|
119
|
+
|
|
120
|
+
useFrame((_state, delta) => {
|
|
121
|
+
const current = configRef.current;
|
|
122
|
+
const controller = current?.controller;
|
|
123
|
+
if (!current || current.enabled === false || !controller) {
|
|
124
|
+
wasActiveRef.current = false;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const activeBindings: KeyboardIkTargetBinding[] = [];
|
|
129
|
+
for (const binding of current.bindings) {
|
|
130
|
+
if (pressedRef.current.has(binding.code)) activeBindings.push(binding);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (activeBindings.length === 0) {
|
|
134
|
+
wasActiveRef.current = false;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!wasActiveRef.current) {
|
|
139
|
+
if (current.syncOnStart !== false) controller.syncTargetToSite();
|
|
140
|
+
if (current.autoEnableIk !== false && !controller.ikEnabledRef.current) {
|
|
141
|
+
controller.setIkEnabled(true);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
wasActiveRef.current = true;
|
|
145
|
+
|
|
146
|
+
const target = controller.ikTargetRef.current;
|
|
147
|
+
if (!target) return;
|
|
148
|
+
|
|
149
|
+
const frame = current.frame ?? 'world';
|
|
150
|
+
_translation.set(0, 0, 0);
|
|
151
|
+
|
|
152
|
+
for (const binding of activeBindings) {
|
|
153
|
+
const translateSpeed = binding.translateSpeed ?? current.translateSpeed ?? DEFAULT_TRANSLATE_SPEED;
|
|
154
|
+
const rotateSpeed = binding.rotateSpeed ?? current.rotateSpeed ?? DEFAULT_ROTATE_SPEED;
|
|
155
|
+
const amount = actionSign(binding.action) * delta;
|
|
156
|
+
const base = actionBase(binding.action);
|
|
157
|
+
|
|
158
|
+
if (base === 'x' || base === 'y' || base === 'z') {
|
|
159
|
+
addTranslation(binding.action, translateSpeed * delta);
|
|
160
|
+
} else {
|
|
161
|
+
applyRotation(target, binding.action, rotateSpeed * amount, frame);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (_translation.lengthSq() > 0) {
|
|
166
|
+
if (frame === 'target') _translation.applyQuaternion(target.quaternion);
|
|
167
|
+
target.position.add(_translation);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -91,6 +91,7 @@ export { useBodyState } from './hooks/useBodyState';
|
|
|
91
91
|
export { useCtrl } from './hooks/useCtrl';
|
|
92
92
|
export { useContacts, useContactEvents } from './hooks/useContacts';
|
|
93
93
|
export { useKeyboardTeleop } from './hooks/useKeyboardTeleop';
|
|
94
|
+
export { useKeyboardIkTarget } from './hooks/useKeyboardIkTarget';
|
|
94
95
|
export { usePolicy } from './hooks/usePolicy';
|
|
95
96
|
export { useObservation } from './hooks/useObservation';
|
|
96
97
|
export { useTrajectoryPlayer } from './hooks/useTrajectoryPlayer';
|
|
@@ -113,6 +114,7 @@ export type {
|
|
|
113
114
|
MountedCameraSequenceRecordResult,
|
|
114
115
|
} from './hooks/useMountedCameraSequenceRecorder';
|
|
115
116
|
export {
|
|
117
|
+
CAPTURE_EXCLUDE_KEY,
|
|
116
118
|
captureCameraFrame,
|
|
117
119
|
captureCameraFrameBlob,
|
|
118
120
|
createCameraFrameCaptureSession,
|
|
@@ -210,6 +212,9 @@ export type {
|
|
|
210
212
|
// Keyboard teleop
|
|
211
213
|
KeyBinding,
|
|
212
214
|
KeyboardTeleopConfig,
|
|
215
|
+
KeyboardIkTargetAction,
|
|
216
|
+
KeyboardIkTargetBinding,
|
|
217
|
+
KeyboardIkTargetConfig,
|
|
213
218
|
// Policy
|
|
214
219
|
PolicyConfig,
|
|
215
220
|
PolicyVector,
|