@realitycollective/xrblocks-interactions 0.1.0-preview.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/host.d.ts +37 -0
- package/dist/host.js +42 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +96 -0
- package/dist/provider.js +150 -0
- package/dist/provider.js.map +1 -0
- package/package.json +62 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Change log for the Reality Collective WebXR Interaction Extensions packages. All four packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
|
|
6
|
+
|
|
7
|
+
## [0.1.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `@realitycollective/webxr-interactions` - engine-free core: interactables and interactors, the behaviour set (`press` including latching, `pulse`, `hinge`, `dial`, `slide`, `grab` in poseOnly and native modes, `tossScore`), gaze (`required` gating and dwell-to-press), the runtime/binder with hints > poke > ray targeting, hysteresis and lifecycle, capability negotiation with a visible `behaviourDisabled` outcome, events as the only outbound pathway, and feedback intents (haptics and audio remain the client's, with `routeHapticsToProvider` as an explicit opt-in).
|
|
12
|
+
- `@realitycollective/threejs-interactions` - the default standalone adapter: raw WebXR (`XRSession` sources, hand joints, select/squeeze plus gamepad analog, haptic pulse), three.js hit-testing and transform ports, and a desktop mouse fallback. No framework required.
|
|
13
|
+
- `@realitycollective/iwsdk-interactions` - Meta IWSDK adapter: player-rig poses and stateful gamepads as the provider, IWSDK's own targeting (`Pressed` / `Grabbed` tags) forwarded as pre-resolved hints, and native grab fulfilment when the app enables IWSDK grabbing/physics. One-call `registerInteractions(world)` setup.
|
|
14
|
+
- `@realitycollective/xrblocks-interactions` - EXPERIMENTAL Google XR Blocks adapter, structurally typed against xrblocks v0.20.0 (`Input.getFrame()` ray sources and direct touches, pooled structs copied, no haptics upstream).
|
|
15
|
+
- `demos/playground` - the standalone interaction playground: the full station set from one portable `InteractionDescriptor`, with client-side audio cues, opt-in controller haptics and client-side toss ballistics.
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
|
|
19
|
+
- All four packages depend on `@realitycollective/webxr-input`, released independently from the [WebXR-Input](https://github.com/realitycollective/WebXR-Input) repository. That package must be published before this one.
|
|
20
|
+
|
|
21
|
+
[0.1.0]: https://github.com/realitycollective/WebXR-Interactions/commits/main
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reality Collective
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @realitycollective/xrblocks-interactions
|
|
2
|
+
|
|
3
|
+
> **EXPERIMENTAL.** The XR Blocks pipeline is young and its API still moves. Treat this adapter as a preview and pin your versions.
|
|
4
|
+
|
|
5
|
+
The Google XR Blocks adapter for the Reality Collective Interaction Extensions. It feeds XR Blocks' input into the [`@realitycollective/webxr-interactions`](https://www.npmjs.com/package/@realitycollective/webxr-interactions) core, reusing the three.js adapter's hit-testing and object movement.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @realitycollective/xrblocks-interactions three
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It re-exports everything from the core, so this is the only interaction package your app needs.
|
|
12
|
+
|
|
13
|
+
## What it binds
|
|
14
|
+
|
|
15
|
+
| Layer | Detail |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| **Input** | `Input.getFrame()` ray sources and direct touches; XR Blocks `Script` select events |
|
|
18
|
+
| **Hit-testing** | Shared with the three.js adapter |
|
|
19
|
+
| **No xrblocks dependency** | It matches the shape of the XR Blocks API in TypeScript rather than importing `xrblocks`, so an upstream release cannot break your install |
|
|
20
|
+
| **Haptics** | XR Blocks has none, so haptic requests are reported but never played |
|
|
21
|
+
|
|
22
|
+
Structurally typed against xrblocks **v0.20.0**.
|
|
23
|
+
|
|
24
|
+
## Known constraint
|
|
25
|
+
|
|
26
|
+
xrblocks declares a peer of `three@^0.184` while Meta's IWSDK mandates the `super-three@0.181` fork. A bundler resolves a single `three` per bundle, so the pairing works in practice, but npm's peer check cannot express it - the Reality Collective workspaces set `legacy-peer-deps=true` for this reason.
|
|
27
|
+
|
|
28
|
+
## Peer dependency
|
|
29
|
+
|
|
30
|
+
`three >= 0.170.0`.
|
|
31
|
+
|
|
32
|
+
## Live demo
|
|
33
|
+
|
|
34
|
+
The interaction playground - the full station set, mouse-capable on desktop, VR button for headsets: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
See the [repository README](https://github.com/realitycollective/WebXR-Interactions#readme).
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT - see [LICENSE](./LICENSE).
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connectXRBlocksInteractions - EXPERIMENTAL one-call setup.
|
|
3
|
+
*
|
|
4
|
+
* XR Blocks Scripts are ordinary three.js Object3Ds, so the shim reuses
|
|
5
|
+
* the three.js adapter's hit-tester and transform ports; only the input
|
|
6
|
+
* provider is XR Blocks-specific.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import * as xb from 'xrblocks';
|
|
10
|
+
* class MyScript extends xb.Script {
|
|
11
|
+
* init() {
|
|
12
|
+
* this.ix = connectXRBlocksInteractions({ input: xb.input, camera: xb.camera });
|
|
13
|
+
* this.ix.register({ id: "button", behaviours: [{ kind: "press" }] }, buttonMesh);
|
|
14
|
+
* }
|
|
15
|
+
* update() { this.ix.update(xb.getDeltaTime()); }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import type { Object3D } from "three";
|
|
20
|
+
import { InteractionRuntime, ThreeHitTester, ThreeTransformPort, type DwellConfig, type InteractableDescriptor } from "@realitycollective/threejs-interactions";
|
|
21
|
+
import { XRBlocksInputProvider, type XRBlocksContext } from "./provider.js";
|
|
22
|
+
export interface XRBlocksInteractionsOptions extends XRBlocksContext {
|
|
23
|
+
dwellDefaults?: DwellConfig;
|
|
24
|
+
}
|
|
25
|
+
export declare class XRBlocksInteractions {
|
|
26
|
+
readonly runtime: InteractionRuntime;
|
|
27
|
+
readonly provider: XRBlocksInputProvider;
|
|
28
|
+
readonly hitTester: ThreeHitTester;
|
|
29
|
+
private readonly ports;
|
|
30
|
+
constructor(options: XRBlocksInteractionsOptions);
|
|
31
|
+
register(descriptor: InteractableDescriptor, object: Object3D): ThreeTransformPort;
|
|
32
|
+
unregister(id: string): void;
|
|
33
|
+
getPort(id: string): ThreeTransformPort | undefined;
|
|
34
|
+
update(dt: number): void;
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
export declare function connectXRBlocksInteractions(options: XRBlocksInteractionsOptions): XRBlocksInteractions;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { InteractionRuntime, ThreeHitTester, ThreeTransformPort, } from "@realitycollective/threejs-interactions";
|
|
2
|
+
import { XRBlocksInputProvider } from "./provider.js";
|
|
3
|
+
export class XRBlocksInteractions {
|
|
4
|
+
runtime;
|
|
5
|
+
provider;
|
|
6
|
+
hitTester;
|
|
7
|
+
ports = new Map();
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.provider = new XRBlocksInputProvider(options);
|
|
10
|
+
this.hitTester = new ThreeHitTester();
|
|
11
|
+
this.runtime = new InteractionRuntime({
|
|
12
|
+
provider: this.provider,
|
|
13
|
+
hitTester: this.hitTester,
|
|
14
|
+
...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
register(descriptor, object) {
|
|
18
|
+
const port = new ThreeTransformPort(object);
|
|
19
|
+
this.ports.set(descriptor.id, port);
|
|
20
|
+
this.hitTester.register(descriptor.id, object);
|
|
21
|
+
this.runtime.registerInteractable(descriptor, { transform: port });
|
|
22
|
+
return port;
|
|
23
|
+
}
|
|
24
|
+
unregister(id) {
|
|
25
|
+
this.runtime.unregisterInteractable(id);
|
|
26
|
+
this.hitTester.unregister(id);
|
|
27
|
+
this.ports.delete(id);
|
|
28
|
+
}
|
|
29
|
+
getPort(id) {
|
|
30
|
+
return this.ports.get(id);
|
|
31
|
+
}
|
|
32
|
+
update(dt) {
|
|
33
|
+
this.runtime.update(dt);
|
|
34
|
+
}
|
|
35
|
+
dispose() {
|
|
36
|
+
this.runtime.dispose();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function connectXRBlocksInteractions(options) {
|
|
40
|
+
return new XRBlocksInteractions(options);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=host.js.map
|
package/dist/host.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,kBAAkB,GAGnB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAwB,MAAM,eAAe,CAAC;AAM5E,MAAM,OAAO,oBAAoB;IACtB,OAAO,CAAqB;IAC5B,QAAQ,CAAwB;IAChC,SAAS,CAAiB;IAClB,KAAK,GAAG,IAAI,GAAG,EAA8B,CAAC;IAE/D,YAAY,OAAoC;QAC9C,IAAI,CAAC,QAAQ,GAAG,IAAI,qBAAqB,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,CAAC,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;QACtC,IAAI,CAAC,OAAO,GAAG,IAAI,kBAAkB,CAAC;YACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3E,CAAC,CAAC;IACL,CAAC;IAED,QAAQ,CAAC,UAAkC,EAAE,MAAgB;QAC3D,MAAM,IAAI,GAAG,IAAI,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,UAAU,CAAC,EAAU;QACnB,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACxB,CAAC;IAED,OAAO,CAAC,EAAU;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;IAED,MAAM,CAAC,EAAU;QACf,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO;QACL,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IACzB,CAAC;CACF;AAED,MAAM,UAAU,2BAA2B,CACzC,OAAoC;IAEpC,OAAO,IAAI,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAC3C,CAAC","sourcesContent":["/**\n * connectXRBlocksInteractions - EXPERIMENTAL one-call setup.\n *\n * XR Blocks Scripts are ordinary three.js Object3Ds, so the shim reuses\n * the three.js adapter's hit-tester and transform ports; only the input\n * provider is XR Blocks-specific.\n *\n * ```ts\n * import * as xb from 'xrblocks';\n * class MyScript extends xb.Script {\n * init() {\n * this.ix = connectXRBlocksInteractions({ input: xb.input, camera: xb.camera });\n * this.ix.register({ id: \"button\", behaviours: [{ kind: \"press\" }] }, buttonMesh);\n * }\n * update() { this.ix.update(xb.getDeltaTime()); }\n * }\n * ```\n */\nimport type { Object3D } from \"three\";\nimport {\n InteractionRuntime,\n ThreeHitTester,\n ThreeTransformPort,\n type DwellConfig,\n type InteractableDescriptor,\n} from \"@realitycollective/threejs-interactions\";\nimport { XRBlocksInputProvider, type XRBlocksContext } from \"./provider.js\";\n\nexport interface XRBlocksInteractionsOptions extends XRBlocksContext {\n dwellDefaults?: DwellConfig;\n}\n\nexport class XRBlocksInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: XRBlocksInputProvider;\n readonly hitTester: ThreeHitTester;\n private readonly ports = new Map<string, ThreeTransformPort>();\n\n constructor(options: XRBlocksInteractionsOptions) {\n this.provider = new XRBlocksInputProvider(options);\n this.hitTester = new ThreeHitTester();\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n });\n }\n\n register(descriptor: InteractableDescriptor, object: Object3D): ThreeTransformPort {\n const port = new ThreeTransformPort(object);\n this.ports.set(descriptor.id, port);\n this.hitTester.register(descriptor.id, object);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.hitTester.unregister(id);\n this.ports.delete(id);\n }\n\n getPort(id: string): ThreeTransformPort | undefined {\n return this.ports.get(id);\n }\n\n update(dt: number): void {\n this.runtime.update(dt);\n }\n\n dispose(): void {\n this.runtime.dispose();\n }\n}\n\nexport function connectXRBlocksInteractions(\n options: XRBlocksInteractionsOptions,\n): XRBlocksInteractions {\n return new XRBlocksInteractions(options);\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @realitycollective/xrblocks-interactions - EXPERIMENTAL Google XR Blocks
|
|
3
|
+
* adapter. Structurally typed against xrblocks v0.20.0 (no xrblocks
|
|
4
|
+
* import); re-exports the core (via the three.js adapter machinery).
|
|
5
|
+
*/
|
|
6
|
+
export * from "@realitycollective/threejs-interactions";
|
|
7
|
+
export * from "./provider.js";
|
|
8
|
+
export * from "./host.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @realitycollective/xrblocks-interactions - EXPERIMENTAL Google XR Blocks
|
|
3
|
+
* adapter. Structurally typed against xrblocks v0.20.0 (no xrblocks
|
|
4
|
+
* import); re-exports the core (via the three.js adapter machinery).
|
|
5
|
+
*/
|
|
6
|
+
export * from "@realitycollective/threejs-interactions";
|
|
7
|
+
export * from "./provider.js";
|
|
8
|
+
export * from "./host.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,yCAAyC,CAAC;AAExD,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC","sourcesContent":["/**\n * @realitycollective/xrblocks-interactions - EXPERIMENTAL Google XR Blocks\n * adapter. Structurally typed against xrblocks v0.20.0 (no xrblocks\n * import); re-exports the core (via the three.js adapter machinery).\n */\nexport * from \"@realitycollective/threejs-interactions\";\n\nexport * from \"./provider.js\";\nexport * from \"./host.js\";\n"]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XRBlocksInputProvider - EXPERIMENTAL.
|
|
3
|
+
*
|
|
4
|
+
* Binds to the SHAPE of Google XR Blocks (verified against xrblocks
|
|
5
|
+
* v0.20.0 source, 2026-08) without importing `xrblocks`, per the
|
|
6
|
+
* UI Extensions structural-typing convention:
|
|
7
|
+
*
|
|
8
|
+
* - `xb.input.getFrame()` → `{ raySources, directTouches }`. Those slot
|
|
9
|
+
* objects are POOLED AND MUTATED per frame upstream - everything is
|
|
10
|
+
* copied here, never retained.
|
|
11
|
+
* - `RaySourceInput`: `{ controller, sourceType: 'mouse'|'controller-ray'
|
|
12
|
+
* |'hand-ray'|'gaze'|'simulator', ray, selected }` - handedness from
|
|
13
|
+
* `controller.inputSource?.handedness`; analog select only via
|
|
14
|
+
* `controller.gamepad?.buttons[0].value`; squeeze only as the boolean
|
|
15
|
+
* `controller.userData.squeezing`.
|
|
16
|
+
* - `DirectTouchInput`: `{ handIndex (0=left,1=right), point }` - the
|
|
17
|
+
* index-fingertip world position, merged into the matching hand source.
|
|
18
|
+
* - Grip poses are not exposed by the frame struct; the ray pose doubles
|
|
19
|
+
* as the carry pose (XR Blocks keeps grip transforms behind
|
|
20
|
+
* `options.controllers.visualization`).
|
|
21
|
+
* - Gaze rides `sourceType: 'gaze'` and never raw-selects (XR Blocks
|
|
22
|
+
* selects gaze via its own dwell; ours is the core's dwell).
|
|
23
|
+
* - Haptics: none in XR Blocks (verified) - capability stays false.
|
|
24
|
+
*/
|
|
25
|
+
import { type HeadPose, type InputCapabilities, type InputProvider, type InputSourceSnapshot, type Unsubscribe } from "@realitycollective/webxr-input";
|
|
26
|
+
/** Structural slice of THREE.Vector3 / Quaternion. */
|
|
27
|
+
interface Vec3Like {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
z: number;
|
|
31
|
+
}
|
|
32
|
+
interface QuatLike extends Vec3Like {
|
|
33
|
+
w: number;
|
|
34
|
+
}
|
|
35
|
+
interface RayLike {
|
|
36
|
+
origin: Vec3Like;
|
|
37
|
+
direction: Vec3Like;
|
|
38
|
+
}
|
|
39
|
+
interface ControllerLike {
|
|
40
|
+
inputSource?: {
|
|
41
|
+
handedness?: string;
|
|
42
|
+
};
|
|
43
|
+
gamepad?: {
|
|
44
|
+
buttons?: ReadonlyArray<{
|
|
45
|
+
value: number;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
userData?: {
|
|
49
|
+
squeezing?: boolean;
|
|
50
|
+
selected?: boolean;
|
|
51
|
+
id?: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Structural `RaySourceInput` (xrblocks src/interaction/InteractionTypes.ts). */
|
|
55
|
+
export interface XBRaySourceLike {
|
|
56
|
+
controller: ControllerLike;
|
|
57
|
+
sourceType: string;
|
|
58
|
+
ray: RayLike;
|
|
59
|
+
selected: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Structural `DirectTouchInput`. */
|
|
62
|
+
export interface XBDirectTouchLike {
|
|
63
|
+
controller: ControllerLike;
|
|
64
|
+
handIndex: number;
|
|
65
|
+
point: Vec3Like;
|
|
66
|
+
selected: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface XBFrameLike {
|
|
69
|
+
raySources: readonly XBRaySourceLike[];
|
|
70
|
+
directTouches: readonly XBDirectTouchLike[];
|
|
71
|
+
}
|
|
72
|
+
/** The slice of the XR Blocks singletons the provider binds to. */
|
|
73
|
+
export interface XRBlocksContext {
|
|
74
|
+
/** `xb.input` - must expose `getFrame()`. */
|
|
75
|
+
input: {
|
|
76
|
+
getFrame(): XBFrameLike;
|
|
77
|
+
};
|
|
78
|
+
/** `xb.camera` / `xb.core.camera` - the head pose. */
|
|
79
|
+
camera: {
|
|
80
|
+
getWorldPosition(target: Vec3Like): Vec3Like;
|
|
81
|
+
getWorldQuaternion(target: QuatLike): QuatLike;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export declare class XRBlocksInputProvider implements InputProvider {
|
|
85
|
+
private readonly context;
|
|
86
|
+
private capabilities;
|
|
87
|
+
private readonly capsListeners;
|
|
88
|
+
constructor(context: XRBlocksContext);
|
|
89
|
+
getCapabilities(): InputCapabilities;
|
|
90
|
+
onCapabilitiesChanged(listener: (c: InputCapabilities) => void): Unsubscribe;
|
|
91
|
+
onSourcesChanged(): Unsubscribe;
|
|
92
|
+
sample(): readonly InputSourceSnapshot[];
|
|
93
|
+
private refreshCapabilities;
|
|
94
|
+
getHeadPose(): HeadPose;
|
|
95
|
+
}
|
|
96
|
+
export {};
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XRBlocksInputProvider - EXPERIMENTAL.
|
|
3
|
+
*
|
|
4
|
+
* Binds to the SHAPE of Google XR Blocks (verified against xrblocks
|
|
5
|
+
* v0.20.0 source, 2026-08) without importing `xrblocks`, per the
|
|
6
|
+
* UI Extensions structural-typing convention:
|
|
7
|
+
*
|
|
8
|
+
* - `xb.input.getFrame()` → `{ raySources, directTouches }`. Those slot
|
|
9
|
+
* objects are POOLED AND MUTATED per frame upstream - everything is
|
|
10
|
+
* copied here, never retained.
|
|
11
|
+
* - `RaySourceInput`: `{ controller, sourceType: 'mouse'|'controller-ray'
|
|
12
|
+
* |'hand-ray'|'gaze'|'simulator', ray, selected }` - handedness from
|
|
13
|
+
* `controller.inputSource?.handedness`; analog select only via
|
|
14
|
+
* `controller.gamepad?.buttons[0].value`; squeeze only as the boolean
|
|
15
|
+
* `controller.userData.squeezing`.
|
|
16
|
+
* - `DirectTouchInput`: `{ handIndex (0=left,1=right), point }` - the
|
|
17
|
+
* index-fingertip world position, merged into the matching hand source.
|
|
18
|
+
* - Grip poses are not exposed by the frame struct; the ray pose doubles
|
|
19
|
+
* as the carry pose (XR Blocks keeps grip transforms behind
|
|
20
|
+
* `options.controllers.visualization`).
|
|
21
|
+
* - Gaze rides `sourceType: 'gaze'` and never raw-selects (XR Blocks
|
|
22
|
+
* selects gaze via its own dwell; ours is the core's dwell).
|
|
23
|
+
* - Haptics: none in XR Blocks (verified) - capability stays false.
|
|
24
|
+
*/
|
|
25
|
+
import { NO_CAPABILITIES, } from "@realitycollective/webxr-input";
|
|
26
|
+
const SCRATCH_V = { x: 0, y: 0, z: 0 };
|
|
27
|
+
const SCRATCH_Q = { x: 0, y: 0, z: 0, w: 1 };
|
|
28
|
+
export class XRBlocksInputProvider {
|
|
29
|
+
context;
|
|
30
|
+
capabilities = { ...NO_CAPABILITIES, headPose: true, gaze: true };
|
|
31
|
+
capsListeners = new Set();
|
|
32
|
+
constructor(context) {
|
|
33
|
+
this.context = context;
|
|
34
|
+
}
|
|
35
|
+
getCapabilities() {
|
|
36
|
+
return this.capabilities;
|
|
37
|
+
}
|
|
38
|
+
onCapabilitiesChanged(listener) {
|
|
39
|
+
this.capsListeners.add(listener);
|
|
40
|
+
return () => this.capsListeners.delete(listener);
|
|
41
|
+
}
|
|
42
|
+
onSourcesChanged() {
|
|
43
|
+
return () => undefined;
|
|
44
|
+
}
|
|
45
|
+
sample() {
|
|
46
|
+
const frame = this.context.input.getFrame();
|
|
47
|
+
const snapshots = [];
|
|
48
|
+
const byHandedness = new Map();
|
|
49
|
+
let sawRay = false;
|
|
50
|
+
let sawTouch = false;
|
|
51
|
+
let sawMouse = false;
|
|
52
|
+
for (const source of frame.raySources) {
|
|
53
|
+
if (source.sourceType === "gaze") {
|
|
54
|
+
snapshots.push({
|
|
55
|
+
id: "xb-gaze",
|
|
56
|
+
kind: "gaze",
|
|
57
|
+
handedness: "none",
|
|
58
|
+
ray: copyRay(source.ray),
|
|
59
|
+
select: 0, // gaze never raw-selects in XR Blocks
|
|
60
|
+
squeeze: 0,
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const handedness = normalizeHandedness(source.controller.inputSource?.handedness);
|
|
65
|
+
const isMouse = source.sourceType === "mouse";
|
|
66
|
+
if (isMouse)
|
|
67
|
+
sawMouse = true;
|
|
68
|
+
else
|
|
69
|
+
sawRay = true;
|
|
70
|
+
const analog = source.controller.gamepad?.buttons?.[0]?.value;
|
|
71
|
+
const select = analog !== undefined && analog > 0 ? analog : source.selected ? 1 : 0;
|
|
72
|
+
const ray = copyRay(source.ray);
|
|
73
|
+
const snapshot = {
|
|
74
|
+
id: isMouse ? "xb-mouse" : `xb-${handedness}-${source.sourceType}`,
|
|
75
|
+
kind: isMouse ? "pointer2d" : source.sourceType === "hand-ray" ? "hand" : "controller",
|
|
76
|
+
handedness,
|
|
77
|
+
ray,
|
|
78
|
+
// XR Blocks does not expose grip transforms in the frame struct;
|
|
79
|
+
// the ray pose doubles as the carry pose.
|
|
80
|
+
gripPose: { position: ray.origin, quaternion: [0, 0, 0, 1] },
|
|
81
|
+
select,
|
|
82
|
+
squeeze: source.controller.userData?.squeezing ? 1 : 0,
|
|
83
|
+
};
|
|
84
|
+
snapshots.push(snapshot);
|
|
85
|
+
if (handedness !== "none")
|
|
86
|
+
byHandedness.set(handedness, snapshot);
|
|
87
|
+
}
|
|
88
|
+
for (const touch of frame.directTouches) {
|
|
89
|
+
sawTouch = true;
|
|
90
|
+
const handedness = touch.handIndex === 0 ? "left" : "right";
|
|
91
|
+
const existing = byHandedness.get(handedness);
|
|
92
|
+
const tip = [touch.point.x, touch.point.y, touch.point.z];
|
|
93
|
+
if (existing) {
|
|
94
|
+
existing.indexTip = tip;
|
|
95
|
+
existing.kind = "hand";
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
snapshots.push({
|
|
99
|
+
id: `xb-${handedness}-touch`,
|
|
100
|
+
kind: "hand",
|
|
101
|
+
handedness,
|
|
102
|
+
indexTip: tip,
|
|
103
|
+
gripPose: { position: tip, quaternion: [0, 0, 0, 1] },
|
|
104
|
+
select: touch.selected ? 1 : 0,
|
|
105
|
+
squeeze: 0,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.refreshCapabilities(sawRay, sawTouch, sawMouse);
|
|
110
|
+
return snapshots;
|
|
111
|
+
}
|
|
112
|
+
refreshCapabilities(rays, touches, mouse) {
|
|
113
|
+
const next = {
|
|
114
|
+
...NO_CAPABILITIES,
|
|
115
|
+
rays,
|
|
116
|
+
pokes: touches,
|
|
117
|
+
grabs: rays || touches ? "poseOnly" : "none",
|
|
118
|
+
handJoints: touches,
|
|
119
|
+
pinch: touches,
|
|
120
|
+
gaze: true,
|
|
121
|
+
pointer2d: mouse,
|
|
122
|
+
headPose: true,
|
|
123
|
+
haptics: false, // xrblocks v0.20.0 has no haptics API
|
|
124
|
+
};
|
|
125
|
+
if (JSON.stringify(next) !== JSON.stringify(this.capabilities)) {
|
|
126
|
+
this.capabilities = next;
|
|
127
|
+
for (const listener of [...this.capsListeners])
|
|
128
|
+
listener(next);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
getHeadPose() {
|
|
132
|
+
const position = this.context.camera.getWorldPosition(SCRATCH_V);
|
|
133
|
+
const quaternion = this.context.camera.getWorldQuaternion(SCRATCH_Q);
|
|
134
|
+
return {
|
|
135
|
+
position: [position.x, position.y, position.z],
|
|
136
|
+
quaternion: [quaternion.x, quaternion.y, quaternion.z, quaternion.w ?? 1],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function normalizeHandedness(value) {
|
|
141
|
+
return value === "left" || value === "right" ? value : "none";
|
|
142
|
+
}
|
|
143
|
+
function copyRay(ray) {
|
|
144
|
+
// Upstream slot objects are pooled - copy, never retain.
|
|
145
|
+
return {
|
|
146
|
+
origin: [ray.origin.x, ray.origin.y, ray.origin.z],
|
|
147
|
+
direction: [ray.direction.x, ray.direction.y, ray.direction.z],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * XRBlocksInputProvider - EXPERIMENTAL.\n *\n * Binds to the SHAPE of Google XR Blocks (verified against xrblocks\n * v0.20.0 source, 2026-08) without importing `xrblocks`, per the\n * UI Extensions structural-typing convention:\n *\n * - `xb.input.getFrame()` → `{ raySources, directTouches }`. Those slot\n * objects are POOLED AND MUTATED per frame upstream - everything is\n * copied here, never retained.\n * - `RaySourceInput`: `{ controller, sourceType: 'mouse'|'controller-ray'\n * |'hand-ray'|'gaze'|'simulator', ray, selected }` - handedness from\n * `controller.inputSource?.handedness`; analog select only via\n * `controller.gamepad?.buttons[0].value`; squeeze only as the boolean\n * `controller.userData.squeezing`.\n * - `DirectTouchInput`: `{ handIndex (0=left,1=right), point }` - the\n * index-fingertip world position, merged into the matching hand source.\n * - Grip poses are not exposed by the frame struct; the ray pose doubles\n * as the carry pose (XR Blocks keeps grip transforms behind\n * `options.controllers.visualization`).\n * - Gaze rides `sourceType: 'gaze'` and never raw-selects (XR Blocks\n * selects gaze via its own dwell; ours is the core's dwell).\n * - Haptics: none in XR Blocks (verified) - capability stays false.\n */\nimport {\n NO_CAPABILITIES,\n type HeadPose,\n type InputCapabilities,\n type InputProvider,\n type InputSourceSnapshot,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\n\n/** Structural slice of THREE.Vector3 / Quaternion. */\ninterface Vec3Like {\n x: number;\n y: number;\n z: number;\n}\ninterface QuatLike extends Vec3Like {\n w: number;\n}\n\ninterface RayLike {\n origin: Vec3Like;\n direction: Vec3Like;\n}\n\ninterface ControllerLike {\n inputSource?: { handedness?: string };\n gamepad?: { buttons?: ReadonlyArray<{ value: number }> };\n userData?: { squeezing?: boolean; selected?: boolean; id?: number };\n}\n\n/** Structural `RaySourceInput` (xrblocks src/interaction/InteractionTypes.ts). */\nexport interface XBRaySourceLike {\n controller: ControllerLike;\n sourceType: string; // 'mouse' | 'controller-ray' | 'hand-ray' | 'gaze' | 'simulator'\n ray: RayLike;\n selected: boolean;\n}\n\n/** Structural `DirectTouchInput`. */\nexport interface XBDirectTouchLike {\n controller: ControllerLike;\n handIndex: number; // 0 = LEFT, 1 = RIGHT (xrblocks Handedness enum)\n point: Vec3Like;\n selected: boolean;\n}\n\nexport interface XBFrameLike {\n raySources: readonly XBRaySourceLike[];\n directTouches: readonly XBDirectTouchLike[];\n}\n\n/** The slice of the XR Blocks singletons the provider binds to. */\nexport interface XRBlocksContext {\n /** `xb.input` - must expose `getFrame()`. */\n input: { getFrame(): XBFrameLike };\n /** `xb.camera` / `xb.core.camera` - the head pose. */\n camera: {\n getWorldPosition(target: Vec3Like): Vec3Like;\n getWorldQuaternion(target: QuatLike): QuatLike;\n };\n}\n\nconst SCRATCH_V = { x: 0, y: 0, z: 0 };\nconst SCRATCH_Q = { x: 0, y: 0, z: 0, w: 1 };\n\nexport class XRBlocksInputProvider implements InputProvider {\n private readonly context: XRBlocksContext;\n private capabilities: InputCapabilities = { ...NO_CAPABILITIES, headPose: true, gaze: true };\n private readonly capsListeners = new Set<(c: InputCapabilities) => void>();\n\n constructor(context: XRBlocksContext) {\n this.context = context;\n }\n\n getCapabilities(): InputCapabilities {\n return this.capabilities;\n }\n\n onCapabilitiesChanged(listener: (c: InputCapabilities) => void): Unsubscribe {\n this.capsListeners.add(listener);\n return () => this.capsListeners.delete(listener);\n }\n\n onSourcesChanged(): Unsubscribe {\n return () => undefined;\n }\n\n sample(): readonly InputSourceSnapshot[] {\n const frame = this.context.input.getFrame();\n const snapshots: InputSourceSnapshot[] = [];\n const byHandedness = new Map<string, InputSourceSnapshot>();\n\n let sawRay = false;\n let sawTouch = false;\n let sawMouse = false;\n\n for (const source of frame.raySources) {\n if (source.sourceType === \"gaze\") {\n snapshots.push({\n id: \"xb-gaze\",\n kind: \"gaze\",\n handedness: \"none\",\n ray: copyRay(source.ray),\n select: 0, // gaze never raw-selects in XR Blocks\n squeeze: 0,\n });\n continue;\n }\n const handedness = normalizeHandedness(source.controller.inputSource?.handedness);\n const isMouse = source.sourceType === \"mouse\";\n if (isMouse) sawMouse = true;\n else sawRay = true;\n const analog = source.controller.gamepad?.buttons?.[0]?.value;\n const select = analog !== undefined && analog > 0 ? analog : source.selected ? 1 : 0;\n const ray = copyRay(source.ray);\n const snapshot: InputSourceSnapshot = {\n id: isMouse ? \"xb-mouse\" : `xb-${handedness}-${source.sourceType}`,\n kind: isMouse ? \"pointer2d\" : source.sourceType === \"hand-ray\" ? \"hand\" : \"controller\",\n handedness,\n ray,\n // XR Blocks does not expose grip transforms in the frame struct;\n // the ray pose doubles as the carry pose.\n gripPose: { position: ray.origin, quaternion: [0, 0, 0, 1] },\n select,\n squeeze: source.controller.userData?.squeezing ? 1 : 0,\n };\n snapshots.push(snapshot);\n if (handedness !== \"none\") byHandedness.set(handedness, snapshot);\n }\n\n for (const touch of frame.directTouches) {\n sawTouch = true;\n const handedness = touch.handIndex === 0 ? \"left\" : \"right\";\n const existing = byHandedness.get(handedness);\n const tip: [number, number, number] = [touch.point.x, touch.point.y, touch.point.z];\n if (existing) {\n existing.indexTip = tip;\n existing.kind = \"hand\";\n } else {\n snapshots.push({\n id: `xb-${handedness}-touch`,\n kind: \"hand\",\n handedness,\n indexTip: tip,\n gripPose: { position: tip, quaternion: [0, 0, 0, 1] },\n select: touch.selected ? 1 : 0,\n squeeze: 0,\n });\n }\n }\n\n this.refreshCapabilities(sawRay, sawTouch, sawMouse);\n return snapshots;\n }\n\n private refreshCapabilities(rays: boolean, touches: boolean, mouse: boolean): void {\n const next: InputCapabilities = {\n ...NO_CAPABILITIES,\n rays,\n pokes: touches,\n grabs: rays || touches ? \"poseOnly\" : \"none\",\n handJoints: touches,\n pinch: touches,\n gaze: true,\n pointer2d: mouse,\n headPose: true,\n haptics: false, // xrblocks v0.20.0 has no haptics API\n };\n if (JSON.stringify(next) !== JSON.stringify(this.capabilities)) {\n this.capabilities = next;\n for (const listener of [...this.capsListeners]) listener(next);\n }\n }\n\n getHeadPose(): HeadPose {\n const position = this.context.camera.getWorldPosition(SCRATCH_V);\n const quaternion = this.context.camera.getWorldQuaternion(SCRATCH_Q);\n return {\n position: [position.x, position.y, position.z],\n quaternion: [quaternion.x, quaternion.y, quaternion.z, quaternion.w ?? 1],\n };\n }\n}\n\nfunction normalizeHandedness(value: string | undefined): \"left\" | \"right\" | \"none\" {\n return value === \"left\" || value === \"right\" ? value : \"none\";\n}\n\nfunction copyRay(ray: RayLike): {\n origin: [number, number, number];\n direction: [number, number, number];\n} {\n // Upstream slot objects are pooled - copy, never retain.\n return {\n origin: [ray.origin.x, ray.origin.y, ray.origin.z],\n direction: [ray.direction.x, ray.direction.y, ray.direction.z],\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@realitycollective/xrblocks-interactions",
|
|
3
|
+
"version": "0.1.0-preview.0",
|
|
4
|
+
"description": "EXPERIMENTAL Google XR Blocks adapter for the Reality Collective Interaction Extensions - maps XR Blocks' input pipeline (Input.getFrame() ray sources + direct touches, Script select events) into the engine-free @realitycollective/webxr-interactions core, reusing the three.js hit-testing/transform machinery. Structurally typed: no xrblocks import. Re-exports the core.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"realitycollective",
|
|
7
|
+
"xrblocks",
|
|
8
|
+
"androidxr",
|
|
9
|
+
"webxr",
|
|
10
|
+
"threejs",
|
|
11
|
+
"interaction",
|
|
12
|
+
"input",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Reality Collective",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"CHANGELOG.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@realitycollective/webxr-input": "^0.1.0",
|
|
37
|
+
"@realitycollective/webxr-interactions": "^0.1.0-preview.0",
|
|
38
|
+
"@realitycollective/threejs-interactions": "^0.1.0-preview.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"three": ">=0.170.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"three": "npm:super-three@0.181.0",
|
|
45
|
+
"@types/three": "*"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/realitycollective/WebXR-Interactions.git",
|
|
50
|
+
"directory": "packages/xrblocks-interactions"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/realitycollective/WebXR-Interactions#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/realitycollective/WebXR-Interactions/issues"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=20.19.0 <21.0.0-0 || >=22.12.0 <23.0.0-0 || >=24.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|