@realitycollective/native-interactions 0.1.1-preview.2 → 0.1.1-preview.3

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 CHANGED
@@ -8,6 +8,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
8
8
 
9
9
  ### Added
10
10
 
11
+ - `@realitycollective/webxr-interactions` - the near-pointer rule, IWSDK 1.0.0's pointer stack restated as one core decision every binding runs (`near-pointer.ts`, `NEAR_POINTER_DEFAULTS`): a fingertip (or a controller's ray origin) within 0.15 m of a target's surface owns the source as the touch pointer (hysteresis out to 0.2 m), presses at 0.02 m arriving from outside and releases on the way out, never pressing from inside or behind; the grip is a 0.07 m grab sphere; per source the first of touch, grab, ray with a candidate is the active pointer, locked while it presses or grabs; and `InteractionRuntime` publishes per-source `PointerVisuals` (`onPointerVisuals`, `getPointerVisuals`, `getActivePointer`): the ray and its cursor show only while the ray owns the source or nothing does, with the cursor at the active pointer's hit. `InteractableDescriptor.pokeRadius` is now the touch hover enter distance for that target (default 0.15 m, was a 0.05 m proximity trigger). `nearPointerContractCases()` is the shared suite, run on every platform's hit tester.
12
+ - `@realitycollective/webxr-interactions` - the physics contract (`physics.ts`): `PhysicsFacility`, the platform physics behind one set of semantics and IWSDK 1.0.0's defaults (`PHYSICS_DEFAULTS`: gravity `[0, -9.81, 0]`, 60 steps per second with interpolation, a dynamic body with no damping and gravity factor 1, an auto shape with density 1, restitution 0 and friction 0.5), static and kinematic states, suspend for a hold, resume with the release velocity, teleport with velocity cleared. `physicsFacilityContractCases()` proves a facility falls and rests, bounces, holds, throws, teleports and disposes; `MemoryPhysicsFacility` is the engine-free reference. Every binding names a default engine and takes an app's own facility instead: `createThreeInteractions({ physics: { rapier } })` (Rapier, `RapierPhysicsFacility`, proved against the real engine), `connectXRBlocksInteractions({ physics: { rapier } })` (the Rapier XR Blocks bundles), `registerInteractions` (Havok through IWSDK's components, `IWSDKPhysicsFacility`), `createBabylonInteractions({ physics: { kit } })` (Havok, `BabylonPhysicsFacility`, proved against a fake), and the native `physics` slice (`NativePhysicsHost`, Jolt on Quest and Android, RealityKit on visionOS, with every obligation stated in `native-types.ts` and the whole shared suite in the host conformance kit). A registered object with a body drives the held pose through its facility; `register` takes `body` and `shape` specs.
13
+ - `@realitycollective/native-interactions` - `NativeInputHost.applyPointerVisuals(sourceId, visuals)`: the host is told each frame whether to draw a source's ray and where its cursor sits, and draws exactly that; the earlier rule that the host draws a cursor at every ray hit is withdrawn. The kit gains `input/the ray and cursor are drawn exactly as told`, the whole `nearPointerContractCases()` suite over the host's own queries, and the physics cases.
14
+ - `@realitycollective/iwsdk-interactions` - `test/near-pointer-reference.test.ts` reads the touch and grab constants off IWSDK 1.0.0's own pointer classes and drives the core touch machine beside `@pmndrs/pointer-events`' press rule, recording the one frame they differ on by design (a fingertip first seen inside the press band).
15
+ - `@realitycollective/xrblocks-interactions` - the provider reads the XR Blocks camera's head pose through real three.js scratch objects; a plain object handed to `getWorldPosition` threw against the real camera.
16
+ - Eye gaze and pinch, on every platform, following Meta IWSDK 1.0.0's gaze-and-pinch interaction (the primary input on Meta VR Glasses). The provider half of the rule is `@realitycollective/webxr-input` 0.1.7's `EyeGazeInput`; this repository applies it in every provider and adds the targeting half to the core. `@realitycollective/webxr-interactions` - `HitTester` gains an optional `hitCone(ray, halfAngle, maxLength)`, IWSDK's `GazeConecaster` query (a target the ray reaches wins outright, otherwise the nearest to the axis within the cone, the nearer on a tie within half a degree), with `coneHitForSpheres` for sphere-based testers and `GazeConsensus`, the time-weighted dwell vote over IWSDK's 0.15 s window. `InteractionRuntime` takes `eyeGaze` options (`EYE_GAZE_DEFAULTS`: 5 degree cone, 30 m, 0.15 s, suppression on, follow the hand on) and, when the provider reports `capabilities.eyeGaze`, targets the gaze source through the cone and the consensus, targets nothing while a hand's near pointer is hovering or grabbing, satisfies `gaze.required` and dwell from the eye gaze, presses or grabs the gazed target when the owning hand pinches, and from the next frame drives the gaze source's ray from the pinching hand aimed at the gaze hit (`selectorPose`), so `onSample` listeners such as the pointer bridge and every behaviour see a drag that follows the hand. `hitTesterContractCases()` gains five `hitCone` cases that run when a tester has one. Without `eyeGaze` nothing changes: a gaze-kind source still targets by plain ray as head gaze does.
17
+ - `@realitycollective/iwsdk-interactions` - `IWSDKInputProvider` reports `eyeGaze` when the session carries a gaze input source, reads IWSDK's own sampled eye pose (`xrOrigin.eyeSpace` while `gazeOrigin` is `"tracked"`) and applies `EyeGazeInput`, so it hands far targeting to gaze exactly when IWSDK's `GazePointer` does; `EntityHitTester` answers `hitCone` over its spheres. `test/gaze-reference.test.ts` drives IWSDK's `FilteredEyeGaze` and `GazeConecaster` side by side with the core's filter and consensus.
18
+ - `@realitycollective/threejs-interactions` (reused by `@realitycollective/xrblocks-interactions`) - `WebXRInputProvider` finds the `targetRayMode === "gaze"` source, poses it against the reference space and applies `EyeGazeInput`; `ThreeHitTester.hitCone` tries the raycast first and then the cone over the registered roots' bounding spheres.
19
+ - `@realitycollective/babylon-interactions` - `BabylonInputProvider` reads the `xr-eye-tracking` feature (`WebXREyeTracking.getEyeGaze()`) when the app enabled it and applies `EyeGazeInput`; `BabylonHitTester.hitCone` runs the cone over its spheres.
20
+ - `@realitycollective/native-interactions` - `NativeInputFacts.eyeTracking` (an eye-gaze source is present: OpenXR `XR_EXT_eye_gaze_interaction` bound and active) and `NativeInputHost.getEyeGazePose()` (this frame's gaze target-ray pose, world space, or null) let a native host report the facts while the binding applies the rule: `eyeGaze` is derived, far rays are dropped, the pinch selects, and the selector pose is built from the hand's ray. The `interactions` slice gains an optional `hitCone` the host answers over its own scene, forwarded by `NativeHitTester`. The host conformance kit gains `input/eye gaze` cases: a host that reports eye tracking must pose the gaze, and a hidden target never answers a cone query.
21
+
11
22
  - `@realitycollective/webxr-interactions` - three `hitTesterContractCases()` that hold every platform to IWSDK's hit-test semantics: `hitProximity` reports the distance to the target's SURFACE, clamped at zero (a point 3 cm outside a 10 cm target reports 0.03 within 1 mm, not 0.13); it misses a target whose surface is farther than the query radius; and a ray that passes a target farther than its radius misses it. Every platform in the repository already passed them. The `HitTester` and `TransformPort.setEffect` docs now state those semantics, and the effect rule: `scale` multiplies the rest scale, `emissive` adds to the base intensity, and the last write wins.
12
23
  - `@realitycollective/native-interactions` - `nativeInteractionsHostConformanceCases()`, the host conformance kit a native app runs on its device against its real slices: the core's whole `hitTesterContractCases()` run against the host itself, the 0.1 m default target radius, the release velocity reaching `endHold`, live-session capabilities, presence hiding the models only per side, `"auto"` following the live modality, and presence never hiding the cursors. The host provides the test readbacks of the new `NativeInteractionsTestHost` type. The kit fails a host that measures proximity to a target's centre, ignores the radius it is handed, loses a release velocity, or draws the wrong presence.
13
24
  - `@realitycollective/webxr-interactions` - shared conformance suites for `HitTester` and `TransformPort`, `hitTesterContractCases()` and `transformPortContractCases()`, alongside the existing `inputProviderContractCases()` from `@realitycollective/webxr-input`. Runner-free data, the same pattern as WebXR-UIExtensions' `windowHostContractCases()`: each case throws a plain `Error` naming what a platform's ray/proximity query or transform read/write broke, and asserts only what `ports.ts` documents - a ray or proximity query answers null or an id with a positive distance and a point near the query, the nearer of two targets wins, an offset round-trips, `getWorldPose()` is finite with a unit quaternion, `getRestWorldPose()` is the registration pose and stays put, `getWorldPose()` follows `setLocalOffset`, `setLocalRotation` and `setWorldPose` from that rest frame, every returned tuple is a fresh value the caller owns and no tuple passed in is kept, and the optional `setEffect` accepts its shape without throwing. Each platform's subject starts away from the origin and turned, so a port that ignores its rest frame fails. Run against all five platforms in `packages/iwsdk-interactions/test/port-parity.test.ts`, next to the provider parity suite; every platform passes every case.
@@ -1,11 +1,13 @@
1
- import type { NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost } from "./native-types.js";
1
+ import type { NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost, NativePhysicsHost } from "./native-types.js";
2
2
  /** The real slices under test, and their readbacks. */
3
3
  export interface NativeInteractionsHostConformanceSetup {
4
4
  input: NativeInputHost;
5
5
  interactions: NativeInteractionHost;
6
6
  testHost: NativeInteractionsTestHost;
7
+ /** The `physics` slice. A host that installs none fails the physics cases. */
8
+ physics?: NativePhysicsHost;
7
9
  }
8
- /** One check a native host must pass. `name` is `interactions/<row>` or `input/<row>`. */
10
+ /** One check a native host must pass. `name` is `interactions/<row>`, `input/<row>` or `physics/<row>`. */
9
11
  export interface NativeInteractionsHostConformanceCase {
10
12
  name: string;
11
13
  run(setup: NativeInteractionsHostConformanceSetup): Promise<void>;
@@ -15,9 +15,10 @@
15
15
  * Runner-free, like the core suites: a case resolves on success and rejects
16
16
  * with a plain `Error` naming its row otherwise.
17
17
  */
18
- import { hitTesterContractCases, } from "@realitycollective/webxr-interactions";
18
+ import { hitTesterContractCases, nearPointerContractCases, physicsFacilityContractCases, } from "@realitycollective/webxr-interactions";
19
19
  import { NativeHitTester } from "./hit-tester.js";
20
20
  import { NativeInteractions } from "./host.js";
21
+ import { NativePhysicsFacility } from "./physics-facility.js";
21
22
  import { NativeInputProvider } from "./provider.js";
22
23
  import { NativeTransformPort } from "./transform-port.js";
23
24
  function fail(name, message) {
@@ -46,8 +47,101 @@ export function nativeInteractionsHostConformanceCases() {
46
47
  testHost.clearTargets();
47
48
  }
48
49
  }));
50
+ // The near-pointer rule over the host's real queries: touch press, grip
51
+ // grab, pointer priority and what the ray and cursor should show.
52
+ const nearCases = nearPointerContractCases().map((contractCase) => hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {
53
+ testHost.clearTargets();
54
+ try {
55
+ contractCase.run({
56
+ hitTester: new NativeHitTester({ interactions }),
57
+ driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },
58
+ });
59
+ }
60
+ catch (error) {
61
+ fail(name, error.message);
62
+ }
63
+ finally {
64
+ testHost.clearTargets();
65
+ }
66
+ }));
67
+ // The platform's physics behind the core contract: the whole shared suite
68
+ // over the host's engine, on the device.
69
+ const physicsCases = physicsFacilityContractCases().map((contractCase) => hostCase(`physics/${contractCase.name}`, ({ physics }, name) => {
70
+ if (!physics)
71
+ fail(name, "the host installs no physics slice; every native platform names a default engine (Jolt on Quest and Android, RealityKit on visionOS)");
72
+ const facility = new NativePhysicsFacility({ physics });
73
+ try {
74
+ contractCase.run({ facility });
75
+ }
76
+ catch (error) {
77
+ fail(name, error.message);
78
+ }
79
+ finally {
80
+ facility.dispose();
81
+ }
82
+ }));
49
83
  return [
50
84
  ...hitCases,
85
+ ...nearCases,
86
+ ...physicsCases,
87
+ hostCase("physics/a registered target with a body is held and released through the physics slice", ({ input, interactions, physics, testHost }, name) => {
88
+ if (!physics)
89
+ fail(name, "the host installs no physics slice");
90
+ testHost.clearTargets();
91
+ const setup = new NativeInteractions({ input, interactions, physics });
92
+ try {
93
+ testHost.placeTarget("rc-kit-body", [0, 1, -1], 0.1);
94
+ const port = setup.register({ id: "rc-kit-body", behaviours: [{ kind: "grab" }] }, { shape: { kind: "sphere", dimensions: [0.1, 0, 0] } });
95
+ if (!physics.hasBody("rc-kit-body"))
96
+ fail(name, "register with a shape must add a body to the physics slice");
97
+ port.beginHold();
98
+ if (!physics.isSuspended("rc-kit-body"))
99
+ fail(name, "beginHold on a target with a body must suspend it in the physics slice");
100
+ port.endHold({ linearVelocity: [1, 0, 0], angularVelocity: [0, 0, 0] });
101
+ if (physics.isSuspended("rc-kit-body"))
102
+ fail(name, "endHold must resume the body");
103
+ const velocity = physics.getVelocity("rc-kit-body").linear;
104
+ if (Math.abs(velocity[0] - 1) > 0.01)
105
+ fail(name, `endHold must hand the body the release velocity, it has ${JSON.stringify(velocity)}`);
106
+ setup.unregister("rc-kit-body");
107
+ if (physics.hasBody("rc-kit-body"))
108
+ fail(name, "unregister must remove the body the binding added");
109
+ }
110
+ finally {
111
+ setup.dispose();
112
+ testHost.clearTargets();
113
+ }
114
+ }),
115
+ hostCase("input/the ray and cursor are drawn exactly as told", ({ input, testHost }, name) => {
116
+ // The binding decides the ray and cursor per source (the near-pointer
117
+ // rule) and hands the host the result; a host that draws its own ray
118
+ // or a cursor at every ray hit fails here, as the Quest host did when
119
+ // it kept the cursor on a touched object.
120
+ if (typeof input.applyPointerVisuals !== "function") {
121
+ fail(name, "the host has no applyPointerVisuals, so it cannot hide the ray while a hand touches something");
122
+ }
123
+ if (typeof testHost.pointerVisuals !== "function") {
124
+ fail(name, "the test host has no pointerVisuals readback");
125
+ }
126
+ const touched = { sourceId: "rc-kit-source", activePointer: "touch", ray: false, cursor: true, cursorPoint: [0, 1, -0.9] };
127
+ input.applyPointerVisuals("rc-kit-source", { ...touched });
128
+ const drawn = testHost.pointerVisuals("rc-kit-source");
129
+ if (!drawn || drawn.ray || !drawn.cursor || JSON.stringify(drawn.cursorPoint) !== JSON.stringify(touched.cursorPoint)) {
130
+ fail(name, `told ${JSON.stringify(touched)}, the host draws ${JSON.stringify(drawn)}`);
131
+ }
132
+ if (!testHost.cursors().some((point) => JSON.stringify(point) === JSON.stringify(touched.cursorPoint))) {
133
+ fail(name, "the host draws no cursor disc at the point it was told");
134
+ }
135
+ const idle = { sourceId: "rc-kit-source", activePointer: null, ray: true, cursor: false, cursorPoint: null };
136
+ input.applyPointerVisuals("rc-kit-source", { ...idle });
137
+ const after = testHost.pointerVisuals("rc-kit-source");
138
+ if (!after || !after.ray || after.cursor) {
139
+ fail(name, `told ${JSON.stringify(idle)}, the host draws ${JSON.stringify(after)}`);
140
+ }
141
+ if (testHost.cursors().some((point) => JSON.stringify(point) === JSON.stringify(touched.cursorPoint))) {
142
+ fail(name, "the host still draws the cursor disc after being told to stop");
143
+ }
144
+ }),
51
145
  hostCase("interactions/a registered target with no radius is a 10 cm sphere", ({ input, interactions, testHost }, name) => {
52
146
  testHost.clearTargets();
53
147
  const setup = new NativeInteractions({ input, interactions });
@@ -132,6 +226,54 @@ export function nativeInteractionsHostConformanceCases() {
132
226
  provider.dispose();
133
227
  }
134
228
  }),
229
+ hostCase("input/eye gaze: a host that reports eye tracking poses the gaze, and the binding takes far targeting", ({ input }, name) => {
230
+ // Runs when the host reports eye tracking; a host without it (visionOS,
231
+ // a Quest without eye tracking) passes without running. With it, the
232
+ // host must hand the binding a valid pose in a live, focused session,
233
+ // and the binding then samples one gaze snapshot with a ray. That the
234
+ // hand and controller far rays drop is the shared contract case.
235
+ if (!input.getFacts().eyeTracking)
236
+ return;
237
+ if (typeof input.getEyeGazePose !== "function") {
238
+ fail(name, "the host reports eyeTracking but has no getEyeGazePose(); the binding cannot use a fact it cannot pose");
239
+ }
240
+ const provider = new NativeInputProvider({ input });
241
+ try {
242
+ if (!provider.getCapabilities().eyeGaze) {
243
+ fail(name, "the host reports eyeTracking in a live session, so capabilities.eyeGaze must be true; is the session focused?");
244
+ }
245
+ const pose = input.getEyeGazePose();
246
+ if (!pose || [...pose.position, ...pose.quaternion].some((n) => !Number.isFinite(n))) {
247
+ fail(name, `getEyeGazePose() returned ${JSON.stringify(pose)}; run the kit while the user's eyes are tracked`);
248
+ }
249
+ const gaze = provider.sample().find((source) => source.kind === "gaze");
250
+ if (!gaze?.ray)
251
+ fail(name, "with a valid gaze pose the binding must sample one gaze snapshot with a ray");
252
+ }
253
+ finally {
254
+ provider.dispose();
255
+ }
256
+ }),
257
+ hostCase("input/eye gaze: a cone query never answers with a hidden target", ({ interactions, testHost }, name) => {
258
+ if (!interactions.hitCone)
259
+ return;
260
+ testHost.clearTargets();
261
+ try {
262
+ testHost.placeTarget("rc-kit-cone", [0.2, 1, -2], 0.1);
263
+ const ray = { origin: [0, 1, 0], direction: [0, 0, -1] };
264
+ const shown = interactions.hitCone(ray, (5 * Math.PI) / 180, 30);
265
+ if (!shown || shown.targetId !== "rc-kit-cone") {
266
+ fail(name, "a target 0.2 m beside a ray 2 m out, inside a 5 degree cone, was not found");
267
+ }
268
+ testHost.setTargetVisible?.("rc-kit-cone", false);
269
+ if (testHost.setTargetVisible && interactions.hitCone(ray, (5 * Math.PI) / 180, 30) !== null) {
270
+ fail(name, "the cone query answered with a hidden target");
271
+ }
272
+ }
273
+ finally {
274
+ testHost.clearTargets();
275
+ }
276
+ }),
135
277
  hostCase("input/presence never hides the cursors", ({ input, testHost }, name) => {
136
278
  const provider = new NativeInputProvider({ input });
137
279
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Interactions host conformance kit: host cases (the third kind in the\n * Masters' \"Validation\" section), shipped as data so a native app runs them\n * against its REAL `input` and `interactions` slices on the device.\n *\n * The contract cases a native package ran before this checked shapes against\n * fakes, and passed while the real host measured proximity to a target's\n * centre, which removed every near interaction on the headset. These run the\n * same checks against the host itself, through the test readbacks of\n * `NativeInteractionsTestHost`: the core's whole `hitTesterContractCases()`\n * (surface distance included) over targets the host places, the default\n * target radius, the release velocity reaching `endHold`, and what the host\n * draws for presence. Run them inside a live, focused session.\n *\n * Runner-free, like the core suites: a case resolves on success and rejects\n * with a plain `Error` naming its row otherwise.\n */\nimport {\n hitTesterContractCases,\n type HoldRelease,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester } from \"./hit-tester.js\";\nimport { NativeInteractions } from \"./host.js\";\nimport { NativeInputProvider } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport type {\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePresenceShown,\n} from \"./native-types.js\";\n\n/** The real slices under test, and their readbacks. */\nexport interface NativeInteractionsHostConformanceSetup {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n testHost: NativeInteractionsTestHost;\n}\n\n/** One check a native host must pass. `name` is `interactions/<row>` or `input/<row>`. */\nexport interface NativeInteractionsHostConformanceCase {\n name: string;\n run(setup: NativeInteractionsHostConformanceSetup): Promise<void>;\n}\n\nfunction fail(name: string, message: string): never {\n throw new Error(`[${name}] ${message}`);\n}\n\nfunction hostCase(\n name: string,\n run: (setup: NativeInteractionsHostConformanceSetup, name: string) => void | Promise<void>,\n): NativeInteractionsHostConformanceCase {\n return { name, run: async (setup) => run(setup, name) };\n}\n\nfunction exactlyOneFamily(shown: NativePresenceShown | undefined): boolean {\n return shown !== undefined && shown.hand !== shown.controller;\n}\n\n/** The Interactions and Input host conformance cases. See the file comment. */\nexport function nativeInteractionsHostConformanceCases(): NativeInteractionsHostConformanceCase[] {\n const hitCases = hitTesterContractCases().map((contractCase) =>\n hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n contractCase.run({\n hitTester: new NativeHitTester({ interactions }),\n driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },\n });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n testHost.clearTargets();\n }\n }),\n );\n\n return [\n ...hitCases,\n hostCase(\"interactions/a registered target with no radius is a 10 cm sphere\", ({ input, interactions, testHost }, name) => {\n testHost.clearTargets();\n const setup = new NativeInteractions({ input, interactions });\n try {\n // Placed small, then registered with no radius: the registration\n // must make it a 0.1 m sphere, so a point 9 cm from its centre is\n // inside it.\n testHost.placeTarget(\"rc-kit-bare\", [0, 1, -1], 0.02);\n setup.register({ id: \"rc-kit-bare\", behaviours: [] });\n const hit = interactions.hitProximity([0.09, 1, -1], 0.001);\n if (!hit || hit.targetId !== \"rc-kit-bare\") {\n fail(name, \"a point 9 cm from a bare target's centre did not reach it; the host is not using the 0.1 m radius\");\n }\n } finally {\n setup.dispose();\n testHost.clearTargets();\n }\n }),\n hostCase(\"interactions/endHold hands the host the release velocity\", ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n testHost.placeTarget(\"rc-kit-throw\", [0, 1, -1], 0.1);\n const port = new NativeTransformPort(\"rc-kit-throw\", { interactions });\n const release: HoldRelease = { linearVelocity: [1, 2, 3], angularVelocity: [0, 0.5, 0] };\n port.beginHold();\n port.endHold(release);\n const received = testHost.lastRelease(\"rc-kit-throw\");\n if (JSON.stringify(received) !== JSON.stringify(release)) {\n fail(name, `the host received ${JSON.stringify(received)}, expected ${JSON.stringify(release)}`);\n }\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/capabilities in a live session: rays, pokes and pose-only grabs\", ({ input }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const caps = provider.getCapabilities();\n // Pose-only grabs follow from the same fact, so rays and pokes carry it.\n if (!caps.rays || !caps.pokes || caps.grabs !== \"poseOnly\") {\n fail(name, `a live session must report rays, pokes and pose-only grabs, got ${JSON.stringify(caps)}; run the kit inside a live session`);\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/presence hides the models only, per side\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n if (!provider.getCapabilities().presence || !provider.setPresenceVisible) {\n fail(name, \"the host has no applyPresence, so presence cannot be shown or hidden\");\n }\n provider.setPresenceVisible(\"left\", false);\n const left = testHost.presenceShown(\"left\");\n if (!left || left.hand || left.controller) fail(name, `a hidden left side still draws ${JSON.stringify(left)}`);\n if (!exactlyOneFamily(testHost.presenceShown(\"right\"))) {\n fail(name, `the right side must still draw exactly one family, drew ${JSON.stringify(testHost.presenceShown(\"right\"))}`);\n }\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n hostCase('input/\"auto\" shows the family the live session uses', ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n provider.setPresenceModality?.(\"auto\");\n const handJoints = provider.getCapabilities().handJoints;\n for (const side of [\"left\", \"right\"] as const) {\n const shown = testHost.presenceShown(side);\n if (!shown || shown.hand !== handJoints || shown.controller !== !handJoints) {\n fail(name, `${side} draws ${JSON.stringify(shown)} with hand joints ${handJoints ? \"live\" : \"off\"}`);\n }\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/presence never hides the cursors\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const before = testHost.cursors().length;\n provider.setPresenceVisible?.(\"all\", false);\n const after = testHost.cursors().length;\n if (after !== before) fail(name, `hiding presence changed the cursors drawn from ${before} to ${after}`);\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n ];\n}\n"]}
1
+ {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Interactions host conformance kit: host cases (the third kind in the\n * Masters' \"Validation\" section), shipped as data so a native app runs them\n * against its REAL `input` and `interactions` slices on the device.\n *\n * The contract cases a native package ran before this checked shapes against\n * fakes, and passed while the real host measured proximity to a target's\n * centre, which removed every near interaction on the headset. These run the\n * same checks against the host itself, through the test readbacks of\n * `NativeInteractionsTestHost`: the core's whole `hitTesterContractCases()`\n * (surface distance included) over targets the host places, the default\n * target radius, the release velocity reaching `endHold`, and what the host\n * draws for presence. Run them inside a live, focused session.\n *\n * Runner-free, like the core suites: a case resolves on success and rejects\n * with a plain `Error` naming its row otherwise.\n */\nimport {\n hitTesterContractCases,\n nearPointerContractCases,\n physicsFacilityContractCases,\n type HoldRelease,\n type PointerVisuals,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester } from \"./hit-tester.js\";\nimport { NativeInteractions } from \"./host.js\";\nimport { NativePhysicsFacility } from \"./physics-facility.js\";\nimport { NativeInputProvider } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type {\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePhysicsHost,\n NativePresenceShown,\n} from \"./native-types.js\";\n\n/** The real slices under test, and their readbacks. */\nexport interface NativeInteractionsHostConformanceSetup {\n input: NativeInputHost;\n interactions: NativeInteractionHost;\n testHost: NativeInteractionsTestHost;\n /** The `physics` slice. A host that installs none fails the physics cases. */\n physics?: NativePhysicsHost;\n}\n\n/** One check a native host must pass. `name` is `interactions/<row>`, `input/<row>` or `physics/<row>`. */\nexport interface NativeInteractionsHostConformanceCase {\n name: string;\n run(setup: NativeInteractionsHostConformanceSetup): Promise<void>;\n}\n\nfunction fail(name: string, message: string): never {\n throw new Error(`[${name}] ${message}`);\n}\n\nfunction hostCase(\n name: string,\n run: (setup: NativeInteractionsHostConformanceSetup, name: string) => void | Promise<void>,\n): NativeInteractionsHostConformanceCase {\n return { name, run: async (setup) => run(setup, name) };\n}\n\nfunction exactlyOneFamily(shown: NativePresenceShown | undefined): boolean {\n return shown !== undefined && shown.hand !== shown.controller;\n}\n\n/** The Interactions and Input host conformance cases. See the file comment. */\nexport function nativeInteractionsHostConformanceCases(): NativeInteractionsHostConformanceCase[] {\n const hitCases = hitTesterContractCases().map((contractCase) =>\n hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n contractCase.run({\n hitTester: new NativeHitTester({ interactions }),\n driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },\n });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n testHost.clearTargets();\n }\n }),\n );\n\n // The near-pointer rule over the host's real queries: touch press, grip\n // grab, pointer priority and what the ray and cursor should show.\n const nearCases = nearPointerContractCases().map((contractCase) =>\n hostCase(`interactions/${contractCase.name}`, ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n contractCase.run({\n hitTester: new NativeHitTester({ interactions }),\n driver: { place: (id, position, radius) => testHost.placeTarget(id, position, radius) },\n });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n testHost.clearTargets();\n }\n }),\n );\n\n // The platform's physics behind the core contract: the whole shared suite\n // over the host's engine, on the device.\n const physicsCases = physicsFacilityContractCases().map((contractCase) =>\n hostCase(`physics/${contractCase.name}`, ({ physics }, name) => {\n if (!physics) fail(name, \"the host installs no physics slice; every native platform names a default engine (Jolt on Quest and Android, RealityKit on visionOS)\");\n const facility = new NativePhysicsFacility({ physics });\n try {\n contractCase.run({ facility });\n } catch (error) {\n fail(name, (error as Error).message);\n } finally {\n facility.dispose();\n }\n }),\n );\n\n return [\n ...hitCases,\n ...nearCases,\n ...physicsCases,\n hostCase(\"physics/a registered target with a body is held and released through the physics slice\", ({ input, interactions, physics, testHost }, name) => {\n if (!physics) fail(name, \"the host installs no physics slice\");\n testHost.clearTargets();\n const setup = new NativeInteractions({ input, interactions, physics });\n try {\n testHost.placeTarget(\"rc-kit-body\", [0, 1, -1], 0.1);\n const port = setup.register({ id: \"rc-kit-body\", behaviours: [{ kind: \"grab\" }] }, { shape: { kind: \"sphere\", dimensions: [0.1, 0, 0] } });\n if (!physics.hasBody(\"rc-kit-body\")) fail(name, \"register with a shape must add a body to the physics slice\");\n port.beginHold();\n if (!physics.isSuspended(\"rc-kit-body\")) fail(name, \"beginHold on a target with a body must suspend it in the physics slice\");\n port.endHold({ linearVelocity: [1, 0, 0], angularVelocity: [0, 0, 0] });\n if (physics.isSuspended(\"rc-kit-body\")) fail(name, \"endHold must resume the body\");\n const velocity = physics.getVelocity(\"rc-kit-body\").linear;\n if (Math.abs(velocity[0] - 1) > 0.01) fail(name, `endHold must hand the body the release velocity, it has ${JSON.stringify(velocity)}`);\n setup.unregister(\"rc-kit-body\");\n if (physics.hasBody(\"rc-kit-body\")) fail(name, \"unregister must remove the body the binding added\");\n } finally {\n setup.dispose();\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/the ray and cursor are drawn exactly as told\", ({ input, testHost }, name) => {\n // The binding decides the ray and cursor per source (the near-pointer\n // rule) and hands the host the result; a host that draws its own ray\n // or a cursor at every ray hit fails here, as the Quest host did when\n // it kept the cursor on a touched object.\n if (typeof input.applyPointerVisuals !== \"function\") {\n fail(name, \"the host has no applyPointerVisuals, so it cannot hide the ray while a hand touches something\");\n }\n if (typeof testHost.pointerVisuals !== \"function\") {\n fail(name, \"the test host has no pointerVisuals readback\");\n }\n const touched: PointerVisuals = { sourceId: \"rc-kit-source\", activePointer: \"touch\", ray: false, cursor: true, cursorPoint: [0, 1, -0.9] };\n input.applyPointerVisuals(\"rc-kit-source\", { ...touched });\n const drawn = testHost.pointerVisuals(\"rc-kit-source\");\n if (!drawn || drawn.ray || !drawn.cursor || JSON.stringify(drawn.cursorPoint) !== JSON.stringify(touched.cursorPoint)) {\n fail(name, `told ${JSON.stringify(touched)}, the host draws ${JSON.stringify(drawn)}`);\n }\n if (!testHost.cursors().some((point) => JSON.stringify(point) === JSON.stringify(touched.cursorPoint))) {\n fail(name, \"the host draws no cursor disc at the point it was told\");\n }\n const idle: PointerVisuals = { sourceId: \"rc-kit-source\", activePointer: null, ray: true, cursor: false, cursorPoint: null };\n input.applyPointerVisuals(\"rc-kit-source\", { ...idle });\n const after = testHost.pointerVisuals(\"rc-kit-source\");\n if (!after || !after.ray || after.cursor) {\n fail(name, `told ${JSON.stringify(idle)}, the host draws ${JSON.stringify(after)}`);\n }\n if (testHost.cursors().some((point) => JSON.stringify(point) === JSON.stringify(touched.cursorPoint))) {\n fail(name, \"the host still draws the cursor disc after being told to stop\");\n }\n }),\n hostCase(\"interactions/a registered target with no radius is a 10 cm sphere\", ({ input, interactions, testHost }, name) => {\n testHost.clearTargets();\n const setup = new NativeInteractions({ input, interactions });\n try {\n // Placed small, then registered with no radius: the registration\n // must make it a 0.1 m sphere, so a point 9 cm from its centre is\n // inside it.\n testHost.placeTarget(\"rc-kit-bare\", [0, 1, -1], 0.02);\n setup.register({ id: \"rc-kit-bare\", behaviours: [] });\n const hit = interactions.hitProximity([0.09, 1, -1], 0.001);\n if (!hit || hit.targetId !== \"rc-kit-bare\") {\n fail(name, \"a point 9 cm from a bare target's centre did not reach it; the host is not using the 0.1 m radius\");\n }\n } finally {\n setup.dispose();\n testHost.clearTargets();\n }\n }),\n hostCase(\"interactions/endHold hands the host the release velocity\", ({ interactions, testHost }, name) => {\n testHost.clearTargets();\n try {\n testHost.placeTarget(\"rc-kit-throw\", [0, 1, -1], 0.1);\n const port = new NativeTransformPort(\"rc-kit-throw\", { interactions });\n const release: HoldRelease = { linearVelocity: [1, 2, 3], angularVelocity: [0, 0.5, 0] };\n port.beginHold();\n port.endHold(release);\n const received = testHost.lastRelease(\"rc-kit-throw\");\n if (JSON.stringify(received) !== JSON.stringify(release)) {\n fail(name, `the host received ${JSON.stringify(received)}, expected ${JSON.stringify(release)}`);\n }\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/capabilities in a live session: rays, pokes and pose-only grabs\", ({ input }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const caps = provider.getCapabilities();\n // Pose-only grabs follow from the same fact, so rays and pokes carry it.\n if (!caps.rays || !caps.pokes || caps.grabs !== \"poseOnly\") {\n fail(name, `a live session must report rays, pokes and pose-only grabs, got ${JSON.stringify(caps)}; run the kit inside a live session`);\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/presence hides the models only, per side\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n if (!provider.getCapabilities().presence || !provider.setPresenceVisible) {\n fail(name, \"the host has no applyPresence, so presence cannot be shown or hidden\");\n }\n provider.setPresenceVisible(\"left\", false);\n const left = testHost.presenceShown(\"left\");\n if (!left || left.hand || left.controller) fail(name, `a hidden left side still draws ${JSON.stringify(left)}`);\n if (!exactlyOneFamily(testHost.presenceShown(\"right\"))) {\n fail(name, `the right side must still draw exactly one family, drew ${JSON.stringify(testHost.presenceShown(\"right\"))}`);\n }\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n hostCase('input/\"auto\" shows the family the live session uses', ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n provider.setPresenceModality?.(\"auto\");\n const handJoints = provider.getCapabilities().handJoints;\n for (const side of [\"left\", \"right\"] as const) {\n const shown = testHost.presenceShown(side);\n if (!shown || shown.hand !== handJoints || shown.controller !== !handJoints) {\n fail(name, `${side} draws ${JSON.stringify(shown)} with hand joints ${handJoints ? \"live\" : \"off\"}`);\n }\n }\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/eye gaze: a host that reports eye tracking poses the gaze, and the binding takes far targeting\", ({ input }, name) => {\n // Runs when the host reports eye tracking; a host without it (visionOS,\n // a Quest without eye tracking) passes without running. With it, the\n // host must hand the binding a valid pose in a live, focused session,\n // and the binding then samples one gaze snapshot with a ray. That the\n // hand and controller far rays drop is the shared contract case.\n if (!input.getFacts().eyeTracking) return;\n if (typeof input.getEyeGazePose !== \"function\") {\n fail(name, \"the host reports eyeTracking but has no getEyeGazePose(); the binding cannot use a fact it cannot pose\");\n }\n const provider = new NativeInputProvider({ input });\n try {\n if (!provider.getCapabilities().eyeGaze) {\n fail(name, \"the host reports eyeTracking in a live session, so capabilities.eyeGaze must be true; is the session focused?\");\n }\n const pose = input.getEyeGazePose();\n if (!pose || [...pose.position, ...pose.quaternion].some((n) => !Number.isFinite(n))) {\n fail(name, `getEyeGazePose() returned ${JSON.stringify(pose)}; run the kit while the user's eyes are tracked`);\n }\n const gaze = provider.sample().find((source) => source.kind === \"gaze\");\n if (!gaze?.ray) fail(name, \"with a valid gaze pose the binding must sample one gaze snapshot with a ray\");\n } finally {\n provider.dispose();\n }\n }),\n hostCase(\"input/eye gaze: a cone query never answers with a hidden target\", ({ interactions, testHost }, name) => {\n if (!interactions.hitCone) return;\n testHost.clearTargets();\n try {\n testHost.placeTarget(\"rc-kit-cone\", [0.2, 1, -2], 0.1);\n const ray = { origin: [0, 1, 0] as Vec3Tuple, direction: [0, 0, -1] as Vec3Tuple };\n const shown = interactions.hitCone(ray, (5 * Math.PI) / 180, 30);\n if (!shown || shown.targetId !== \"rc-kit-cone\") {\n fail(name, \"a target 0.2 m beside a ray 2 m out, inside a 5 degree cone, was not found\");\n }\n testHost.setTargetVisible?.(\"rc-kit-cone\", false);\n if (testHost.setTargetVisible && interactions.hitCone(ray, (5 * Math.PI) / 180, 30) !== null) {\n fail(name, \"the cone query answered with a hidden target\");\n }\n } finally {\n testHost.clearTargets();\n }\n }),\n hostCase(\"input/presence never hides the cursors\", ({ input, testHost }, name) => {\n const provider = new NativeInputProvider({ input });\n try {\n const before = testHost.cursors().length;\n provider.setPresenceVisible?.(\"all\", false);\n const after = testHost.cursors().length;\n if (after !== before) fail(name, `hiding presence changed the cursors drawn from ${before} to ${after}`);\n } finally {\n provider.setPresenceVisible?.(\"all\", true);\n provider.dispose();\n }\n }),\n ];\n}\n"]}
@@ -1,9 +1,10 @@
1
1
  /**
2
- * NativeHitTester - resolves the core's ray and proximity queries against
3
- * the native host's `interactions` slice. The native app owns the scene and
4
- * physics, so targeting is entirely the host's answer; this class only
5
- * copies the result across and renames its `targetId` to the core's
6
- * `interactableId`.
2
+ * NativeHitTester - resolves the core's ray, proximity and cone queries
3
+ * against the native host's `interactions` slice. The native app owns the
4
+ * scene and physics, so targeting is entirely the host's answer; this class
5
+ * only copies the result across and renames its `targetId` to the core's
6
+ * `interactableId`. `hitCone` is present only when the host slice carries
7
+ * one, so the runtime falls back to `hitRay` for eye gaze on a host without.
7
8
  */
8
9
  import type { RayTuple, Vec3Tuple } from "@realitycollective/webxr-input";
9
10
  import type { HitTester, InteractableHit } from "@realitycollective/webxr-interactions";
@@ -14,6 +15,7 @@ export interface NativeHitTesterOptions {
14
15
  }
15
16
  export declare class NativeHitTester implements HitTester {
16
17
  private readonly host;
18
+ readonly hitCone?: (ray: RayTuple, halfAngle: number, maxLength: number) => InteractableHit | null;
17
19
  constructor(options?: NativeHitTesterOptions);
18
20
  hitRay(ray: RayTuple): InteractableHit | null;
19
21
  hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;
@@ -1,8 +1,16 @@
1
1
  import { copyRay, copyVec3, resolveHostSlice, } from "./native-types.js";
2
2
  export class NativeHitTester {
3
3
  host;
4
+ hitCone;
4
5
  constructor(options = {}) {
5
6
  this.host = resolveHostSlice("interactions", options.interactions);
7
+ const host = this.host;
8
+ if (host.hitCone) {
9
+ this.hitCone = (ray, halfAngle, maxLength) => {
10
+ const hit = host.hitCone(copyRay(ray), halfAngle, maxLength);
11
+ return hit ? toInteractableHit(hit) : null;
12
+ };
13
+ }
6
14
  }
7
15
  hitRay(ray) {
8
16
  const hit = this.host.hitRay(copyRay(ray));
@@ -1 +1 @@
1
- {"version":3,"file":"hit-tester.js","sourceRoot":"","sources":["../src/hit-tester.ts"],"names":[],"mappings":"AASA,OAAO,EACL,OAAO,EACP,QAAQ,EACR,gBAAgB,GAGjB,MAAM,mBAAmB,CAAC;AAO3B,MAAM,OAAO,eAAe;IACT,IAAI,CAAwB;IAE7C,YAAY,UAAkC,EAAE;QAC9C,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,CAAC,GAAa;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3C,OAAO,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;IAED,YAAY,CAAC,KAAgB,EAAE,MAAc;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QAC5D,OAAO,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;CACF;AAED,SAAS,iBAAiB,CAAC,GAAc;IACvC,OAAO,EAAE,cAAc,EAAE,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;AAC9F,CAAC","sourcesContent":["/**\n * NativeHitTester - resolves the core's ray and proximity queries against\n * the native host's `interactions` slice. The native app owns the scene and\n * physics, so targeting is entirely the host's answer; this class only\n * copies the result across and renames its `targetId` to the core's\n * `interactableId`.\n */\nimport type { RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HitTester, InteractableHit } from \"@realitycollective/webxr-interactions\";\nimport {\n copyRay,\n copyVec3,\n resolveHostSlice,\n type NativeHit,\n type NativeInteractionHost,\n} from \"./native-types.js\";\n\nexport interface NativeHitTesterOptions {\n /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */\n interactions?: NativeInteractionHost;\n}\n\nexport class NativeHitTester implements HitTester {\n private readonly host: NativeInteractionHost;\n\n constructor(options: NativeHitTesterOptions = {}) {\n this.host = resolveHostSlice(\"interactions\", options.interactions);\n }\n\n hitRay(ray: RayTuple): InteractableHit | null {\n const hit = this.host.hitRay(copyRay(ray));\n return hit ? toInteractableHit(hit) : null;\n }\n\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null {\n const hit = this.host.hitProximity(copyVec3(point), radius);\n return hit ? toInteractableHit(hit) : null;\n }\n}\n\nfunction toInteractableHit(hit: NativeHit): InteractableHit {\n return { interactableId: hit.targetId, distance: hit.distance, point: copyVec3(hit.point) };\n}\n"]}
1
+ {"version":3,"file":"hit-tester.js","sourceRoot":"","sources":["../src/hit-tester.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,OAAO,EACP,QAAQ,EACR,gBAAgB,GAGjB,MAAM,mBAAmB,CAAC;AAO3B,MAAM,OAAO,eAAe;IACT,IAAI,CAAwB;IACpC,OAAO,CAAmF;IAEnG,YAAY,UAAkC,EAAE;QAC9C,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;QACnE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE;gBAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;gBAC9D,OAAO,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC7C,CAAC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,CAAC,GAAa;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3C,OAAO,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;IAED,YAAY,CAAC,KAAgB,EAAE,MAAc;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QAC5D,OAAO,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;CACF;AAED,SAAS,iBAAiB,CAAC,GAAc;IACvC,OAAO,EAAE,cAAc,EAAE,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;AAC9F,CAAC","sourcesContent":["/**\n * NativeHitTester - resolves the core's ray, proximity and cone queries\n * against the native host's `interactions` slice. The native app owns the\n * scene and physics, so targeting is entirely the host's answer; this class\n * only copies the result across and renames its `targetId` to the core's\n * `interactableId`. `hitCone` is present only when the host slice carries\n * one, so the runtime falls back to `hitRay` for eye gaze on a host without.\n */\nimport type { RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport type { HitTester, InteractableHit } from \"@realitycollective/webxr-interactions\";\nimport {\n copyRay,\n copyVec3,\n resolveHostSlice,\n type NativeHit,\n type NativeInteractionHost,\n} from \"./native-types.js\";\n\nexport interface NativeHitTesterOptions {\n /** The `interactions` slice. Omit to read `globalThis.__rcHost.interactions`. */\n interactions?: NativeInteractionHost;\n}\n\nexport class NativeHitTester implements HitTester {\n private readonly host: NativeInteractionHost;\n readonly hitCone?: (ray: RayTuple, halfAngle: number, maxLength: number) => InteractableHit | null;\n\n constructor(options: NativeHitTesterOptions = {}) {\n this.host = resolveHostSlice(\"interactions\", options.interactions);\n const host = this.host;\n if (host.hitCone) {\n this.hitCone = (ray, halfAngle, maxLength) => {\n const hit = host.hitCone!(copyRay(ray), halfAngle, maxLength);\n return hit ? toInteractableHit(hit) : null;\n };\n }\n }\n\n hitRay(ray: RayTuple): InteractableHit | null {\n const hit = this.host.hitRay(copyRay(ray));\n return hit ? toInteractableHit(hit) : null;\n }\n\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null {\n const hit = this.host.hitProximity(copyVec3(point), radius);\n return hit ? toInteractableHit(hit) : null;\n }\n}\n\nfunction toInteractableHit(hit: NativeHit): InteractableHit {\n return { interactableId: hit.targetId, distance: hit.distance, point: copyVec3(hit.point) };\n}\n"]}
package/dist/host.d.ts CHANGED
@@ -14,13 +14,26 @@
14
14
  * app's own frame callback. Without it, call `update(dt)` yourself with
15
15
  * seconds.
16
16
  */
17
- import { InteractionRuntime, type DwellConfig, type InteractableDescriptor } from "@realitycollective/webxr-interactions";
17
+ import { InteractionRuntime, type DwellConfig, type InteractableDescriptor, type NearPointerOptions, type PhysicsBodySpec, type PhysicsShapeSpec } from "@realitycollective/webxr-interactions";
18
18
  import { NativeHitTester, type NativeHitTesterOptions } from "./hit-tester.js";
19
+ import { NativePhysicsFacility } from "./physics-facility.js";
19
20
  import { NativeInputProvider, type NativeInputProviderOptions } from "./provider.js";
20
21
  import { NativeTransformPort } from "./transform-port.js";
21
- import { type NativeFrameSource } from "./native-types.js";
22
+ import { type NativeFrameSource, type NativePhysicsHost } from "./native-types.js";
22
23
  export interface NativeInteractionsOptions extends NativeInputProviderOptions, NativeHitTesterOptions {
23
24
  dwellDefaults?: DwellConfig;
25
+ /** The near-pointer distances (touch hover, touch press, grab radius). Defaults are IWSDK 1.0.0's. */
26
+ nearPointer?: Partial<NearPointerOptions>;
27
+ /**
28
+ * The `physics` slice: the host's default engine (Jolt on Quest and
29
+ * Android, RealityKit on visionOS) behind the core contract, or an app's
30
+ * own `PhysicsFacility` in its place. Omit to read
31
+ * `globalThis.__rcHost.physics`. Without either, `physics` is null and no
32
+ * target can carry a body; the host conformance kit fails such a host.
33
+ */
34
+ physics?: NativePhysicsHost;
35
+ /** Step the physics from `update(dt)`. Default true; false when the host steps its engine itself. */
36
+ stepPhysics?: boolean;
24
37
  /**
25
38
  * Drive `update(dt)` from the native app's frame callback. Default false -
26
39
  * the app calls `update` from its own loop.
@@ -36,15 +49,29 @@ export interface NativeRegisterOptions {
36
49
  * 0.1, the radius IWSDK gives a target that declares none.
37
50
  */
38
51
  targetRadius?: number;
52
+ /**
53
+ * Give the target a body in the `physics` slice at its current world pose
54
+ * (core defaults: dynamic, no damping, gravity factor 1). A target whose
55
+ * body the host already holds (`physics.hasBody(id)`) needs neither this
56
+ * nor `shape`: its port is bound to that body as it is.
57
+ */
58
+ body?: PhysicsBodySpec;
59
+ /** The body's collider (default `"auto"`, the host's collider for the object). Implies `body`. */
60
+ shape?: PhysicsShapeSpec;
39
61
  }
40
62
  export declare class NativeInteractions {
41
63
  readonly runtime: InteractionRuntime;
42
64
  readonly provider: NativeInputProvider;
43
65
  readonly hitTester: NativeHitTester;
66
+ /** The platform's physics over the `physics` slice, or null when the host installs none. */
67
+ readonly physics: NativePhysicsFacility | null;
68
+ private readonly stepPhysics;
44
69
  private readonly ports;
70
+ private readonly ownedBodies;
45
71
  private readonly options;
46
72
  private readonly interactionsHost;
47
73
  private detachHost;
74
+ private readonly unsubscribeVisuals;
48
75
  constructor(options?: NativeInteractionsOptions);
49
76
  /**
50
77
  * Register an interactable. The native app knows it by `descriptor.id`,
package/dist/host.js CHANGED
@@ -16,9 +16,10 @@
16
16
  */
17
17
  import { InteractionRuntime, } from "@realitycollective/webxr-interactions";
18
18
  import { NativeHitTester } from "./hit-tester.js";
19
+ import { NativePhysicsFacility } from "./physics-facility.js";
19
20
  import { NativeInputProvider } from "./provider.js";
20
21
  import { NativeTransformPort } from "./transform-port.js";
21
- import { installedHost, resolveHostSlice, } from "./native-types.js";
22
+ import { findHostSlice, installedHost, resolveHostSlice, } from "./native-types.js";
22
23
  /** The radius a target is hit-tested with when it declares none: IWSDK's `targetRadius ?? 0.1`. */
23
24
  const DEFAULT_TARGET_RADIUS = 0.1;
24
25
  /** Longest frame the host-attached loop will report, in seconds. */
@@ -27,10 +28,15 @@ export class NativeInteractions {
27
28
  runtime;
28
29
  provider;
29
30
  hitTester;
31
+ /** The platform's physics over the `physics` slice, or null when the host installs none. */
32
+ physics;
33
+ stepPhysics;
30
34
  ports = new Map();
35
+ ownedBodies = new Set();
31
36
  options;
32
37
  interactionsHost;
33
38
  detachHost = null;
39
+ unsubscribeVisuals;
34
40
  constructor(options = {}) {
35
41
  this.options = options;
36
42
  this.interactionsHost = resolveHostSlice("interactions", options.interactions);
@@ -40,6 +46,19 @@ export class NativeInteractions {
40
46
  provider: this.provider,
41
47
  hitTester: this.hitTester,
42
48
  ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),
49
+ ...(options.nearPointer ? { nearPointer: options.nearPointer } : {}),
50
+ });
51
+ const physicsHost = findHostSlice("physics", options.physics);
52
+ this.physics = physicsHost ? new NativePhysicsFacility({ physics: physicsHost }) : null;
53
+ this.stepPhysics = options.stepPhysics ?? true;
54
+ // The host is handed the ray and cursor decision every frame, per source
55
+ // (`NativeInputHost.applyPointerVisuals`); it draws exactly that.
56
+ const input = resolveHostSlice("input", options.input);
57
+ this.unsubscribeVisuals = this.runtime.onPointerVisuals((visuals) => {
58
+ if (!input.applyPointerVisuals)
59
+ return;
60
+ for (const entry of visuals)
61
+ input.applyPointerVisuals(entry.sourceId, { ...entry });
43
62
  });
44
63
  if (options.attachToHost)
45
64
  this.attachToHost(options);
@@ -50,7 +69,19 @@ export class NativeInteractions {
50
69
  * 0.1 m, so a bare target is a 10 cm sphere as on IWSDK.
51
70
  */
52
71
  register(descriptor, options = {}) {
53
- const port = new NativeTransformPort(descriptor.id, this.options.interactions ? { interactions: this.options.interactions } : {});
72
+ const id = descriptor.id;
73
+ if (options.body || options.shape) {
74
+ if (!this.physics) {
75
+ throw new Error(`@realitycollective/native-interactions: "${id}" asks for a physics body but no physics slice is installed; the host installs globalThis.__rcHost.physics, or pass \`physics\` to createNativeInteractions`);
76
+ }
77
+ this.physics.addBody(id, this.interactionsHost.getWorldPose(id), options.body, options.shape);
78
+ this.ownedBodies.add(id);
79
+ }
80
+ const bound = this.physics && this.physics.hasBody(id) ? this.physics : null;
81
+ const port = new NativeTransformPort(id, {
82
+ ...(this.options.interactions ? { interactions: this.options.interactions } : {}),
83
+ ...(bound ? { physics: { facility: bound, bodyId: id } } : {}),
84
+ });
54
85
  this.interactionsHost.setTargetRadius(descriptor.id, options.targetRadius ?? DEFAULT_TARGET_RADIUS);
55
86
  this.ports.set(descriptor.id, port);
56
87
  this.runtime.registerInteractable(descriptor, { transform: port });
@@ -59,16 +90,23 @@ export class NativeInteractions {
59
90
  unregister(id) {
60
91
  this.runtime.unregisterInteractable(id);
61
92
  this.ports.delete(id);
93
+ if (this.ownedBodies.delete(id))
94
+ this.physics?.removeBody(id);
62
95
  }
63
96
  getPort(id) {
64
97
  return this.ports.get(id);
65
98
  }
66
99
  update(dt) {
100
+ // The provider's eye-gaze filter and grace integrate over the same step.
101
+ this.provider.setFrameDelta(dt);
67
102
  this.runtime.update(dt);
103
+ if (this.stepPhysics)
104
+ this.physics?.step(dt);
68
105
  }
69
106
  dispose() {
70
107
  this.detachHost?.();
71
108
  this.detachHost = null;
109
+ this.unsubscribeVisuals();
72
110
  this.runtime.dispose();
73
111
  this.provider.dispose();
74
112
  }
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * createNativeInteractions - one-call setup for the native adapter, with the\n * same shape as the Babylon, three.js, IWSDK and XR Blocks setups.\n *\n * ```ts\n * const interactions = createNativeInteractions({ attachToHost: true });\n * interactions.register({ id: \"button\", behaviours: [{ kind: \"press\" }] });\n * interactions.runtime.onEvent((event) => console.log(event.type));\n * ```\n *\n * The native app owns the scene, so an interactable is registered by id\n * alone: the app answers hit queries and pose reads for that id through the\n * `interactions` slice. With `attachToHost` the loop drives itself from the\n * app's own frame callback. Without it, call `update(dt)` yourself with\n * seconds.\n */\nimport {\n InteractionRuntime,\n type DwellConfig,\n type InteractableDescriptor,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester, type NativeHitTesterOptions } from \"./hit-tester.js\";\nimport { NativeInputProvider, type NativeInputProviderOptions } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport {\n installedHost,\n resolveHostSlice,\n type NativeFrameSource,\n type NativeInteractionHost,\n} from \"./native-types.js\";\n\nexport interface NativeInteractionsOptions\n extends NativeInputProviderOptions,\n NativeHitTesterOptions {\n dwellDefaults?: DwellConfig;\n /**\n * Drive `update(dt)` from the native app's frame callback. Default false -\n * the app calls `update` from its own loop.\n */\n attachToHost?: boolean;\n /** Where frames come from for `attachToHost`. Omit to use `globalThis.__rcHost`. */\n frames?: NativeFrameSource;\n}\n\n/** Per-registration options, as IWSDK's `register` takes them. */\nexport interface NativeRegisterOptions {\n /**\n * The radius, in metres, the host hit-tests this target with. Default\n * 0.1, the radius IWSDK gives a target that declares none.\n */\n targetRadius?: number;\n}\n\n/** The radius a target is hit-tested with when it declares none: IWSDK's `targetRadius ?? 0.1`. */\nconst DEFAULT_TARGET_RADIUS = 0.1;\n\n/** Longest frame the host-attached loop will report, in seconds. */\nconst MAX_FRAME_SECONDS = 0.1;\n\nexport class NativeInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: NativeInputProvider;\n readonly hitTester: NativeHitTester;\n private readonly ports = new Map<string, NativeTransformPort>();\n private readonly options: NativeInteractionsOptions;\n private readonly interactionsHost: NativeInteractionHost;\n private detachHost: (() => void) | null = null;\n\n constructor(options: NativeInteractionsOptions = {}) {\n this.options = options;\n this.interactionsHost = resolveHostSlice(\"interactions\", options.interactions);\n this.provider = new NativeInputProvider(options);\n this.hitTester = new NativeHitTester(options);\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n });\n if (options.attachToHost) this.attachToHost(options);\n }\n\n /**\n * Register an interactable. The native app knows it by `descriptor.id`,\n * and is told the radius to hit-test it with: `options.targetRadius`, or\n * 0.1 m, so a bare target is a 10 cm sphere as on IWSDK.\n */\n register(descriptor: InteractableDescriptor, options: NativeRegisterOptions = {}): NativeTransformPort {\n const port = new NativeTransformPort(\n descriptor.id,\n this.options.interactions ? { interactions: this.options.interactions } : {},\n );\n this.interactionsHost.setTargetRadius(descriptor.id, options.targetRadius ?? DEFAULT_TARGET_RADIUS);\n this.ports.set(descriptor.id, port);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.ports.delete(id);\n }\n\n getPort(id: string): NativeTransformPort | 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.detachHost?.();\n this.detachHost = null;\n this.runtime.dispose();\n this.provider.dispose();\n }\n\n private attachToHost(options: NativeInteractionsOptions): void {\n const frames = options.frames ?? installedHost();\n if (!frames?.onFrame) {\n throw new Error(\n \"@realitycollective/native-interactions: attachToHost needs a frame source, and globalThis.__rcHost.onFrame is not installed. Pass `frames`, or call update(dt) yourself.\",\n );\n }\n // The app reports the frame delta in seconds. A long frame is clamped so\n // behaviours integrate over a sane step rather than jumping.\n this.detachHost = frames.onFrame((_timestampMs, deltaS) => {\n this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, deltaS)));\n });\n }\n}\n\nexport function createNativeInteractions(\n options: NativeInteractionsOptions = {},\n): NativeInteractions {\n return new NativeInteractions(options);\n}\n"]}
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * createNativeInteractions - one-call setup for the native adapter, with the\n * same shape as the Babylon, three.js, IWSDK and XR Blocks setups.\n *\n * ```ts\n * const interactions = createNativeInteractions({ attachToHost: true });\n * interactions.register({ id: \"button\", behaviours: [{ kind: \"press\" }] });\n * interactions.runtime.onEvent((event) => console.log(event.type));\n * ```\n *\n * The native app owns the scene, so an interactable is registered by id\n * alone: the app answers hit queries and pose reads for that id through the\n * `interactions` slice. With `attachToHost` the loop drives itself from the\n * app's own frame callback. Without it, call `update(dt)` yourself with\n * seconds.\n */\nimport {\n InteractionRuntime,\n type DwellConfig,\n type InteractableDescriptor,\n type NearPointerOptions,\n type PhysicsBodySpec,\n type PhysicsShapeSpec,\n} from \"@realitycollective/webxr-interactions\";\nimport { NativeHitTester, type NativeHitTesterOptions } from \"./hit-tester.js\";\nimport { NativePhysicsFacility } from \"./physics-facility.js\";\nimport { NativeInputProvider, type NativeInputProviderOptions } from \"./provider.js\";\nimport { NativeTransformPort } from \"./transform-port.js\";\nimport {\n findHostSlice,\n installedHost,\n resolveHostSlice,\n type NativeFrameSource,\n type NativeInteractionHost,\n type NativePhysicsHost,\n} from \"./native-types.js\";\n\nexport interface NativeInteractionsOptions\n extends NativeInputProviderOptions,\n NativeHitTesterOptions {\n dwellDefaults?: DwellConfig;\n /** The near-pointer distances (touch hover, touch press, grab radius). Defaults are IWSDK 1.0.0's. */\n nearPointer?: Partial<NearPointerOptions>;\n /**\n * The `physics` slice: the host's default engine (Jolt on Quest and\n * Android, RealityKit on visionOS) behind the core contract, or an app's\n * own `PhysicsFacility` in its place. Omit to read\n * `globalThis.__rcHost.physics`. Without either, `physics` is null and no\n * target can carry a body; the host conformance kit fails such a host.\n */\n physics?: NativePhysicsHost;\n /** Step the physics from `update(dt)`. Default true; false when the host steps its engine itself. */\n stepPhysics?: boolean;\n /**\n * Drive `update(dt)` from the native app's frame callback. Default false -\n * the app calls `update` from its own loop.\n */\n attachToHost?: boolean;\n /** Where frames come from for `attachToHost`. Omit to use `globalThis.__rcHost`. */\n frames?: NativeFrameSource;\n}\n\n/** Per-registration options, as IWSDK's `register` takes them. */\nexport interface NativeRegisterOptions {\n /**\n * The radius, in metres, the host hit-tests this target with. Default\n * 0.1, the radius IWSDK gives a target that declares none.\n */\n targetRadius?: number;\n /**\n * Give the target a body in the `physics` slice at its current world pose\n * (core defaults: dynamic, no damping, gravity factor 1). A target whose\n * body the host already holds (`physics.hasBody(id)`) needs neither this\n * nor `shape`: its port is bound to that body as it is.\n */\n body?: PhysicsBodySpec;\n /** The body's collider (default `\"auto\"`, the host's collider for the object). Implies `body`. */\n shape?: PhysicsShapeSpec;\n}\n\n/** The radius a target is hit-tested with when it declares none: IWSDK's `targetRadius ?? 0.1`. */\nconst DEFAULT_TARGET_RADIUS = 0.1;\n\n/** Longest frame the host-attached loop will report, in seconds. */\nconst MAX_FRAME_SECONDS = 0.1;\n\nexport class NativeInteractions {\n readonly runtime: InteractionRuntime;\n readonly provider: NativeInputProvider;\n readonly hitTester: NativeHitTester;\n /** The platform's physics over the `physics` slice, or null when the host installs none. */\n readonly physics: NativePhysicsFacility | null;\n private readonly stepPhysics: boolean;\n private readonly ports = new Map<string, NativeTransformPort>();\n private readonly ownedBodies = new Set<string>();\n private readonly options: NativeInteractionsOptions;\n private readonly interactionsHost: NativeInteractionHost;\n private detachHost: (() => void) | null = null;\n private readonly unsubscribeVisuals: () => void;\n\n constructor(options: NativeInteractionsOptions = {}) {\n this.options = options;\n this.interactionsHost = resolveHostSlice(\"interactions\", options.interactions);\n this.provider = new NativeInputProvider(options);\n this.hitTester = new NativeHitTester(options);\n this.runtime = new InteractionRuntime({\n provider: this.provider,\n hitTester: this.hitTester,\n ...(options.dwellDefaults ? { dwellDefaults: options.dwellDefaults } : {}),\n ...(options.nearPointer ? { nearPointer: options.nearPointer } : {}),\n });\n const physicsHost = findHostSlice(\"physics\", options.physics);\n this.physics = physicsHost ? new NativePhysicsFacility({ physics: physicsHost }) : null;\n this.stepPhysics = options.stepPhysics ?? true;\n // The host is handed the ray and cursor decision every frame, per source\n // (`NativeInputHost.applyPointerVisuals`); it draws exactly that.\n const input = resolveHostSlice(\"input\", options.input);\n this.unsubscribeVisuals = this.runtime.onPointerVisuals((visuals) => {\n if (!input.applyPointerVisuals) return;\n for (const entry of visuals) input.applyPointerVisuals(entry.sourceId, { ...entry });\n });\n if (options.attachToHost) this.attachToHost(options);\n }\n\n /**\n * Register an interactable. The native app knows it by `descriptor.id`,\n * and is told the radius to hit-test it with: `options.targetRadius`, or\n * 0.1 m, so a bare target is a 10 cm sphere as on IWSDK.\n */\n register(descriptor: InteractableDescriptor, options: NativeRegisterOptions = {}): NativeTransformPort {\n const id = descriptor.id;\n if (options.body || options.shape) {\n if (!this.physics) {\n throw new Error(\n `@realitycollective/native-interactions: \"${id}\" asks for a physics body but no physics slice is installed; the host installs globalThis.__rcHost.physics, or pass \\`physics\\` to createNativeInteractions`,\n );\n }\n this.physics.addBody(id, this.interactionsHost.getWorldPose(id), options.body, options.shape);\n this.ownedBodies.add(id);\n }\n const bound = this.physics && this.physics.hasBody(id) ? this.physics : null;\n const port = new NativeTransformPort(id, {\n ...(this.options.interactions ? { interactions: this.options.interactions } : {}),\n ...(bound ? { physics: { facility: bound, bodyId: id } } : {}),\n });\n this.interactionsHost.setTargetRadius(descriptor.id, options.targetRadius ?? DEFAULT_TARGET_RADIUS);\n this.ports.set(descriptor.id, port);\n this.runtime.registerInteractable(descriptor, { transform: port });\n return port;\n }\n\n unregister(id: string): void {\n this.runtime.unregisterInteractable(id);\n this.ports.delete(id);\n if (this.ownedBodies.delete(id)) this.physics?.removeBody(id);\n }\n\n getPort(id: string): NativeTransformPort | undefined {\n return this.ports.get(id);\n }\n\n update(dt: number): void {\n // The provider's eye-gaze filter and grace integrate over the same step.\n this.provider.setFrameDelta(dt);\n this.runtime.update(dt);\n if (this.stepPhysics) this.physics?.step(dt);\n }\n\n dispose(): void {\n this.detachHost?.();\n this.detachHost = null;\n this.unsubscribeVisuals();\n this.runtime.dispose();\n this.provider.dispose();\n }\n\n private attachToHost(options: NativeInteractionsOptions): void {\n const frames = options.frames ?? installedHost();\n if (!frames?.onFrame) {\n throw new Error(\n \"@realitycollective/native-interactions: attachToHost needs a frame source, and globalThis.__rcHost.onFrame is not installed. Pass `frames`, or call update(dt) yourself.\",\n );\n }\n // The app reports the frame delta in seconds. A long frame is clamped so\n // behaviours integrate over a sane step rather than jumping.\n this.detachHost = frames.onFrame((_timestampMs, deltaS) => {\n this.update(Math.min(MAX_FRAME_SECONDS, Math.max(0, deltaS)));\n });\n }\n}\n\nexport function createNativeInteractions(\n options: NativeInteractionsOptions = {},\n): NativeInteractions {\n return new NativeInteractions(options);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -9,10 +9,11 @@
9
9
  * internal.
10
10
  */
11
11
  export * from "@realitycollective/webxr-interactions";
12
- export type { NativeFrameSource, NativeHit, NativeHostSlices, NativeInputFacts, NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost, NativePresenceShown, } from "./native-types.js";
12
+ export type { NativeFrameSource, NativeHit, NativeHostSlices, NativeInputFacts, NativeInputHost, NativeInteractionHost, NativeInteractionsTestHost, NativePhysicsHost, NativePresenceShown, } from "./native-types.js";
13
13
  export * from "./provider.js";
14
14
  export * from "./hit-tester.js";
15
15
  export * from "./transform-port.js";
16
+ export * from "./physics-facility.js";
16
17
  export * from "./host.js";
17
18
  export { nativeInteractionsHostConformanceCases } from "./conformance.js";
18
19
  export type { NativeInteractionsHostConformanceCase, NativeInteractionsHostConformanceSetup, } from "./conformance.js";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export * from "@realitycollective/webxr-interactions";
12
12
  export * from "./provider.js";
13
13
  export * from "./hit-tester.js";
14
14
  export * from "./transform-port.js";
15
+ export * from "./physics-facility.js";
15
16
  export * from "./host.js";
16
17
  export { nativeInteractionsHostConformanceCases } from "./conformance.js";
17
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,uCAAuC,CAAC;AAWtD,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,sCAAsC,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * Public entry point of the native adapter: the same kinds of export as the\n * Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;\n * the provider, hit tester and transform port implement its contracts; the\n * setup entry point wires them together; and the slice types describe what\n * the native app installs on `globalThis.__rcHost`. One addition the web\n * adapters do not need: the host conformance kit a native app runs on its\n * device against its real slices. Slice reading and tuple copying stay\n * internal.\n */\nexport * from \"@realitycollective/webxr-interactions\";\nexport type {\n NativeFrameSource,\n NativeHit,\n NativeHostSlices,\n NativeInputFacts,\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePresenceShown,\n} from \"./native-types.js\";\nexport * from \"./provider.js\";\nexport * from \"./hit-tester.js\";\nexport * from \"./transform-port.js\";\nexport * from \"./host.js\";\nexport { nativeInteractionsHostConformanceCases } from \"./conformance.js\";\nexport type {\n NativeInteractionsHostConformanceCase,\n NativeInteractionsHostConformanceSetup,\n} from \"./conformance.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,uCAAuC,CAAC;AAYtD,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,sCAAsC,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * Public entry point of the native adapter: the same kinds of export as the\n * Babylon, three.js, IWSDK and XR Blocks adapters. The core is re-exported;\n * the provider, hit tester and transform port implement its contracts; the\n * setup entry point wires them together; and the slice types describe what\n * the native app installs on `globalThis.__rcHost`. One addition the web\n * adapters do not need: the host conformance kit a native app runs on its\n * device against its real slices. Slice reading and tuple copying stay\n * internal.\n */\nexport * from \"@realitycollective/webxr-interactions\";\nexport type {\n NativeFrameSource,\n NativeHit,\n NativeHostSlices,\n NativeInputFacts,\n NativeInputHost,\n NativeInteractionHost,\n NativeInteractionsTestHost,\n NativePhysicsHost,\n NativePresenceShown,\n} from \"./native-types.js\";\nexport * from \"./provider.js\";\nexport * from \"./hit-tester.js\";\nexport * from \"./transform-port.js\";\nexport * from \"./physics-facility.js\";\nexport * from \"./host.js\";\nexport { nativeInteractionsHostConformanceCases } from \"./conformance.js\";\nexport type {\n NativeInteractionsHostConformanceCase,\n NativeInteractionsHostConformanceSetup,\n} from \"./conformance.js\";\n"]}