Skip to content

Commit 64a6b98

Browse files
zjm-metameta-codesync[bot]
authored andcommitted
feat(core) Support holding hand pinching for OneHand and TwoHand grabbables.
Summary: As per requested by the developers in github, we should support OneHandGrabbable and TwoHandsGrabbable with hand tracking. This diff added a new option in Grab System to forward pinch to squeeze signal when hand tracking is enabled to support this use case. Reviewed By: felixtrz Differential Revision: D88771989 fbshipit-source-id: 3d1b2fe510515849d7347d5aceded1837c3c5a89
1 parent 53cc57e commit 64a6b98

File tree

8 files changed

+85
-27
lines changed

8 files changed

+85
-27
lines changed

docs/concepts/grabbing/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Grabbing Overview
66

77
Interactive object manipulation is foundational to immersive experiences. IWSDK provides a comprehensive grabbing system that enables natural, performant object interaction through three distinct manipulation patterns—each optimized for different use cases and comfort requirements.
88

9-
- **One-Hand Grabbing** — Direct single-controller manipulation for basic interactions
9+
- **One-Hand Grabbing** — Direct single input manipulation for basic interactions
1010
- **Two-Hand Grabbing** — Dual-hand manipulation with scaling capabilities for precise control
1111
- **Distance Grabbing** — Ray-based remote manipulation with telekinetic-style interactions
1212

docs/concepts/grabbing/interaction-types.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ IWSDK's grabbing system provides three distinct interaction patterns, each imple
88

99
## One-Hand Grabbing
1010

11-
Single-controller direct manipulation optimized for immediate, responsive interactions.
11+
Single-input direct manipulation optimized for immediate, responsive interactions.
1212

1313
### Architecture Characteristics
1414

