@realitycollective/webxr-interactions 0.1.1-preview.3 → 0.1.1
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 +18 -2
- package/README.md +16 -2
- package/dist/behaviours/behaviour.d.ts +17 -1
- package/dist/behaviours/behaviour.js +12 -2
- package/dist/behaviours/behaviour.js.map +1 -1
- package/dist/behaviours/grab.d.ts +1 -0
- package/dist/behaviours/grab.js +10 -4
- package/dist/behaviours/grab.js.map +1 -1
- package/dist/contract-cases.js +15 -0
- package/dist/contract-cases.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +8 -0
- package/dist/math.js +17 -0
- package/dist/math.js.map +1 -1
- package/dist/memory-ports.d.ts +55 -0
- package/dist/memory-ports.js +128 -0
- package/dist/memory-ports.js.map +1 -0
- package/dist/near-pointer-contract-cases.js +48 -0
- package/dist/near-pointer-contract-cases.js.map +1 -1
- package/dist/near-pointer.d.ts +9 -52
- package/dist/near-pointer.js +9 -23
- package/dist/near-pointer.js.map +1 -1
- package/dist/release-velocity.d.ts +58 -0
- package/dist/release-velocity.js +89 -0
- package/dist/release-velocity.js.map +1 -0
- package/dist/runtime.d.ts +38 -2
- package/dist/runtime.js +75 -15
- package/dist/runtime.js.map +1 -1
- package/dist/trace-synthetic.d.ts +12 -0
- package/dist/trace-synthetic.js +194 -0
- package/dist/trace-synthetic.js.map +1 -0
- package/dist/trace.d.ts +215 -0
- package/dist/trace.js +350 -0
- package/dist/trace.js.map +1 -0
- package/package.json +8 -4
package/dist/near-pointer.d.ts
CHANGED
|
@@ -21,9 +21,16 @@
|
|
|
21
21
|
* - **Priority.** Per source, the first of touch, grab, ray with a candidate
|
|
22
22
|
* is the active pointer (`PRIORITY_ORDER`), and once it is pressing or
|
|
23
23
|
* grabbing it stays active until the release (IWSDK's selection lock).
|
|
24
|
+
* Since 29 September 2026 that decision is made ONCE per source across
|
|
25
|
+
* interactables and UI panels by the `PointerArbiter` in
|
|
26
|
+
* `@realitycollective/webxr-input` (`pointer-arbiter.ts`), which this
|
|
27
|
+
* runtime feeds and reads; `POINTER_PRIORITY`, `pickActivePointer`,
|
|
28
|
+
* `PointerCandidate` and `PointerVisuals` live there and are re-exported
|
|
29
|
+
* here unchanged.
|
|
24
30
|
* - **Visuals.** The ray and its cursor show only while the ray is the active
|
|
25
31
|
* pointer or nothing is (`shouldHideRay`); a near pointer owning the hand
|
|
26
|
-
* hides them, and the shared cursor sits at the active pointer's hit
|
|
32
|
+
* hides them, and the shared cursor sits at the active pointer's hit. What
|
|
33
|
+
* the app then shows of them is its `PointerDisplay` configuration.
|
|
27
34
|
*
|
|
28
35
|
* One rule is deliberately stricter than the template, as the Pale Signal
|
|
29
36
|
* work order of 28 September 2026 required: a touch never presses from inside
|
|
@@ -33,10 +40,7 @@
|
|
|
33
40
|
* press on that first frame; `touch-reference.test.ts` records the one frame
|
|
34
41
|
* on which the two differ, and that they agree everywhere else.
|
|
35
42
|
*/
|
|
36
|
-
|
|
37
|
-
export type ActivePointerKind = "touch" | "grab" | "ray" | "gaze";
|
|
38
|
-
/** IWSDK's order: the first with a candidate wins. `multi-pointer.js` `PRIORITY_ORDER`. */
|
|
39
|
-
export declare const POINTER_PRIORITY: readonly ActivePointerKind[];
|
|
43
|
+
export { POINTER_PRIORITY, pickActivePointer, pointerVisualsFor, type ActivePointerKind, type PointerCandidate, type PointerVisuals, } from "@realitycollective/webxr-input";
|
|
40
44
|
export interface NearPointerOptions {
|
|
41
45
|
/** Surface distance, metres, within which a fingertip starts hovering a target. IWSDK `enterHoverDistance` 0.15. */
|
|
42
46
|
touchHoverEnter: number;
|
|
@@ -82,50 +86,3 @@ export declare class TouchPointerState {
|
|
|
82
86
|
/** Drop everything without reporting (the source went away). */
|
|
83
87
|
reset(): void;
|
|
84
88
|
}
|
|
85
|
-
/** A candidate one pointer found: the target and the point the cursor sits at. */
|
|
86
|
-
export interface PointerCandidate {
|
|
87
|
-
targetId: string;
|
|
88
|
-
/** World-space point: the ray's hit, or the closest point of a near target. */
|
|
89
|
-
point: readonly [number, number, number];
|
|
90
|
-
/** Metres: the ray parameter for a ray, the surface distance for a near pointer. */
|
|
91
|
-
distance: number;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* IWSDK's `pickActiveByPriority` with its selection lock: while the current
|
|
95
|
-
* active pointer is selecting it stays; otherwise the first pointer in
|
|
96
|
-
* `POINTER_PRIORITY` with a candidate wins, or none.
|
|
97
|
-
*/
|
|
98
|
-
export declare function pickActivePointer(candidates: {
|
|
99
|
-
touch: boolean;
|
|
100
|
-
grab: boolean;
|
|
101
|
-
ray: boolean;
|
|
102
|
-
}, current: ActivePointerKind | null, selecting: boolean): ActivePointerKind | null;
|
|
103
|
-
/**
|
|
104
|
-
* What a binding shows for one source: IWSDK's `shouldHideRay` and the shared
|
|
105
|
-
* cursor from `MultiPointer.update`. A host draws exactly this and decides
|
|
106
|
-
* nothing.
|
|
107
|
-
*/
|
|
108
|
-
export interface PointerVisuals {
|
|
109
|
-
/** The source these visuals belong to. */
|
|
110
|
-
sourceId: string;
|
|
111
|
-
/** The pointer owning the source, or null when none has a candidate. */
|
|
112
|
-
activePointer: ActivePointerKind | null;
|
|
113
|
-
/**
|
|
114
|
-
* Draw this source's ray. True while the source has a ray and no near
|
|
115
|
-
* pointer owns it: the ray is active, or nothing is. False while touch or
|
|
116
|
-
* grab owns the hand, while eye gaze has taken the far ray, and for a
|
|
117
|
-
* source with no ray at all.
|
|
118
|
-
*/
|
|
119
|
-
ray: boolean;
|
|
120
|
-
/**
|
|
121
|
-
* Draw the cursor disc at `cursorPoint`. True exactly while the active
|
|
122
|
-
* pointer has a candidate: at the ray's hit, or on the surface the
|
|
123
|
-
* fingertip or grip reaches. Never drawn "at every ray hit" regardless of
|
|
124
|
-
* ownership, which is what the September 2026 native contract said.
|
|
125
|
-
*/
|
|
126
|
-
cursor: boolean;
|
|
127
|
-
/** World-space cursor position while `cursor` is true, else null. */
|
|
128
|
-
cursorPoint: readonly [number, number, number] | null;
|
|
129
|
-
}
|
|
130
|
-
/** Build the visuals for one source from its active pointer and that pointer's candidate. */
|
|
131
|
-
export declare function pointerVisualsFor(sourceId: string, hasRay: boolean, active: ActivePointerKind | null, candidate: PointerCandidate | null): PointerVisuals;
|
package/dist/near-pointer.js
CHANGED
|
@@ -21,9 +21,16 @@
|
|
|
21
21
|
* - **Priority.** Per source, the first of touch, grab, ray with a candidate
|
|
22
22
|
* is the active pointer (`PRIORITY_ORDER`), and once it is pressing or
|
|
23
23
|
* grabbing it stays active until the release (IWSDK's selection lock).
|
|
24
|
+
* Since 29 September 2026 that decision is made ONCE per source across
|
|
25
|
+
* interactables and UI panels by the `PointerArbiter` in
|
|
26
|
+
* `@realitycollective/webxr-input` (`pointer-arbiter.ts`), which this
|
|
27
|
+
* runtime feeds and reads; `POINTER_PRIORITY`, `pickActivePointer`,
|
|
28
|
+
* `PointerCandidate` and `PointerVisuals` live there and are re-exported
|
|
29
|
+
* here unchanged.
|
|
24
30
|
* - **Visuals.** The ray and its cursor show only while the ray is the active
|
|
25
31
|
* pointer or nothing is (`shouldHideRay`); a near pointer owning the hand
|
|
26
|
-
* hides them, and the shared cursor sits at the active pointer's hit
|
|
32
|
+
* hides them, and the shared cursor sits at the active pointer's hit. What
|
|
33
|
+
* the app then shows of them is its `PointerDisplay` configuration.
|
|
27
34
|
*
|
|
28
35
|
* One rule is deliberately stricter than the template, as the Pale Signal
|
|
29
36
|
* work order of 28 September 2026 required: a touch never presses from inside
|
|
@@ -33,8 +40,7 @@
|
|
|
33
40
|
* press on that first frame; `touch-reference.test.ts` records the one frame
|
|
34
41
|
* on which the two differ, and that they agree everywhere else.
|
|
35
42
|
*/
|
|
36
|
-
|
|
37
|
-
export const POINTER_PRIORITY = Object.freeze(["touch", "grab", "ray"]);
|
|
43
|
+
export { POINTER_PRIORITY, pickActivePointer, pointerVisualsFor, } from "@realitycollective/webxr-input";
|
|
38
44
|
/** IWSDK 1.0.0's constants: `TOUCH_HYSTERESIS`, `TOUCH_DEFAULTS.downRadius`, `createGrabPointer`'s radius. */
|
|
39
45
|
export const NEAR_POINTER_DEFAULTS = Object.freeze({
|
|
40
46
|
touchHoverEnter: 0.15,
|
|
@@ -105,24 +111,4 @@ export class TouchPointerState {
|
|
|
105
111
|
this.armed = false;
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
|
-
/**
|
|
109
|
-
* IWSDK's `pickActiveByPriority` with its selection lock: while the current
|
|
110
|
-
* active pointer is selecting it stays; otherwise the first pointer in
|
|
111
|
-
* `POINTER_PRIORITY` with a candidate wins, or none.
|
|
112
|
-
*/
|
|
113
|
-
export function pickActivePointer(candidates, current, selecting) {
|
|
114
|
-
if (selecting && current !== null)
|
|
115
|
-
return current;
|
|
116
|
-
for (const kind of POINTER_PRIORITY) {
|
|
117
|
-
if (candidates[kind])
|
|
118
|
-
return kind;
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
/** Build the visuals for one source from its active pointer and that pointer's candidate. */
|
|
123
|
-
export function pointerVisualsFor(sourceId, hasRay, active, candidate) {
|
|
124
|
-
const ray = hasRay && (active === null || active === "ray");
|
|
125
|
-
const cursor = active !== null && candidate !== null;
|
|
126
|
-
return { sourceId, activePointer: active, ray, cursor, cursorPoint: cursor ? candidate.point : null };
|
|
127
|
-
}
|
|
128
114
|
//# sourceMappingURL=near-pointer.js.map
|
package/dist/near-pointer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"near-pointer.js","sourceRoot":"","sources":["../src/near-pointer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Near pointers - the touch press, the grip grab, which pointer owns a hand\n * and what that means for the ray and cursor. Pure logic, no engine.\n *\n * The template is IWSDK 1.0.0's pointer stack (`@iwsdk/xr-input`\n * `multi-pointer.js`, `touch-pointer.js`, `grab-pointer.js`, over\n * `@pmndrs/pointer-events`), restated here so every binding runs the same\n * rule from the facts it already reports (`indexTip`, `gripPose`, `ray`):\n *\n * - **Touch.** A fingertip (a controller's ray origin stands in for one)\n * hovers a target once its distance to the target's SURFACE is within\n * `touchHoverEnter` (0.15 m) and keeps hovering until it is beyond\n * `touchHoverExit` (0.2 m): `TOUCH_HYSTERESIS`. It presses while that\n * distance is at or under `touchDown` (0.02 m): `TOUCH_DEFAULTS.downRadius`,\n * which `@pmndrs/pointer-events` compares every frame, so the release is\n * the way out past the same distance.\n * - **Grab.** The grip is a sphere of `grabRadius` (0.07 m,\n * `createGrabPointer`'s default) and a target whose surface it reaches is\n * the grab candidate. The squeeze (or a hand's pinch on a grab-only\n * target) then grabs it.\n * - **Priority.** Per source, the first of touch, grab, ray with a candidate\n * is the active pointer (`PRIORITY_ORDER`), and once it is pressing or\n * grabbing it stays active until the release (IWSDK's selection lock).\n * - **Visuals.** The ray and its cursor show only while the ray is the active\n * pointer or nothing is (`shouldHideRay`); a near pointer owning the hand\n * hides them, and the shared cursor sits at the active pointer's hit point.\n *\n * One rule is deliberately stricter than the template, as the Pale Signal\n * work order of 28 September 2026 required: a touch never presses from inside\n * or from behind. A fingertip first seen already within the press band, a\n * hand that pops into tracking inside the target, does not press until it has\n * been seen outside the band and comes back in. IWSDK's touch pointer would\n * press on that first frame; `touch-reference.test.ts` records the one frame\n * on which the two differ, and that they agree everywhere else.\n */\n\n/** The pointer that owns a source this frame. `\"gaze\"` is the eye-gaze source's own pointer. */\nexport type ActivePointerKind = \"touch\" | \"grab\" | \"ray\" | \"gaze\";\n\n/** IWSDK's order: the first with a candidate wins. `multi-pointer.js` `PRIORITY_ORDER`. */\nexport const POINTER_PRIORITY: readonly ActivePointerKind[] = Object.freeze([\"touch\", \"grab\", \"ray\"]);\n\nexport interface NearPointerOptions {\n /** Surface distance, metres, within which a fingertip starts hovering a target. IWSDK `enterHoverDistance` 0.15. */\n touchHoverEnter: number;\n /** Surface distance, metres, beyond which a hovering fingertip stops hovering. IWSDK `exitHoverDistance` 0.2. Never less than `touchHoverEnter`. */\n touchHoverExit: number;\n /** Surface distance, metres, at or under which a hovering fingertip presses, and above which it releases. IWSDK `downRadius` 0.02. */\n touchDown: number;\n /** Radius, metres, of the grip sphere that finds a grab candidate. `createGrabPointer` default 0.07. */\n grabRadius: number;\n}\n\n/** IWSDK 1.0.0's constants: `TOUCH_HYSTERESIS`, `TOUCH_DEFAULTS.downRadius`, `createGrabPointer`'s radius. */\nexport const NEAR_POINTER_DEFAULTS: Readonly<NearPointerOptions> = Object.freeze({\n touchHoverEnter: 0.15,\n touchHoverExit: 0.2,\n touchDown: 0.02,\n grabRadius: 0.07,\n});\n\nexport function resolveNearPointerOptions(options: Partial<NearPointerOptions> = {}): NearPointerOptions {\n const resolved = { ...NEAR_POINTER_DEFAULTS, ...options };\n if (resolved.touchDown < 0 || resolved.touchHoverEnter < 0 || resolved.grabRadius < 0) {\n throw new Error(\"[webxr-interactions] near pointer distances must be >= 0\");\n }\n if (resolved.touchHoverExit < resolved.touchHoverEnter) {\n throw new Error(\n `[webxr-interactions] touchHoverExit (${resolved.touchHoverExit}) must be >= touchHoverEnter (${resolved.touchHoverEnter})`,\n );\n }\n return resolved;\n}\n\n/** What the touch machine reports after one frame. */\nexport interface TouchPointerUpdate {\n /** The fingertip is hovering a target this frame (the touch pointer has a candidate). */\n hovering: boolean;\n /** The touch is pressing this frame. */\n pressed: boolean;\n /** True on the exact frame a press started. */\n pressStarted: boolean;\n /** True on the exact frame a press ended. */\n pressEnded: boolean;\n}\n\n/**\n * One source's touch pointer: hover with hysteresis, press at `touchDown`\n * armed only from outside the band. Feed it the fingertip's surface distance\n * to the nearest target each frame, or `null` when no target is in reach.\n */\nexport class TouchPointerState {\n private hovering = false;\n private pressed = false;\n /** The fingertip has been seen outside the press band since it last lost contact. */\n private armed = false;\n\n get isHovering(): boolean {\n return this.hovering;\n }\n\n get isPressed(): boolean {\n return this.pressed;\n }\n\n /**\n * Advance one frame. `enter`/`exit` are the hover band for the target in\n * question (an interactable may widen the default), `down` the press\n * distance.\n */\n update(distance: number | null, enter: number, exit: number, down: number): TouchPointerUpdate {\n const wasPressed = this.pressed;\n const threshold = this.hovering ? exit : enter;\n if (distance === null || distance > threshold) {\n this.hovering = false;\n this.pressed = false;\n this.armed = false;\n return { hovering: false, pressed: false, pressStarted: false, pressEnded: wasPressed };\n }\n this.hovering = true;\n if (distance > down) {\n // Outside the press band: released if it was pressing, and armed for the next press.\n this.armed = true;\n this.pressed = false;\n return { hovering: true, pressed: false, pressStarted: false, pressEnded: wasPressed };\n }\n // Within the press band. IWSDK presses here unconditionally; the core\n // requires the fingertip to have arrived from outside (see the file comment).\n if (!this.pressed && this.armed) {\n this.pressed = true;\n this.armed = false;\n return { hovering: true, pressed: true, pressStarted: true, pressEnded: false };\n }\n return { hovering: true, pressed: this.pressed, pressStarted: false, pressEnded: false };\n }\n\n /** Drop everything without reporting (the source went away). */\n reset(): void {\n this.hovering = false;\n this.pressed = false;\n this.armed = false;\n }\n}\n\n/** A candidate one pointer found: the target and the point the cursor sits at. */\nexport interface PointerCandidate {\n targetId: string;\n /** World-space point: the ray's hit, or the closest point of a near target. */\n point: readonly [number, number, number];\n /** Metres: the ray parameter for a ray, the surface distance for a near pointer. */\n distance: number;\n}\n\n/**\n * IWSDK's `pickActiveByPriority` with its selection lock: while the current\n * active pointer is selecting it stays; otherwise the first pointer in\n * `POINTER_PRIORITY` with a candidate wins, or none.\n */\nexport function pickActivePointer(\n candidates: { touch: boolean; grab: boolean; ray: boolean },\n current: ActivePointerKind | null,\n selecting: boolean,\n): ActivePointerKind | null {\n if (selecting && current !== null) return current;\n for (const kind of POINTER_PRIORITY) {\n if (candidates[kind as \"touch\" | \"grab\" | \"ray\"]) return kind;\n }\n return null;\n}\n\n/**\n * What a binding shows for one source: IWSDK's `shouldHideRay` and the shared\n * cursor from `MultiPointer.update`. A host draws exactly this and decides\n * nothing.\n */\nexport interface PointerVisuals {\n /** The source these visuals belong to. */\n sourceId: string;\n /** The pointer owning the source, or null when none has a candidate. */\n activePointer: ActivePointerKind | null;\n /**\n * Draw this source's ray. True while the source has a ray and no near\n * pointer owns it: the ray is active, or nothing is. False while touch or\n * grab owns the hand, while eye gaze has taken the far ray, and for a\n * source with no ray at all.\n */\n ray: boolean;\n /**\n * Draw the cursor disc at `cursorPoint`. True exactly while the active\n * pointer has a candidate: at the ray's hit, or on the surface the\n * fingertip or grip reaches. Never drawn \"at every ray hit\" regardless of\n * ownership, which is what the September 2026 native contract said.\n */\n cursor: boolean;\n /** World-space cursor position while `cursor` is true, else null. */\n cursorPoint: readonly [number, number, number] | null;\n}\n\n/** Build the visuals for one source from its active pointer and that pointer's candidate. */\nexport function pointerVisualsFor(\n sourceId: string,\n hasRay: boolean,\n active: ActivePointerKind | null,\n candidate: PointerCandidate | null,\n): PointerVisuals {\n const ray = hasRay && (active === null || active === \"ray\");\n const cursor = active !== null && candidate !== null;\n return { sourceId, activePointer: active, ray, cursor, cursorPoint: cursor ? candidate!.point : null };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"near-pointer.js","sourceRoot":"","sources":["../src/near-pointer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Near pointers - the touch press, the grip grab, which pointer owns a hand\n * and what that means for the ray and cursor. Pure logic, no engine.\n *\n * The template is IWSDK 1.0.0's pointer stack (`@iwsdk/xr-input`\n * `multi-pointer.js`, `touch-pointer.js`, `grab-pointer.js`, over\n * `@pmndrs/pointer-events`), restated here so every binding runs the same\n * rule from the facts it already reports (`indexTip`, `gripPose`, `ray`):\n *\n * - **Touch.** A fingertip (a controller's ray origin stands in for one)\n * hovers a target once its distance to the target's SURFACE is within\n * `touchHoverEnter` (0.15 m) and keeps hovering until it is beyond\n * `touchHoverExit` (0.2 m): `TOUCH_HYSTERESIS`. It presses while that\n * distance is at or under `touchDown` (0.02 m): `TOUCH_DEFAULTS.downRadius`,\n * which `@pmndrs/pointer-events` compares every frame, so the release is\n * the way out past the same distance.\n * - **Grab.** The grip is a sphere of `grabRadius` (0.07 m,\n * `createGrabPointer`'s default) and a target whose surface it reaches is\n * the grab candidate. The squeeze (or a hand's pinch on a grab-only\n * target) then grabs it.\n * - **Priority.** Per source, the first of touch, grab, ray with a candidate\n * is the active pointer (`PRIORITY_ORDER`), and once it is pressing or\n * grabbing it stays active until the release (IWSDK's selection lock).\n * Since 29 September 2026 that decision is made ONCE per source across\n * interactables and UI panels by the `PointerArbiter` in\n * `@realitycollective/webxr-input` (`pointer-arbiter.ts`), which this\n * runtime feeds and reads; `POINTER_PRIORITY`, `pickActivePointer`,\n * `PointerCandidate` and `PointerVisuals` live there and are re-exported\n * here unchanged.\n * - **Visuals.** The ray and its cursor show only while the ray is the active\n * pointer or nothing is (`shouldHideRay`); a near pointer owning the hand\n * hides them, and the shared cursor sits at the active pointer's hit. What\n * the app then shows of them is its `PointerDisplay` configuration.\n *\n * One rule is deliberately stricter than the template, as the Pale Signal\n * work order of 28 September 2026 required: a touch never presses from inside\n * or from behind. A fingertip first seen already within the press band, a\n * hand that pops into tracking inside the target, does not press until it has\n * been seen outside the band and comes back in. IWSDK's touch pointer would\n * press on that first frame; `touch-reference.test.ts` records the one frame\n * on which the two differ, and that they agree everywhere else.\n */\nexport {\n POINTER_PRIORITY,\n pickActivePointer,\n pointerVisualsFor,\n type ActivePointerKind,\n type PointerCandidate,\n type PointerVisuals,\n} from \"@realitycollective/webxr-input\";\n\nexport interface NearPointerOptions {\n /** Surface distance, metres, within which a fingertip starts hovering a target. IWSDK `enterHoverDistance` 0.15. */\n touchHoverEnter: number;\n /** Surface distance, metres, beyond which a hovering fingertip stops hovering. IWSDK `exitHoverDistance` 0.2. Never less than `touchHoverEnter`. */\n touchHoverExit: number;\n /** Surface distance, metres, at or under which a hovering fingertip presses, and above which it releases. IWSDK `downRadius` 0.02. */\n touchDown: number;\n /** Radius, metres, of the grip sphere that finds a grab candidate. `createGrabPointer` default 0.07. */\n grabRadius: number;\n}\n\n/** IWSDK 1.0.0's constants: `TOUCH_HYSTERESIS`, `TOUCH_DEFAULTS.downRadius`, `createGrabPointer`'s radius. */\nexport const NEAR_POINTER_DEFAULTS: Readonly<NearPointerOptions> = Object.freeze({\n touchHoverEnter: 0.15,\n touchHoverExit: 0.2,\n touchDown: 0.02,\n grabRadius: 0.07,\n});\n\nexport function resolveNearPointerOptions(options: Partial<NearPointerOptions> = {}): NearPointerOptions {\n const resolved = { ...NEAR_POINTER_DEFAULTS, ...options };\n if (resolved.touchDown < 0 || resolved.touchHoverEnter < 0 || resolved.grabRadius < 0) {\n throw new Error(\"[webxr-interactions] near pointer distances must be >= 0\");\n }\n if (resolved.touchHoverExit < resolved.touchHoverEnter) {\n throw new Error(\n `[webxr-interactions] touchHoverExit (${resolved.touchHoverExit}) must be >= touchHoverEnter (${resolved.touchHoverEnter})`,\n );\n }\n return resolved;\n}\n\n/** What the touch machine reports after one frame. */\nexport interface TouchPointerUpdate {\n /** The fingertip is hovering a target this frame (the touch pointer has a candidate). */\n hovering: boolean;\n /** The touch is pressing this frame. */\n pressed: boolean;\n /** True on the exact frame a press started. */\n pressStarted: boolean;\n /** True on the exact frame a press ended. */\n pressEnded: boolean;\n}\n\n/**\n * One source's touch pointer: hover with hysteresis, press at `touchDown`\n * armed only from outside the band. Feed it the fingertip's surface distance\n * to the nearest target each frame, or `null` when no target is in reach.\n */\nexport class TouchPointerState {\n private hovering = false;\n private pressed = false;\n /** The fingertip has been seen outside the press band since it last lost contact. */\n private armed = false;\n\n get isHovering(): boolean {\n return this.hovering;\n }\n\n get isPressed(): boolean {\n return this.pressed;\n }\n\n /**\n * Advance one frame. `enter`/`exit` are the hover band for the target in\n * question (an interactable may widen the default), `down` the press\n * distance.\n */\n update(distance: number | null, enter: number, exit: number, down: number): TouchPointerUpdate {\n const wasPressed = this.pressed;\n const threshold = this.hovering ? exit : enter;\n if (distance === null || distance > threshold) {\n this.hovering = false;\n this.pressed = false;\n this.armed = false;\n return { hovering: false, pressed: false, pressStarted: false, pressEnded: wasPressed };\n }\n this.hovering = true;\n if (distance > down) {\n // Outside the press band: released if it was pressing, and armed for the next press.\n this.armed = true;\n this.pressed = false;\n return { hovering: true, pressed: false, pressStarted: false, pressEnded: wasPressed };\n }\n // Within the press band. IWSDK presses here unconditionally; the core\n // requires the fingertip to have arrived from outside (see the file comment).\n if (!this.pressed && this.armed) {\n this.pressed = true;\n this.armed = false;\n return { hovering: true, pressed: true, pressStarted: true, pressEnded: false };\n }\n return { hovering: true, pressed: this.pressed, pressStarted: false, pressEnded: false };\n }\n\n /** Drop everything without reporting (the source went away). */\n reset(): void {\n this.hovering = false;\n this.pressed = false;\n this.armed = false;\n }\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release velocity of a throw - the core rule, restated from what Meta
|
|
3
|
+
* IWSDK 1.0.0 does with a held object so every platform's throw carries the
|
|
4
|
+
* same speed on the same input.
|
|
5
|
+
*
|
|
6
|
+
* On the web a thrown object is carried by IWSDK's grab system and Havok.
|
|
7
|
+
* While the object is `Grabbed`, `physics-system.js` sends the body a
|
|
8
|
+
* `SetTargetTransform` command every frame with the object's pose (the pose
|
|
9
|
+
* the grab pointer moved it to), so Havok drives the body kinematically to
|
|
10
|
+
* that target at each 1/60 s step (`PHYSICS_DEFAULTS.stepsPerSecond`). When
|
|
11
|
+
* the grab ends the tag is removed and nothing resets the body: it keeps the
|
|
12
|
+
* velocity the engine gave it to reach the last target, the displacement of
|
|
13
|
+
* the OBJECT over the last physics step divided by the step. So IWSDK's
|
|
14
|
+
* throw is:
|
|
15
|
+
*
|
|
16
|
+
* - the velocity of the held OBJECT, not of the grip: for a rigid carry the
|
|
17
|
+
* two agree, but the rule is stated on the object, which every platform
|
|
18
|
+
* has, rather than on a runtime-reported hand velocity, which most do not;
|
|
19
|
+
* - measured over the last physics step, `RELEASE_WINDOW_SECONDS` (1/60 s):
|
|
20
|
+
* at 72 Hz that is one frame, at 90 Hz two, at 120 Hz two;
|
|
21
|
+
* - angular velocity from the orientation delta over the same window.
|
|
22
|
+
*
|
|
23
|
+
* `ReleaseVelocityTracker` keeps the poses the carry wrote and their frame
|
|
24
|
+
* times; `release()` differentiates over the most recent frames that cover
|
|
25
|
+
* the window, or over the last frame when only one has been seen. A hold
|
|
26
|
+
* that never moved (or was released the frame it started) throws nothing:
|
|
27
|
+
* zeros, so the object rests where it was let go. A synthesised release
|
|
28
|
+
* (source lost, target unregistered, runtime disposed) never reads this: the
|
|
29
|
+
* grab behaviour hands the port zeros, as `grab.ts` says.
|
|
30
|
+
*/
|
|
31
|
+
import { type PoseTuple } from "@realitycollective/webxr-input";
|
|
32
|
+
import type { HoldRelease } from "./ports.js";
|
|
33
|
+
/** IWSDK's physics step, 1/60 s: the window a throw's velocity is measured over. */
|
|
34
|
+
export declare const RELEASE_WINDOW_SECONDS: number;
|
|
35
|
+
export interface ReleaseVelocityTrackerOptions {
|
|
36
|
+
/** Seconds the release velocity is measured over. Default `RELEASE_WINDOW_SECONDS`. */
|
|
37
|
+
windowSeconds?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare class ReleaseVelocityTracker {
|
|
40
|
+
private readonly window;
|
|
41
|
+
private readonly samples;
|
|
42
|
+
constructor(options?: ReleaseVelocityTrackerOptions);
|
|
43
|
+
/** Forget every pose: the hold starts afresh. */
|
|
44
|
+
reset(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Record the pose the carry wrote this frame, `dt` seconds after the last
|
|
47
|
+
* one. The tuples are copied, so the caller may reuse them.
|
|
48
|
+
*/
|
|
49
|
+
record(pose: PoseTuple, dt: number): void;
|
|
50
|
+
/** How many poses are recorded. */
|
|
51
|
+
get size(): number;
|
|
52
|
+
/**
|
|
53
|
+
* The velocity the object carries at release: linear m/s and angular
|
|
54
|
+
* rad/s (axis scaled), world space, over the most recent frames that span
|
|
55
|
+
* at least the window. Zeros with fewer than two poses, or no elapsed time.
|
|
56
|
+
*/
|
|
57
|
+
release(): HoldRelease;
|
|
58
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release velocity of a throw - the core rule, restated from what Meta
|
|
3
|
+
* IWSDK 1.0.0 does with a held object so every platform's throw carries the
|
|
4
|
+
* same speed on the same input.
|
|
5
|
+
*
|
|
6
|
+
* On the web a thrown object is carried by IWSDK's grab system and Havok.
|
|
7
|
+
* While the object is `Grabbed`, `physics-system.js` sends the body a
|
|
8
|
+
* `SetTargetTransform` command every frame with the object's pose (the pose
|
|
9
|
+
* the grab pointer moved it to), so Havok drives the body kinematically to
|
|
10
|
+
* that target at each 1/60 s step (`PHYSICS_DEFAULTS.stepsPerSecond`). When
|
|
11
|
+
* the grab ends the tag is removed and nothing resets the body: it keeps the
|
|
12
|
+
* velocity the engine gave it to reach the last target, the displacement of
|
|
13
|
+
* the OBJECT over the last physics step divided by the step. So IWSDK's
|
|
14
|
+
* throw is:
|
|
15
|
+
*
|
|
16
|
+
* - the velocity of the held OBJECT, not of the grip: for a rigid carry the
|
|
17
|
+
* two agree, but the rule is stated on the object, which every platform
|
|
18
|
+
* has, rather than on a runtime-reported hand velocity, which most do not;
|
|
19
|
+
* - measured over the last physics step, `RELEASE_WINDOW_SECONDS` (1/60 s):
|
|
20
|
+
* at 72 Hz that is one frame, at 90 Hz two, at 120 Hz two;
|
|
21
|
+
* - angular velocity from the orientation delta over the same window.
|
|
22
|
+
*
|
|
23
|
+
* `ReleaseVelocityTracker` keeps the poses the carry wrote and their frame
|
|
24
|
+
* times; `release()` differentiates over the most recent frames that cover
|
|
25
|
+
* the window, or over the last frame when only one has been seen. A hold
|
|
26
|
+
* that never moved (or was released the frame it started) throws nothing:
|
|
27
|
+
* zeros, so the object rests where it was let go. A synthesised release
|
|
28
|
+
* (source lost, target unregistered, runtime disposed) never reads this: the
|
|
29
|
+
* grab behaviour hands the port zeros, as `grab.ts` says.
|
|
30
|
+
*/
|
|
31
|
+
import { velocityBetween } from "@realitycollective/webxr-input";
|
|
32
|
+
/** IWSDK's physics step, 1/60 s: the window a throw's velocity is measured over. */
|
|
33
|
+
export const RELEASE_WINDOW_SECONDS = 1 / 60;
|
|
34
|
+
/** How many past frames the tracker keeps: enough to cover the window at any refresh rate down to 30 Hz. */
|
|
35
|
+
const MAX_SAMPLES = 8;
|
|
36
|
+
export class ReleaseVelocityTracker {
|
|
37
|
+
window;
|
|
38
|
+
samples = [];
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
const window = options.windowSeconds ?? RELEASE_WINDOW_SECONDS;
|
|
41
|
+
if (!Number.isFinite(window) || window <= 0) {
|
|
42
|
+
throw new Error(`[webxr-interactions] release window must be a positive number of seconds, got ${String(window)}`);
|
|
43
|
+
}
|
|
44
|
+
this.window = window;
|
|
45
|
+
}
|
|
46
|
+
/** Forget every pose: the hold starts afresh. */
|
|
47
|
+
reset() {
|
|
48
|
+
this.samples.length = 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Record the pose the carry wrote this frame, `dt` seconds after the last
|
|
52
|
+
* one. The tuples are copied, so the caller may reuse them.
|
|
53
|
+
*/
|
|
54
|
+
record(pose, dt) {
|
|
55
|
+
const copy = {
|
|
56
|
+
position: [pose.position[0], pose.position[1], pose.position[2]],
|
|
57
|
+
quaternion: [pose.quaternion[0], pose.quaternion[1], pose.quaternion[2], pose.quaternion[3]],
|
|
58
|
+
};
|
|
59
|
+
const step = this.samples.length === 0 || !Number.isFinite(dt) || dt < 0 ? 0 : dt;
|
|
60
|
+
this.samples.push({ pose: copy, dt: step });
|
|
61
|
+
if (this.samples.length > MAX_SAMPLES)
|
|
62
|
+
this.samples.shift();
|
|
63
|
+
}
|
|
64
|
+
/** How many poses are recorded. */
|
|
65
|
+
get size() {
|
|
66
|
+
return this.samples.length;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The velocity the object carries at release: linear m/s and angular
|
|
70
|
+
* rad/s (axis scaled), world space, over the most recent frames that span
|
|
71
|
+
* at least the window. Zeros with fewer than two poses, or no elapsed time.
|
|
72
|
+
*/
|
|
73
|
+
release() {
|
|
74
|
+
const n = this.samples.length;
|
|
75
|
+
if (n < 2)
|
|
76
|
+
return { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] };
|
|
77
|
+
let elapsed = 0;
|
|
78
|
+
let start = n - 1;
|
|
79
|
+
while (start > 0 && elapsed < this.window) {
|
|
80
|
+
elapsed += this.samples[start].dt;
|
|
81
|
+
start -= 1;
|
|
82
|
+
}
|
|
83
|
+
if (elapsed <= 0)
|
|
84
|
+
return { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] };
|
|
85
|
+
const velocity = velocityBetween(this.samples[start].pose, this.samples[n - 1].pose, elapsed);
|
|
86
|
+
return { linearVelocity: velocity.linear, angularVelocity: velocity.angular };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=release-velocity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-velocity.js","sourceRoot":"","sources":["../src/release-velocity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The release velocity of a throw - the core rule, restated from what Meta\n * IWSDK 1.0.0 does with a held object so every platform's throw carries the\n * same speed on the same input.\n *\n * On the web a thrown object is carried by IWSDK's grab system and Havok.\n * While the object is `Grabbed`, `physics-system.js` sends the body a\n * `SetTargetTransform` command every frame with the object's pose (the pose\n * the grab pointer moved it to), so Havok drives the body kinematically to\n * that target at each 1/60 s step (`PHYSICS_DEFAULTS.stepsPerSecond`). When\n * the grab ends the tag is removed and nothing resets the body: it keeps the\n * velocity the engine gave it to reach the last target, the displacement of\n * the OBJECT over the last physics step divided by the step. So IWSDK's\n * throw is:\n *\n * - the velocity of the held OBJECT, not of the grip: for a rigid carry the\n * two agree, but the rule is stated on the object, which every platform\n * has, rather than on a runtime-reported hand velocity, which most do not;\n * - measured over the last physics step, `RELEASE_WINDOW_SECONDS` (1/60 s):\n * at 72 Hz that is one frame, at 90 Hz two, at 120 Hz two;\n * - angular velocity from the orientation delta over the same window.\n *\n * `ReleaseVelocityTracker` keeps the poses the carry wrote and their frame\n * times; `release()` differentiates over the most recent frames that cover\n * the window, or over the last frame when only one has been seen. A hold\n * that never moved (or was released the frame it started) throws nothing:\n * zeros, so the object rests where it was let go. A synthesised release\n * (source lost, target unregistered, runtime disposed) never reads this: the\n * grab behaviour hands the port zeros, as `grab.ts` says.\n */\nimport { velocityBetween, type PoseTuple } from \"@realitycollective/webxr-input\";\nimport type { HoldRelease } from \"./ports.js\";\n\n/** IWSDK's physics step, 1/60 s: the window a throw's velocity is measured over. */\nexport const RELEASE_WINDOW_SECONDS = 1 / 60;\n\n/** How many past frames the tracker keeps: enough to cover the window at any refresh rate down to 30 Hz. */\nconst MAX_SAMPLES = 8;\n\ninterface Sample {\n pose: PoseTuple;\n /** Seconds between this pose and the one before it (0 for the first). */\n dt: number;\n}\n\nexport interface ReleaseVelocityTrackerOptions {\n /** Seconds the release velocity is measured over. Default `RELEASE_WINDOW_SECONDS`. */\n windowSeconds?: number;\n}\n\nexport class ReleaseVelocityTracker {\n private readonly window: number;\n private readonly samples: Sample[] = [];\n\n constructor(options: ReleaseVelocityTrackerOptions = {}) {\n const window = options.windowSeconds ?? RELEASE_WINDOW_SECONDS;\n if (!Number.isFinite(window) || window <= 0) {\n throw new Error(`[webxr-interactions] release window must be a positive number of seconds, got ${String(window)}`);\n }\n this.window = window;\n }\n\n /** Forget every pose: the hold starts afresh. */\n reset(): void {\n this.samples.length = 0;\n }\n\n /**\n * Record the pose the carry wrote this frame, `dt` seconds after the last\n * one. The tuples are copied, so the caller may reuse them.\n */\n record(pose: PoseTuple, dt: number): void {\n const copy: PoseTuple = {\n position: [pose.position[0], pose.position[1], pose.position[2]],\n quaternion: [pose.quaternion[0], pose.quaternion[1], pose.quaternion[2], pose.quaternion[3]],\n };\n const step = this.samples.length === 0 || !Number.isFinite(dt) || dt < 0 ? 0 : dt;\n this.samples.push({ pose: copy, dt: step });\n if (this.samples.length > MAX_SAMPLES) this.samples.shift();\n }\n\n /** How many poses are recorded. */\n get size(): number {\n return this.samples.length;\n }\n\n /**\n * The velocity the object carries at release: linear m/s and angular\n * rad/s (axis scaled), world space, over the most recent frames that span\n * at least the window. Zeros with fewer than two poses, or no elapsed time.\n */\n release(): HoldRelease {\n const n = this.samples.length;\n if (n < 2) return { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] };\n let elapsed = 0;\n let start = n - 1;\n while (start > 0 && elapsed < this.window) {\n elapsed += this.samples[start]!.dt;\n start -= 1;\n }\n if (elapsed <= 0) return { linearVelocity: [0, 0, 0], angularVelocity: [0, 0, 0] };\n const velocity = velocityBetween(this.samples[start]!.pose, this.samples[n - 1]!.pose, elapsed);\n return { linearVelocity: velocity.linear, angularVelocity: velocity.angular };\n }\n}\n"]}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* unregister, enable and disable; behaviours disabled by capability
|
|
14
14
|
* negotiation announce themselves with a `behaviourDisabled` event.
|
|
15
15
|
*/
|
|
16
|
-
import { type EyeGazeOptions, type InputCapabilities, type InputProvider, type InputSourceSnapshot, type Unsubscribe } from "@realitycollective/webxr-input";
|
|
16
|
+
import { PointerArbiter, PointerDisplay, type ActivePointerKind, type EyeGazeOptions, type InputCapabilities, type InputProvider, type InputSourceSnapshot, type PointerDisplayConfig, type PointerDrawing, type PointerVisuals, type Unsubscribe } from "@realitycollective/webxr-input";
|
|
17
17
|
import type { Behaviour } from "./behaviours/behaviour.js";
|
|
18
18
|
import type { InteractableDescriptor } from "./descriptor.js";
|
|
19
19
|
import { type InteractionEventListener } from "./events.js";
|
|
20
20
|
import type { FeedbackListener } from "./feedback.js";
|
|
21
21
|
import { type DwellConfig } from "./gaze.js";
|
|
22
22
|
import type { HitTester, TransformPort } from "./ports.js";
|
|
23
|
-
import { type
|
|
23
|
+
import { type NearPointerOptions } from "./near-pointer.js";
|
|
24
24
|
import { type VelocityTrackerOptions } from "./velocity-tracker.js";
|
|
25
25
|
/** Interactor id used when gaze is synthesized from the head pose. */
|
|
26
26
|
export declare const HEAD_GAZE_INTERACTOR_ID = "head-gaze";
|
|
@@ -60,7 +60,23 @@ export interface InteractionRuntimeOptions {
|
|
|
60
60
|
* the touch hover enter distance for that target alone.
|
|
61
61
|
*/
|
|
62
62
|
nearPointer?: Partial<NearPointerOptions>;
|
|
63
|
+
/**
|
|
64
|
+
* The pointer arbiter shared with everything else a pointer can reach (the
|
|
65
|
+
* UI Extensions window hosts), so one decision per source covers panels
|
|
66
|
+
* and interactables alike, as IWSDK's `MultiPointer` does. Omit for a
|
|
67
|
+
* runtime used on its own: it makes one, and decides over its own targets.
|
|
68
|
+
*/
|
|
69
|
+
pointers?: PointerArbiter;
|
|
70
|
+
/**
|
|
71
|
+
* The app's pointer display settings (ray never, always or while hitting;
|
|
72
|
+
* cursors on objects and on panels; the ray and cursor look), or a
|
|
73
|
+
* `PointerDisplay` to share. Defaults are IWSDK 1.0.0's. Settable at run
|
|
74
|
+
* time through `getPointerDisplay().set(...)`.
|
|
75
|
+
*/
|
|
76
|
+
pointerDisplay?: PointerDisplay | Partial<PointerDisplayConfig>;
|
|
63
77
|
}
|
|
78
|
+
/** The target set this runtime registers with its arbiter. */
|
|
79
|
+
export declare const INTERACTIONS_POINTER_SET = "interactions";
|
|
64
80
|
export interface RegisterPorts {
|
|
65
81
|
transform?: TransformPort;
|
|
66
82
|
}
|
|
@@ -74,8 +90,13 @@ export declare class InteractionRuntime {
|
|
|
74
90
|
private readonly feedbackEmitter;
|
|
75
91
|
private readonly afterSample;
|
|
76
92
|
private readonly visualsEmitter;
|
|
93
|
+
private readonly drawingEmitter;
|
|
77
94
|
private readonly nearPointer;
|
|
95
|
+
private readonly pointers;
|
|
96
|
+
private readonly targetSet;
|
|
97
|
+
private readonly pointerDisplay;
|
|
78
98
|
private lastVisuals;
|
|
99
|
+
private lastDrawings;
|
|
79
100
|
private readonly velocityTracker;
|
|
80
101
|
private lastSources;
|
|
81
102
|
private capabilities;
|
|
@@ -149,6 +170,21 @@ export declare class InteractionRuntime {
|
|
|
149
170
|
onPointerVisuals(listener: (visuals: readonly PointerVisuals[]) => void): Unsubscribe;
|
|
150
171
|
/** The visuals last published for a source, or undefined before its first frame. */
|
|
151
172
|
getPointerVisuals(sourceId: string): PointerVisuals | undefined;
|
|
173
|
+
/**
|
|
174
|
+
* What to draw for every source this frame under the app's pointer display
|
|
175
|
+
* settings: the visuals above resolved through `pointerDrawing`. Published
|
|
176
|
+
* right after `onPointerVisuals`. A binding that draws its own ray and
|
|
177
|
+
* cursor applies exactly this; a native binding hands it to the host.
|
|
178
|
+
*/
|
|
179
|
+
onPointerDrawing(listener: (drawings: readonly PointerDrawing[]) => void): Unsubscribe;
|
|
180
|
+
/** The drawing last published for a source, or undefined before its first frame. */
|
|
181
|
+
getPointerDrawing(sourceId: string): PointerDrawing | undefined;
|
|
182
|
+
/** The arbiter deciding which pointer owns each source, shared with the UI Extensions hosts when the app passed one. */
|
|
183
|
+
getPointerArbiter(): PointerArbiter;
|
|
184
|
+
/** The app's pointer display settings; `set` changes them at run time. */
|
|
185
|
+
getPointerDisplay(): PointerDisplay;
|
|
186
|
+
/** Whether a source is pressing or grabbing this frame (the selection that recolours its pointer). */
|
|
187
|
+
isSelecting(sourceId: string): boolean;
|
|
152
188
|
update(dt: number): void;
|
|
153
189
|
dispose(): void;
|
|
154
190
|
/**
|
package/dist/runtime.js
CHANGED
|
@@ -13,16 +13,18 @@
|
|
|
13
13
|
* unregister, enable and disable; behaviours disabled by capability
|
|
14
14
|
* negotiation announce themselves with a `behaviourDisabled` event.
|
|
15
15
|
*/
|
|
16
|
-
import { SELECT_PRESS_THRESHOLD, SELECT_RELEASE_THRESHOLD, rayPoseFromRay, resolveEyeGazeOptions, unmetRequirements, } from "@realitycollective/webxr-input";
|
|
16
|
+
import { PointerArbiter, PointerDisplay, SELECT_PRESS_THRESHOLD, SELECT_RELEASE_THRESHOLD, pointerVisualsFor, rayPoseFromRay, resolveEyeGazeOptions, unmetRequirements, } from "@realitycollective/webxr-input";
|
|
17
17
|
import { createBehaviour, } from "./behaviours/factory.js";
|
|
18
18
|
import { GrabBehaviour } from "./behaviours/grab.js";
|
|
19
19
|
import { Emitter } from "./events.js";
|
|
20
20
|
import { DwellState, GazeConsensus, resolveDwellConfig, DWELL_DEFAULTS } from "./gaze.js";
|
|
21
21
|
import { quatConjugate, quatMultiply, vApplyQuat, vLength, vNormalize, vSub } from "./math.js";
|
|
22
|
-
import {
|
|
22
|
+
import { resolveNearPointerOptions, TouchPointerState } from "./near-pointer.js";
|
|
23
23
|
import { VelocityTracker } from "./velocity-tracker.js";
|
|
24
24
|
/** Interactor id used when gaze is synthesized from the head pose. */
|
|
25
25
|
export const HEAD_GAZE_INTERACTOR_ID = "head-gaze";
|
|
26
|
+
/** The target set this runtime registers with its arbiter. */
|
|
27
|
+
export const INTERACTIONS_POINTER_SET = "interactions";
|
|
26
28
|
export class InteractionRuntime {
|
|
27
29
|
provider;
|
|
28
30
|
hitTester;
|
|
@@ -33,8 +35,13 @@ export class InteractionRuntime {
|
|
|
33
35
|
feedbackEmitter = new Emitter();
|
|
34
36
|
afterSample = new Emitter();
|
|
35
37
|
visualsEmitter = new Emitter();
|
|
38
|
+
drawingEmitter = new Emitter();
|
|
36
39
|
nearPointer;
|
|
40
|
+
pointers;
|
|
41
|
+
targetSet;
|
|
42
|
+
pointerDisplay;
|
|
37
43
|
lastVisuals = [];
|
|
44
|
+
lastDrawings = [];
|
|
38
45
|
velocityTracker;
|
|
39
46
|
lastSources = new Map();
|
|
40
47
|
capabilities;
|
|
@@ -51,6 +58,10 @@ export class InteractionRuntime {
|
|
|
51
58
|
this.provider = options.provider;
|
|
52
59
|
this.hitTester = options.hitTester ?? null;
|
|
53
60
|
this.nearPointer = resolveNearPointerOptions(options.nearPointer);
|
|
61
|
+
this.pointers = options.pointers ?? new PointerArbiter();
|
|
62
|
+
this.targetSet = this.pointers.registerSet(INTERACTIONS_POINTER_SET, "object");
|
|
63
|
+
this.pointerDisplay =
|
|
64
|
+
options.pointerDisplay instanceof PointerDisplay ? options.pointerDisplay : new PointerDisplay(options.pointerDisplay);
|
|
54
65
|
this.eyeGaze = resolveEyeGazeOptions(options.eyeGaze);
|
|
55
66
|
this.gazeConsensus = new GazeConsensus(this.eyeGaze.dwellWindowSeconds);
|
|
56
67
|
const d = options.dwellDefaults;
|
|
@@ -218,6 +229,32 @@ export class InteractionRuntime {
|
|
|
218
229
|
getPointerVisuals(sourceId) {
|
|
219
230
|
return this.lastVisuals.find((visuals) => visuals.sourceId === sourceId);
|
|
220
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* What to draw for every source this frame under the app's pointer display
|
|
234
|
+
* settings: the visuals above resolved through `pointerDrawing`. Published
|
|
235
|
+
* right after `onPointerVisuals`. A binding that draws its own ray and
|
|
236
|
+
* cursor applies exactly this; a native binding hands it to the host.
|
|
237
|
+
*/
|
|
238
|
+
onPointerDrawing(listener) {
|
|
239
|
+
return this.drawingEmitter.subscribe(listener);
|
|
240
|
+
}
|
|
241
|
+
/** The drawing last published for a source, or undefined before its first frame. */
|
|
242
|
+
getPointerDrawing(sourceId) {
|
|
243
|
+
return this.lastDrawings.find((drawing) => drawing.sourceId === sourceId);
|
|
244
|
+
}
|
|
245
|
+
/** The arbiter deciding which pointer owns each source, shared with the UI Extensions hosts when the app passed one. */
|
|
246
|
+
getPointerArbiter() {
|
|
247
|
+
return this.pointers;
|
|
248
|
+
}
|
|
249
|
+
/** The app's pointer display settings; `set` changes them at run time. */
|
|
250
|
+
getPointerDisplay() {
|
|
251
|
+
return this.pointerDisplay;
|
|
252
|
+
}
|
|
253
|
+
/** Whether a source is pressing or grabbing this frame (the selection that recolours its pointer). */
|
|
254
|
+
isSelecting(sourceId) {
|
|
255
|
+
const state = this.sourceStates.get(sourceId);
|
|
256
|
+
return state !== undefined && (state.pressTarget !== null || state.grabTarget !== null);
|
|
257
|
+
}
|
|
221
258
|
// -- frame ------------------------------------------------------------------
|
|
222
259
|
update(dt) {
|
|
223
260
|
if (this.disposed)
|
|
@@ -267,14 +304,19 @@ export class InteractionRuntime {
|
|
|
267
304
|
if (!seen.has(id)) {
|
|
268
305
|
this.forceRelease(id, state);
|
|
269
306
|
this.sourceStates.delete(id);
|
|
307
|
+
this.pointers.forget(id);
|
|
270
308
|
}
|
|
271
309
|
}
|
|
272
310
|
this.updateGaze(sources, dt);
|
|
273
311
|
this.tickBehaviours(sources, dt);
|
|
274
312
|
this.lastVisuals = visuals;
|
|
275
313
|
this.visualsEmitter.emit(visuals);
|
|
314
|
+
const drawings = visuals.map((entry) => this.pointerDisplay.drawing(entry, this.isSelecting(entry.sourceId)));
|
|
315
|
+
this.lastDrawings = drawings;
|
|
316
|
+
this.drawingEmitter.emit(drawings);
|
|
276
317
|
}
|
|
277
318
|
dispose() {
|
|
319
|
+
this.targetSet.dispose();
|
|
278
320
|
if (this.disposed)
|
|
279
321
|
return;
|
|
280
322
|
this.disposed = true;
|
|
@@ -460,7 +502,9 @@ export class InteractionRuntime {
|
|
|
460
502
|
state.touch.reset();
|
|
461
503
|
const gazeTarget = this.eyeGazeTarget;
|
|
462
504
|
const point = gazeTarget ? this.gazePoints.get(gazeTarget) : undefined;
|
|
463
|
-
state.candidate =
|
|
505
|
+
state.candidate =
|
|
506
|
+
gazeTarget && point ? { targetId: gazeTarget, point, distance: 0, set: this.targetSet.id, targetKind: "object" } : null;
|
|
507
|
+
this.pointers.setDecision({ sourceId: source.id, active: "gaze", candidate: state.candidate });
|
|
464
508
|
if (hint && this.targetable(hint.targetId)) {
|
|
465
509
|
state.resolvedBy = "hint";
|
|
466
510
|
return hint.targetId;
|
|
@@ -469,21 +513,31 @@ export class InteractionRuntime {
|
|
|
469
513
|
state.resolvedBy = "gaze";
|
|
470
514
|
return gazeTarget;
|
|
471
515
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
516
|
+
// This runtime's own candidates go to the arbiter, which decides across
|
|
517
|
+
// every target set (a UI panel may be nearer than any interactable). A
|
|
518
|
+
// hand or controller is also reachable by its side, so a UI host that
|
|
519
|
+
// knows only the side offers to the same source.
|
|
520
|
+
if (source.handedness === "left" || source.handedness === "right")
|
|
521
|
+
this.pointers.alias(source.handedness, source.id);
|
|
522
|
+
this.targetSet.offer(source.id, "touch", this.touchCandidate(source, state));
|
|
523
|
+
this.targetSet.offer(source.id, "grab", this.grabCandidate(source));
|
|
524
|
+
this.targetSet.offer(source.id, "ray", this.rayCandidate(source));
|
|
525
|
+
const decision = this.pointers.resolve(source.id);
|
|
526
|
+
state.active = decision.active;
|
|
527
|
+
state.candidate = decision.candidate;
|
|
479
528
|
if (hint && this.targetable(hint.targetId)) {
|
|
480
529
|
state.resolvedBy = "hint";
|
|
481
530
|
return hint.targetId;
|
|
482
531
|
}
|
|
483
|
-
|
|
532
|
+
const active = decision.active;
|
|
533
|
+
if (!decision.candidate || !active)
|
|
534
|
+
return null;
|
|
535
|
+
// Another set's target (a panel) owns the pointer: no interactable is
|
|
536
|
+
// targeted, and the visuals still say where the cursor sits.
|
|
537
|
+
if (decision.candidate.set !== this.targetSet.id)
|
|
484
538
|
return null;
|
|
485
539
|
state.resolvedBy = active === "touch" ? "poke" : active;
|
|
486
|
-
return
|
|
540
|
+
return decision.candidate.targetId;
|
|
487
541
|
}
|
|
488
542
|
/**
|
|
489
543
|
* The touch pointer's candidate this frame, advancing the source's touch
|
|
@@ -628,6 +682,12 @@ export class InteractionRuntime {
|
|
|
628
682
|
state.grabTarget = null;
|
|
629
683
|
}
|
|
630
684
|
}
|
|
685
|
+
// The selection lock: the pointer that owns this source keeps it while
|
|
686
|
+
// it presses or grabs, whatever appears nearer meanwhile.
|
|
687
|
+
const selecting = state.pressTarget !== null || state.grabTarget !== null;
|
|
688
|
+
for (const kind of ["touch", "grab", "ray"]) {
|
|
689
|
+
this.targetSet.setSelecting(source.id, kind, selecting && state.active === kind);
|
|
690
|
+
}
|
|
631
691
|
if (this.capabilities.eyeGaze && source.kind === "gaze")
|
|
632
692
|
this.trackGazeHold(source, state, target);
|
|
633
693
|
}
|
|
@@ -691,7 +751,7 @@ export class InteractionRuntime {
|
|
|
691
751
|
}
|
|
692
752
|
}
|
|
693
753
|
forceRelease(sourceId, state) {
|
|
694
|
-
const info = { id: sourceId, kind: "other", select: 0, squeeze: 0 };
|
|
754
|
+
const info = { id: sourceId, kind: "other", select: 0, squeeze: 0, synthesized: true };
|
|
695
755
|
if (state.pressTarget)
|
|
696
756
|
this.routePressEnd(state.pressTarget, sourceId, info);
|
|
697
757
|
if (state.grabTarget)
|
|
@@ -705,12 +765,12 @@ export class InteractionRuntime {
|
|
|
705
765
|
releaseAllHolds(id) {
|
|
706
766
|
for (const [sourceId, state] of this.sourceStates) {
|
|
707
767
|
if (state.pressTarget === id) {
|
|
708
|
-
this.routePressEnd(id, sourceId, { id: sourceId, kind: "other", select: 0, squeeze: 0 });
|
|
768
|
+
this.routePressEnd(id, sourceId, { id: sourceId, kind: "other", select: 0, squeeze: 0, synthesized: true });
|
|
709
769
|
state.pressTarget = null;
|
|
710
770
|
state.pressed = false;
|
|
711
771
|
}
|
|
712
772
|
if (state.grabTarget === id) {
|
|
713
|
-
this.endGrab(id, sourceId, { id: sourceId, kind: "other", select: 0, squeeze: 0 });
|
|
773
|
+
this.endGrab(id, sourceId, { id: sourceId, kind: "other", select: 0, squeeze: 0, synthesized: true });
|
|
714
774
|
state.grabTarget = null;
|
|
715
775
|
state.grabbed = false;
|
|
716
776
|
}
|