@@ -52,7 +52,7 @@ const handle = new HandleStore(object, () => ({
5252

5353
## Two-Hand Grabbing
5454

55-
Dual-controller manipulation enabling advanced operations including scaling through multi-pointer coordination.
55+
Dual-input manipulation enabling advanced operations including scaling through multi-pointer coordination.
5656

5757
### Architecture Characteristics
5858

docs/guides/06-built-in-interactions.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ With grabbing enabled, you can add grab components to any object. IWSDK provides
5555

5656
### OneHandGrabbable - Direct Manipulation
5757

58-
For objects you grab and move directly with one hand:
58+
For objects you grab and move directly with one input:
5959

6060
```javascript
6161
// Make your existing robot grabbable
@@ -73,6 +73,25 @@ world
7373
});
7474
```
7575

76+
> Note that hand tracking is by default not working with OneHandGrabbable and TwoHandsGrabbable as the hand pinch is mapped to the trigger button instead of the squeeze button. To use hand tracking for OneHandGrabbable and TwoHandsGrabbable, you will need to enable `useHandPinchForGrab` in the grab system. For example,
77+
78+
```javascript
79+
// Update your existing World.create() call in src/index.ts
80+
World.create(document.getElementById('scene-container'), {
81+
assets,
82+
xr: {
83+
sessionMode: SessionMode.ImmersiveVR,
84+
features: { handTracking: true },
85+
},
86+
features: {
87+
grabbing: { useHandPinchForGrab: true }, // Enable grab system with hand pinch grabbing
88+
// ... other existing features
89+
},
90+
}).then((world) => {
91+
// ... your existing setup
92+
});
93+
```
94+
7695
### DistanceGrabbable - At-a-Distance Interaction
7796

7897
For objects you can grab, scale, and manipulate from far away:

examples/grab/metaspatial/Composition/Main.metaspatialcomp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ entities:
99
scenes:
1010
- ref:Scene
1111
metadata:
12-
version: 1.34
12+
version: 1.35

examples/grab/metaspatial/Composition/Main.scene

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ entities:
4747
tag: Environment
4848
- components:
4949
com.iwsdk.components.DistanceGrabbable:
50-
scale: false
51-
movementMode: 3
5250
translate: false
51+
movementMode: 3
52+
scale: false
5353
com.iwsdk.components.Interactable:
5454
{}
5555
com.meta.components.Animatable:
@@ -319,4 +319,4 @@ entities:
319319
visible: true
320320
tag: pyramid duplicate
321321
metadata:
322-
version: 1.34
322+
version: 1.35

examples/grab/src/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ World.create(document.getElementById('scene-container'), {
2424
assets,
2525
xr: {
2626
sessionMode: SessionMode.ImmersiveVR,
27-
requiredFeatures: ['hand-tracking'],
27+
features: {
28+
handTracking: { required: true },
29+
},
2830
},
2931
level: '/glxf/Composition.glxf',
3032
features: {
31-
grabbing: true,
33+
grabbing: { useHandPinchForGrab: true },
3234
locomotion: true,
3335
spatialUI: true,
3436
},

packages/core/src/grab/grab-system.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { HandleStore } from '@pmndrs/handle';
99
import { PointerEventsMap } from '@pmndrs/pointer-events';
10-
import { createSystem, Entity } from '../ecs/index.js';
10+
import { createSystem, Entity, Types } from '../ecs/index.js';
1111
import { Object3D, Object3DEventMap } from '../runtime/index.js';
1212
import { DistanceGrabbable } from './distance-grabbable.js';
1313
import { DistanceGrabHandle, MovementMode, Handle } from './handles.js';
@@ -48,20 +48,30 @@ import { TwoHandsGrabbable } from './two-hands-grabbable.js';
4848
* @see {@link TwoHandsGrabbable}
4949
* @see {@link DistanceGrabbable}
5050
*/
51-
export class GrabSystem extends createSystem({
52-
oneHandGrabbables: {
53-
required: [OneHandGrabbable],
51+
export class GrabSystem extends createSystem(
52+
{
53+
oneHandGrabbables: {
54+
required: [OneHandGrabbable],
55+
},
56+
twoHandsGrabbables: {
57+
required: [TwoHandsGrabbable],
58+
},
59+
distanceGrabbables: {
60+
required: [DistanceGrabbable],
61+
},
62+
handles: {
63+
required: [Handle],
64+
},
5465
},
55-
twoHandsGrabbables: {
56-
required: [TwoHandsGrabbable],
66+
{
67+
/**
68+
* Controls whether hand pinch gestures are forwarded as squeeze events for grab interactions.
69+
* Set to `false` to disable pinch-to-grab and require the physical squeeze button instead.
70+
* @default false
71+
*/
72+
useHandPinchForGrab: { type: Types.Boolean, default: false },
5773
},
58-
distanceGrabbables: {
59-
required: [DistanceGrabbable],
60-
},
61-
handles: {
62-
required: [Handle],
63-
},
64-
}) {
74+
) {
6575
init() {
6676
// Ensure Handle component is registered in the world before queries rely on it
6777
if (!Handle.bitmask) {
@@ -110,7 +120,27 @@ export class GrabSystem extends createSystem({
110120
this.input.multiPointers.right.toggleSubPointer('grab', true);
111121
}
112122

113-
update(delta: number): void {
123+
update(delta: number, time: number): void {
124+
// Forward hand pinch events to squeeze for grab interactions
125+
// Only forward if the system-level useHandPinchForGrab flag is enabled
126+
if (this.config.useHandPinchForGrab) {
127+
(['left', 'right'] as const).forEach((handedness) => {
128+
if (this.input.isPrimary('hand', handedness)) {
129+
const timeStamp = time * 1000;
130+
if (this.input.gamepads[handedness]?.getSelectStart()) {
131+
this.input.multiPointers[handedness].routeDown('squeeze', 'grab', {
132+
timeStamp,
133+
});
134+
}
135+
if (this.input.gamepads[handedness]?.getSelectEnd()) {
136+
this.input.multiPointers[handedness].routeUp('squeeze', 'grab', {
137+
timeStamp,
138+
});
139+
}
140+
}
141+
});
142+
}
143+
114144
// Unified handle updates
115145
this.queries.handles.entities.forEach((entity) => {
116146
const h = Handle.data.instance[entity.index] as

packages/core/src/init/world-initializer.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export type WorldOptions = {
9696
/** Locomotion (teleport/slide/turn). Boolean or config. @defaultValue false */
9797
locomotion?: boolean | { useWorker?: boolean };
9898
/** Grabbing (one/two‑hand, distance). @defaultValue false */
99-
grabbing?: boolean;
99+
grabbing?: boolean | { useHandPinchForGrab?: boolean };
100100
/** Physics simulation (Havok). @defaultValue false */
101101
physics?: boolean;
102102
/** Scene Understanding (planes/meshes/anchors). Boolean or config. @defaultValue false */
@@ -420,7 +420,10 @@ function registerFeatureSystems(
420420
| boolean
421421
| { useWorker?: boolean };
422422
const locomotionEnabled = !!locomotion;
423-
const grabbingEnabled = !!config.features.grabbing;
423+
const grabbing = config.features.grabbing as
424+
| boolean
425+
| { useHandPinchForGrab?: boolean };
426+
const grabbingEnabled = !!grabbing;
424427
const physicsEnabled = !!config.features.physics;
425428
const sceneUnderstanding = config.features.sceneUnderstanding as
426429
| boolean
@@ -444,7 +447,11 @@ function registerFeatureSystems(
444447
}
445448
world.registerSystem(InputSystem, { priority: -4 });
446449
if (grabbingEnabled) {
447-
world.registerSystem(GrabSystem, { priority: -3 });
450+
const grabOpts =
451+
typeof grabbing === 'object' && grabbing
452+
? { useHandPinchForGrab: grabbing.useHandPinchForGrab }
453+
: undefined;
454+
world.registerSystem(GrabSystem, { priority: -3, configData: grabOpts });
448455
}
449456

450457
// Physics runs after Grab so it can respect Pressed overrides

0 commit comments

Comments
 (0)